~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Martin Pool
  • Date: 2005-07-07 10:22:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050707102201-2d2a13a25098b101
- rearrange and clear up merged weave

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
import errno
18
18
import patch
19
19
import stat
20
 
from bzrlib.trace import mutter
21
 
from bzrlib.osutils import rename
22
 
import bzrlib
23
 
 
24
 
# XXX: mbp: I'm not totally convinced that we should handle conflicts
25
 
# as part of changeset application, rather than only in the merge
26
 
# operation.
27
 
 
28
 
"""Represent and apply a changeset
29
 
 
30
 
Conflicts in applying a changeset are represented as exceptions.
31
 
"""
32
 
 
 
20
"""
 
21
Represent and apply a changeset
 
22
"""
33
23
__docformat__ = "restructuredtext"
34
24
 
35
25
NULL_ID = "!NULL"
44
34
        newdict[value] = key
45
35
    return newdict
46
36
 
47
 
       
48
 
class ChangeExecFlag(object):
 
37
 
 
38
class PatchApply(object):
 
39
    """Patch application as a kind of content change"""
 
40
    def __init__(self, contents):
 
41
        """Constructor.
 
42
 
 
43
        :param contents: The text of the patch to apply
 
44
        :type contents: str"""
 
45
        self.contents = contents
 
46
 
 
47
    def __eq__(self, other):
 
48
        if not isinstance(other, PatchApply):
 
49
            return False
 
50
        elif self.contents != other.contents:
 
51
            return False
 
52
        else:
 
53
            return True
 
54
 
 
55
    def __ne__(self, other):
 
56
        return not (self == other)
 
57
 
 
58
    def apply(self, filename, conflict_handler, reverse=False):
 
59
        """Applies the patch to the specified file.
 
60
 
 
61
        :param filename: the file to apply the patch to
 
62
        :type filename: str
 
63
        :param reverse: If true, apply the patch in reverse
 
64
        :type reverse: bool
 
65
        """
 
66
        input_name = filename+".orig"
 
67
        try:
 
68
            os.rename(filename, input_name)
 
69
        except OSError, e:
 
70
            if e.errno != errno.ENOENT:
 
71
                raise
 
72
            if conflict_handler.patch_target_missing(filename, self.contents)\
 
73
                == "skip":
 
74
                return
 
75
            os.rename(filename, input_name)
 
76
            
 
77
 
 
78
        status = patch.patch(self.contents, input_name, filename, 
 
79
                                    reverse)
 
80
        os.chmod(filename, os.stat(input_name).st_mode)
 
81
        if status == 0:
 
82
            os.unlink(input_name)
 
83
        elif status == 1:
 
84
            conflict_handler.failed_hunks(filename)
 
85
 
 
86
        
 
87
class ChangeUnixPermissions(object):
49
88
    """This is two-way change, suitable for file modification, creation,
50
89
    deletion"""
51
 
    def __init__(self, old_exec_flag, new_exec_flag):
52
 
        self.old_exec_flag = old_exec_flag
53
 
        self.new_exec_flag = new_exec_flag
 
90
    def __init__(self, old_mode, new_mode):
 
91
        self.old_mode = old_mode
 
92
        self.new_mode = new_mode
54
93
 
55
94
    def apply(self, filename, conflict_handler, reverse=False):
56
95
        if not reverse:
57
 
            from_exec_flag = self.old_exec_flag
58
 
            to_exec_flag = self.new_exec_flag
 
96
            from_mode = self.old_mode
 
97
            to_mode = self.new_mode
59
98
        else:
60
 
            from_exec_flag = self.new_exec_flag
61
 
            to_exec_flag = self.old_exec_flag
 
99
            from_mode = self.new_mode
 
100
            to_mode = self.old_mode
62
101
        try:
63
 
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
 
102
            current_mode = os.stat(filename).st_mode &0777
64
103
        except OSError, e:
65
104
            if e.errno == errno.ENOENT:
66
 
                if conflict_handler.missing_for_exec_flag(filename) == "skip":
 
105
                if conflict_handler.missing_for_chmod(filename) == "skip":
67
106
                    return
68
107
                else:
69
 
                    current_exec_flag = from_exec_flag
 
108
                    current_mode = from_mode
70
109
 
71
 
        if from_exec_flag is not None and current_exec_flag != from_exec_flag:
72
 
            if conflict_handler.wrong_old_exec_flag(filename,
73
 
                        from_exec_flag, current_exec_flag) != "continue":
 
110
        if from_mode is not None and current_mode != from_mode:
 
111
            if conflict_handler.wrong_old_perms(filename, from_mode, 
 
112
                                                current_mode) != "continue":
74
113
                return
75
114
 
76
 
        if to_exec_flag is not None:
77
 
            current_mode = os.stat(filename).st_mode
78
 
            if to_exec_flag:
79
 
                umask = os.umask(0)
80
 
                os.umask(umask)
81
 
                to_mode = current_mode | (0100 & ~umask)
82
 
                # Enable x-bit for others only if they can read it.
83
 
                if current_mode & 0004:
84
 
                    to_mode |= 0001 & ~umask
85
 
                if current_mode & 0040:
86
 
                    to_mode |= 0010 & ~umask
87
 
            else:
88
 
                to_mode = current_mode & ~0111
 
115
        if to_mode is not None:
89
116
            try:
90
117
                os.chmod(filename, to_mode)
91
118
            except IOError, e:
92
119
                if e.errno == errno.ENOENT:
93
 
                    conflict_handler.missing_for_exec_flag(filename)
 
120
                    conflict_handler.missing_for_chmod(filename)
94
121
 
95
122
    def __eq__(self, other):
96
 
        return (isinstance(other, ChangeExecFlag) and
97
 
                self.old_exec_flag == other.old_exec_flag and
98
 
                self.new_exec_flag == other.new_exec_flag)
 
123
        if not isinstance(other, ChangeUnixPermissions):
 
124
            return False
 
125
        elif self.old_mode != other.old_mode:
 
126
            return False
 
127
        elif self.new_mode != other.new_mode:
 
128
            return False
 
129
        else:
 
130
            return True
99
131
 
100
132
    def __ne__(self, other):
101
133
        return not (self == other)
102
134
 
103
 
 
104
135
def dir_create(filename, conflict_handler, reverse):
105
136
    """Creates the directory, or deletes it if reverse is true.  Intended to be
106
137
    used with ReplaceContents.
126
157
        try:
127
158
            os.rmdir(filename)
128
159
        except OSError, e:
129
 
            if e.errno != errno.ENOTEMPTY:
 
160
            if e.errno != 39:
130
161
                raise
131
162
            if conflict_handler.rmdir_non_empty(filename) == "skip":
132
163
                return
133
164
            os.rmdir(filename)
134
165
 
 
166
                
 
167
            
135
168
 
136
169
class SymlinkCreate(object):
137
170
    """Creates or deletes a symlink (for use with ReplaceContents)"""
333
366
 
334
367
 
335
368
class Diff3Merge(object):
336
 
    def __init__(self, file_id, base, other):
337
 
        self.file_id = file_id
338
 
        self.base = base
339
 
        self.other = other
 
369
    def __init__(self, base_file, other_file):
 
370
        self.base_file = base_file
 
371
        self.other_file = other_file
340
372
 
341
373
    def __eq__(self, other):
342
374
        if not isinstance(other, Diff3Merge):
343
375
            return False
344
 
        return (self.base == other.base and 
345
 
                self.other == other.other and self.file_id == other.file_id)
 
376
        return (self.base_file == other.base_file and 
 
377
                self.other_file == other.other_file)
346
378
 
347
379
    def __ne__(self, other):
348
380
        return not (self == other)
349
381
 
350
382
    def apply(self, filename, conflict_handler, reverse=False):
351
 
        new_file = filename+".new"
352
 
        base_file = self.base.readonly_path(self.file_id)
353
 
        other_file = self.other.readonly_path(self.file_id)
 
383
        new_file = filename+".new" 
354
384
        if not reverse:
355
 
            base = base_file
356
 
            other = other_file
 
385
            base = self.base_file
 
386
            other = self.other_file
357
387
        else:
358
 
            base = other_file
359
 
            other = base_file
 
388
            base = self.other_file
 
389
            other = self.base_file
360
390
        status = patch.diff3(new_file, filename, base, other)
361
391
        if status == 0:
362
392
            os.chmod(new_file, os.stat(filename).st_mode)
363
 
            rename(new_file, filename)
 
393
            os.rename(new_file, filename)
364
394
            return
365
395
        else:
366
396
            assert(status == 1)
367
 
            def get_lines(filename):
368
 
                my_file = file(filename, "rb")
369
 
                lines = my_file.readlines()
370
 
                my_file.close()
371
 
                return lines
372
 
            base_lines = get_lines(base)
373
 
            other_lines = get_lines(other)
374
 
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
375
 
                                            other_lines)
 
397
            conflict_handler.merge_conflict(new_file, filename, base, other)
376
398
 
377
399
 
378
400
def CreateDir():
634
656
                return None
635
657
            return self.path
636
658
 
637
 
    def summarize_name(self, reverse=False):
 
659
    def summarize_name(self, changeset, reverse=False):
638
660
        """Produce a one-line summary of the filename.  Indicates renames as
639
661
        old => new, indicates creation as None => new, indicates deletion as
640
662
        old => None.
671
693
        :type reverse: bool
672
694
        :rtype: str
673
695
        """
674
 
        mutter("Finding new path for %s" % self.summarize_name())
675
696
        if reverse:
676
697
            parent = self.parent
677
698
            to_dir = self.dir
696
717
        if from_dir == to_dir:
697
718
            dir = os.path.dirname(id_map[self.id])
698
719
        else:
699
 
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
700
720
            parent_entry = changeset.entries[parent]
701
721
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
702
722
        if from_name == to_name:
839
859
            if src_path is not None:
840
860
                src_path = os.path.join(dir, src_path)
841
861
                try:
842
 
                    rename(src_path, to_name)
 
862
                    os.rename(src_path, to_name)
843
863
                    temp_name[entry.id] = to_name
844
864
                except OSError, e:
845
865
                    if e.errno != errno.ENOENT:
871
891
            continue
872
892
        new_path = os.path.join(dir, new_tree_path)
873
893
        old_path = changed_inventory.get(entry.id)
874
 
        if bzrlib.osutils.lexists(new_path):
 
894
        if os.path.exists(new_path):
875
895
            if conflict_handler.target_exists(entry, new_path, old_path) == \
876
896
                "skip":
877
897
                continue
882
902
            if old_path is None:
883
903
                continue
884
904
            try:
885
 
                rename(old_path, new_path)
 
905
                os.rename(old_path, new_path)
886
906
                changed_inventory[entry.id] = new_tree_path
887
907
            except OSError, e:
888
908
                raise Exception ("%s is missing" % new_path)
923
943
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
924
944
        self.this_path = this_path
925
945
 
 
946
class MergePermissionConflict(Exception):
 
947
    def __init__(self, this_path, base_path, other_path):
 
948
        this_perms = os.stat(this_path).st_mode & 0755
 
949
        base_perms = os.stat(base_path).st_mode & 0755
 
950
        other_perms = os.stat(other_path).st_mode & 0755
 
951
        msg = """Conflicting permission for %s
 
952
this: %o
 
953
base: %o
 
954
other: %o
 
955
        """ % (this_path, this_perms, base_perms, other_perms)
 
956
        self.this_path = this_path
 
957
        self.base_path = base_path
 
958
        self.other_path = other_path
 
959
        Exception.__init__(self, msg)
 
960
 
926
961
class WrongOldContents(Exception):
927
962
    def __init__(self, filename):
928
963
        msg = "Contents mismatch deleting %s" % filename
929
964
        self.filename = filename
930
965
        Exception.__init__(self, msg)
931
966
 
932
 
class WrongOldExecFlag(Exception):
933
 
    def __init__(self, filename, old_exec_flag, new_exec_flag):
934
 
        msg = "Executable flag missmatch on %s:\n" \
935
 
        "Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
 
967
class WrongOldPermissions(Exception):
 
968
    def __init__(self, filename, old_perms, new_perms):
 
969
        msg = "Permission missmatch on %s:\n" \
 
970
        "Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
936
971
        self.filename = filename
937
972
        Exception.__init__(self, msg)
938
973
 
956
991
        Exception.__init__(self, msg)
957
992
        self.filename = filename
958
993
 
959
 
class MissingForSetExec(Exception):
 
994
class MissingPermsFile(Exception):
960
995
    def __init__(self, filename):
961
996
        msg = "Attempt to change permissions on  %s, which does not exist" %\
962
997
            filename
990
1025
 
991
1026
 
992
1027
class ExceptionConflictHandler(object):
993
 
    """Default handler for merge exceptions.
994
 
 
995
 
    This throws an error on any kind of conflict.  Conflict handlers can
996
 
    descend from this class if they have a better way to handle some or
997
 
    all types of conflict.
998
 
    """
 
1028
    def __init__(self, dir):
 
1029
        self.dir = dir
 
1030
    
999
1031
    def missing_parent(self, pathname):
1000
1032
        parent = os.path.dirname(pathname)
1001
1033
        raise Exception("Parent directory missing for %s" % pathname)
1012
1044
    def rename_conflict(self, id, this_name, base_name, other_name):
1013
1045
        raise RenameConflict(id, this_name, base_name, other_name)
1014
1046
 
1015
 
    def move_conflict(self, id, this_dir, base_dir, other_dir):
 
1047
    def move_conflict(self, id, inventory):
 
1048
        this_dir = inventory.this.get_dir(id)
 
1049
        base_dir = inventory.base.get_dir(id)
 
1050
        other_dir = inventory.other.get_dir(id)
1016
1051
        raise MoveConflict(id, this_dir, base_dir, other_dir)
1017
1052
 
1018
 
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
 
1053
    def merge_conflict(self, new_file, this_path, base_path, other_path):
1019
1054
        os.unlink(new_file)
1020
1055
        raise MergeConflict(this_path)
1021
1056
 
 
1057
    def permission_conflict(self, this_path, base_path, other_path):
 
1058
        raise MergePermissionConflict(this_path, base_path, other_path)
 
1059
 
1022
1060
    def wrong_old_contents(self, filename, expected_contents):
1023
1061
        raise WrongOldContents(filename)
1024
1062
 
1025
1063
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1026
1064
        raise RemoveContentsConflict(filename)
1027
1065
 
1028
 
    def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1029
 
        raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
 
1066
    def wrong_old_perms(self, filename, old_perms, new_perms):
 
1067
        raise WrongOldPermissions(filename, old_perms, new_perms)
1030
1068
 
1031
1069
    def rmdir_non_empty(self, filename):
1032
1070
        raise DeletingNonEmptyDirectory(filename)
1037
1075
    def patch_target_missing(self, filename, contents):
1038
1076
        raise PatchTargetMissing(filename)
1039
1077
 
1040
 
    def missing_for_exec_flag(self, filename):
1041
 
        raise MissingForExecFlag(filename)
 
1078
    def missing_for_chmod(self, filename):
 
1079
        raise MissingPermsFile(filename)
1042
1080
 
1043
1081
    def missing_for_rm(self, filename, change):
1044
1082
        raise MissingForRm(filename)
1046
1084
    def missing_for_rename(self, filename):
1047
1085
        raise MissingForRename(filename)
1048
1086
 
1049
 
    def missing_for_merge(self, file_id, other_path):
1050
 
        raise MissingForMerge(other_path)
 
1087
    def missing_for_merge(self, file_id, inventory):
 
1088
        raise MissingForMerge(inventory.other.get_path(file_id))
1051
1089
 
1052
1090
    def new_contents_conflict(self, filename, other_contents):
1053
1091
        raise NewContentsConflict(filename)
1054
1092
 
1055
 
    def finalize(self):
 
1093
    def finalize():
1056
1094
        pass
1057
1095
 
1058
1096
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
1071
1109
    :rtype: Dictionary
1072
1110
    """
1073
1111
    if conflict_handler is None:
1074
 
        conflict_handler = ExceptionConflictHandler()
 
1112
        conflict_handler = ExceptionConflictHandler(dir)
1075
1113
    temp_dir = os.path.join(dir, "bzr-tree-change")
1076
1114
    try:
1077
1115
        os.mkdir(temp_dir)
1254
1292
        return new_meta
1255
1293
    elif new_meta is None:
1256
1294
        return old_meta
1257
 
    elif (isinstance(old_meta, ChangeExecFlag) and
1258
 
          isinstance(new_meta, ChangeExecFlag)):
1259
 
        return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
 
1295
    elif isinstance(old_meta, ChangeUnixPermissions) and \
 
1296
        isinstance(new_meta, ChangeUnixPermissions):
 
1297
        return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1260
1298
    else:
1261
1299
        return ApplySequence(old_meta, new_meta)
1262
1300
 
1274
1312
        self.full_path = full_path
1275
1313
        self.stat_result = stat_result
1276
1314
 
1277
 
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1278
 
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1279
 
 
 
1315
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
 
1316
    return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1280
1317
 
1281
1318
class ChangesetGenerator(object):
1282
 
    def __init__(self, tree_a, tree_b, interesting_ids=None):
 
1319
    def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1283
1320
        object.__init__(self)
1284
1321
        self.tree_a = tree_a
1285
1322
        self.tree_b = tree_b
1286
 
        self._interesting_ids = interesting_ids
 
1323
        if inventory_a is not None:
 
1324
            self.inventory_a = inventory_a
 
1325
        else:
 
1326
            self.inventory_a = tree_a.inventory()
 
1327
        if inventory_b is not None:
 
1328
            self.inventory_b = inventory_b
 
1329
        else:
 
1330
            self.inventory_b = tree_b.inventory()
 
1331
        self.r_inventory_a = self.reverse_inventory(self.inventory_a)
 
1332
        self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1287
1333
 
1288
 
    def iter_both_tree_ids(self):
1289
 
        for file_id in self.tree_a:
1290
 
            yield file_id
1291
 
        for file_id in self.tree_b:
1292
 
            if file_id not in self.tree_a:
1293
 
                yield file_id
 
1334
    def reverse_inventory(self, inventory):
 
1335
        r_inventory = {}
 
1336
        for entry in inventory.itervalues():
 
1337
            if entry.id is None:
 
1338
                continue
 
1339
            r_inventory[entry.id] = entry
 
1340
        return r_inventory
1294
1341
 
1295
1342
    def __call__(self):
1296
1343
        cset = Changeset()
1297
 
        for file_id in self.iter_both_tree_ids():
1298
 
            cs_entry = self.make_entry(file_id)
 
1344
        for entry in self.inventory_a.itervalues():
 
1345
            if entry.id is None:
 
1346
                continue
 
1347
            cs_entry = self.make_entry(entry.id)
1299
1348
            if cs_entry is not None and not cs_entry.is_boring():
1300
1349
                cset.add_entry(cs_entry)
1301
1350
 
 
1351
        for entry in self.inventory_b.itervalues():
 
1352
            if entry.id is None:
 
1353
                continue
 
1354
            if not self.r_inventory_a.has_key(entry.id):
 
1355
                cs_entry = self.make_entry(entry.id)
 
1356
                if cs_entry is not None and not cs_entry.is_boring():
 
1357
                    cset.add_entry(cs_entry)
1302
1358
        for entry in list(cset.entries.itervalues()):
1303
1359
            if entry.parent != entry.new_parent:
1304
1360
                if not cset.entries.has_key(entry.parent) and\
1312
1368
                    cset.add_entry(parent_entry)
1313
1369
        return cset
1314
1370
 
1315
 
    def iter_inventory(self, tree):
1316
 
        for file_id in tree:
1317
 
            yield self.get_entry(file_id, tree)
1318
 
 
1319
 
    def get_entry(self, file_id, tree):
1320
 
        if not tree.has_or_had_id(file_id):
1321
 
            return None
1322
 
        return tree.tree.inventory[file_id]
1323
 
 
1324
 
    def get_entry_parent(self, entry):
1325
 
        if entry is None:
1326
 
            return None
1327
 
        return entry.parent_id
1328
 
 
1329
 
    def get_path(self, file_id, tree):
1330
 
        if not tree.has_or_had_id(file_id):
1331
 
            return None
1332
 
        path = tree.id2path(file_id)
1333
 
        if path == '':
1334
 
            return './.'
1335
 
        else:
1336
 
            return path
1337
 
 
1338
 
    def make_basic_entry(self, file_id, only_interesting):
1339
 
        entry_a = self.get_entry(file_id, self.tree_a)
1340
 
        entry_b = self.get_entry(file_id, self.tree_b)
 
1371
    def get_entry_parent(self, entry, inventory):
 
1372
        if entry is None:
 
1373
            return None
 
1374
        if entry.path == "./.":
 
1375
            return NULL_ID
 
1376
        dirname = os.path.dirname(entry.path)
 
1377
        if dirname == ".":
 
1378
            dirname = "./."
 
1379
        parent = inventory[dirname]
 
1380
        return parent.id
 
1381
 
 
1382
    def get_paths(self, entry, tree):
 
1383
        if entry is None:
 
1384
            return (None, None)
 
1385
        full_path = tree.readonly_path(entry.id)
 
1386
        if entry.path == ".":
 
1387
            return ("", full_path)
 
1388
        return (entry.path, full_path)
 
1389
 
 
1390
    def make_basic_entry(self, id, only_interesting):
 
1391
        entry_a = self.r_inventory_a.get(id)
 
1392
        entry_b = self.r_inventory_b.get(id)
1341
1393
        if only_interesting and not self.is_interesting(entry_a, entry_b):
1342
 
            return None
1343
 
        parent = self.get_entry_parent(entry_a)
1344
 
        path = self.get_path(file_id, self.tree_a)
1345
 
        cs_entry = ChangesetEntry(file_id, parent, path)
1346
 
        new_parent = self.get_entry_parent(entry_b)
1347
 
 
1348
 
        new_path = self.get_path(file_id, self.tree_b)
 
1394
            return (None, None, None)
 
1395
        parent = self.get_entry_parent(entry_a, self.inventory_a)
 
1396
        (path, full_path_a) = self.get_paths(entry_a, self.tree_a)
 
1397
        cs_entry = ChangesetEntry(id, parent, path)
 
1398
        new_parent = self.get_entry_parent(entry_b, self.inventory_b)
 
1399
 
 
1400
 
 
1401
        (new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1349
1402
 
1350
1403
        cs_entry.new_path = new_path
1351
1404
        cs_entry.new_parent = new_parent
1352
 
        return cs_entry
 
1405
        return (cs_entry, full_path_a, full_path_b)
1353
1406
 
1354
1407
    def is_interesting(self, entry_a, entry_b):
1355
 
        if self._interesting_ids is None:
1356
 
            return True
1357
1408
        if entry_a is not None:
1358
 
            file_id = entry_a.file_id
1359
 
        elif entry_b is not None:
1360
 
            file_id = entry_b.file_id
1361
 
        else:
1362
 
            return False
1363
 
        return file_id in self._interesting_ids
 
1409
            if entry_a.interesting:
 
1410
                return True
 
1411
        if entry_b is not None:
 
1412
            if entry_b.interesting:
 
1413
                return True
 
1414
        return False
1364
1415
 
1365
1416
    def make_boring_entry(self, id):
1366
 
        cs_entry = self.make_basic_entry(id, only_interesting=False)
 
1417
        (cs_entry, full_path_a, full_path_b) = \
 
1418
            self.make_basic_entry(id, only_interesting=False)
1367
1419
        if cs_entry.is_creation_or_deletion():
1368
1420
            return self.make_entry(id, only_interesting=False)
1369
1421
        else:
1371
1423
        
1372
1424
 
1373
1425
    def make_entry(self, id, only_interesting=True):
1374
 
        cs_entry = self.make_basic_entry(id, only_interesting)
 
1426
        (cs_entry, full_path_a, full_path_b) = \
 
1427
            self.make_basic_entry(id, only_interesting)
1375
1428
 
1376
1429
        if cs_entry is None:
1377
1430
            return None
1378
 
 
1379
 
        full_path_a = self.tree_a.readonly_path(id)
1380
 
        full_path_b = self.tree_b.readonly_path(id)
 
1431
       
1381
1432
        stat_a = self.lstat(full_path_a)
1382
1433
        stat_b = self.lstat(full_path_b)
1383
 
 
1384
 
        cs_entry.metadata_change = self.make_exec_flag_change(stat_a, stat_b)
1385
 
 
1386
 
        if id in self.tree_a and id in self.tree_b:
1387
 
            a_sha1 = self.tree_a.get_file_sha1(id)
1388
 
            b_sha1 = self.tree_b.get_file_sha1(id)
1389
 
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1390
 
                return cs_entry
1391
 
 
 
1434
        if stat_b is None:
 
1435
            cs_entry.new_parent = None
 
1436
            cs_entry.new_path = None
 
1437
        
 
1438
        cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1392
1439
        cs_entry.contents_change = self.make_contents_change(full_path_a,
1393
1440
                                                             stat_a, 
1394
1441
                                                             full_path_b, 
1395
1442
                                                             stat_b)
1396
1443
        return cs_entry
1397
1444
 
1398
 
    def make_exec_flag_change(self, stat_a, stat_b):
1399
 
        exec_flag_a = exec_flag_b = None
 
1445
    def make_mode_change(self, stat_a, stat_b):
 
1446
        mode_a = None
1400
1447
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1401
 
            exec_flag_a = bool(stat_a.st_mode & 0111)
 
1448
            mode_a = stat_a.st_mode & 0777
 
1449
        mode_b = None
1402
1450
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1403
 
            exec_flag_b = bool(stat_b.st_mode & 0111)
1404
 
        if exec_flag_a == exec_flag_b:
 
1451
            mode_b = stat_b.st_mode & 0777
 
1452
        if mode_a == mode_b:
1405
1453
            return None
1406
 
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
 
1454
        return ChangeUnixPermissions(mode_a, mode_b)
1407
1455
 
1408
1456
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1409
1457
        if stat_a is None and stat_b is None:
1416
1464
            if stat_a.st_ino == stat_b.st_ino and \
1417
1465
                stat_a.st_dev == stat_b.st_dev:
1418
1466
                return None
 
1467
            if file(full_path_a, "rb").read() == \
 
1468
                file(full_path_b, "rb").read():
 
1469
                return None
 
1470
 
 
1471
            patch_contents = patch.diff(full_path_a, 
 
1472
                                        file(full_path_b, "rb").read())
 
1473
            if patch_contents is None:
 
1474
                return None
 
1475
            return PatchApply(patch_contents)
1419
1476
 
1420
1477
        a_contents = self.get_contents(stat_a, full_path_a)
1421
1478
        b_contents = self.get_contents(stat_b, full_path_b)
1469
1526
 
1470
1527
 
1471
1528
        
1472
 
# XXX: Can't we unify this with the regular inventory object
 
1529
    
1473
1530
class Inventory(object):
1474
1531
    def __init__(self, inventory):
1475
1532
        self.inventory = inventory