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
17
"""Represent and apply a changeset.
19
Conflicts in applying a changeset are represented as exceptions.
21
This only handles the in-memory objects representing changesets, which are
22
primarily used by the merge code.
28
from tempfile import mkdtemp
29
from shutil import rmtree
30
from itertools import izip
32
from bzrlib.trace import mutter, warning
33
from bzrlib.osutils import rename, sha_file
35
from bzrlib.errors import BzrCheckError
21
Represent and apply a changeset
37
23
__docformat__ = "restructuredtext"
41
class OldFailedTreeOp(Exception):
43
Exception.__init__(self, "bzr-tree-change contains files from a"
44
" previous failed merge operation.")
45
28
def invert_dict(dict):
47
30
for (key,value) in dict.iteritems():
48
31
newdict[value] = key
52
class ChangeExecFlag(object):
36
"""Patch application as a kind of content change"""
37
def __init__(self, contents):
40
:param contents: The text of the patch to apply
41
:type contents: str"""
42
self.contents = contents
44
def __eq__(self, other):
45
if not isinstance(other, PatchApply):
47
elif self.contents != other.contents:
52
def __ne__(self, other):
53
return not (self == other)
55
def apply(self, filename, conflict_handler, reverse=False):
56
"""Applies the patch to the specified file.
58
:param filename: the file to apply the patch to
60
:param reverse: If true, apply the patch in reverse
63
input_name = filename+".orig"
65
os.rename(filename, input_name)
67
if e.errno != errno.ENOENT:
69
if conflict_handler.patch_target_missing(filename, self.contents)\
72
os.rename(filename, input_name)
75
status = patch.patch(self.contents, input_name, filename,
77
os.chmod(filename, os.stat(input_name).st_mode)
81
conflict_handler.failed_hunks(filename)
84
class ChangeUnixPermissions:
53
85
"""This is two-way change, suitable for file modification, creation,
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
87
def __init__(self, old_mode, new_mode):
88
self.old_mode = old_mode
89
self.new_mode = new_mode
59
91
def apply(self, filename, conflict_handler, reverse=False):
61
from_exec_flag = self.old_exec_flag
62
to_exec_flag = self.new_exec_flag
93
from_mode = self.old_mode
94
to_mode = self.new_mode
64
from_exec_flag = self.new_exec_flag
65
to_exec_flag = self.old_exec_flag
96
from_mode = self.new_mode
97
to_mode = self.old_mode
67
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
99
current_mode = os.stat(filename).st_mode &0777
68
100
except OSError, e:
69
101
if e.errno == errno.ENOENT:
70
if conflict_handler.missing_for_exec_flag(filename) == "skip":
102
if conflict_handler.missing_for_chmod(filename) == "skip":
73
current_exec_flag = from_exec_flag
105
current_mode = from_mode
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":
107
if from_mode is not None and current_mode != from_mode:
108
if conflict_handler.wrong_old_perms(filename, from_mode,
109
current_mode) != "continue":
80
if to_exec_flag is not None:
81
current_mode = os.stat(filename).st_mode
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
92
to_mode = current_mode & ~0111
112
if to_mode is not None:
94
114
os.chmod(filename, to_mode)
95
115
except IOError, e:
96
116
if e.errno == errno.ENOENT:
97
conflict_handler.missing_for_exec_flag(filename)
117
conflict_handler.missing_for_chmod(filename)
99
119
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)
120
if not isinstance(other, ChangeUnixPermissions):
122
elif self.old_mode != other.old_mode:
124
elif self.new_mode != other.new_mode:
104
129
def __ne__(self, other):
105
130
return not (self == other)
108
132
def dir_create(filename, conflict_handler, reverse):
109
133
"""Creates the directory, or deletes it if reverse is true. Intended to be
110
134
used with ReplaceContents.
240
class TreeFileCreate(object):
241
"""Create or delete a file (for use with ReplaceContents)"""
242
def __init__(self, tree, file_id):
245
:param contents: The contents of the file to write
249
self.file_id = file_id
252
return "TreeFileCreate(%s)" % self.file_id
254
def __eq__(self, other):
255
if not isinstance(other, TreeFileCreate):
257
return self.tree.get_file_sha1(self.file_id) == \
258
other.tree.get_file_sha1(other.file_id)
260
def __ne__(self, other):
261
return not (self == other)
263
def write_file(self, filename):
264
outfile = file(filename, "wb")
265
for line in self.tree.get_file(self.file_id):
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)
272
def __call__(self, filename, conflict_handler, reverse):
273
"""Create or delete a file
275
:param filename: The name of the file to create
277
:param reverse: Delete the file instead of creating it
282
self.write_file(filename)
284
if e.errno == errno.ENOENT:
285
if conflict_handler.missing_parent(filename)=="continue":
286
self.write_file(filename)
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":
299
if e.errno != errno.ENOENT:
301
if conflict_handler.missing_for_rm(filename, undo) == "skip":
306
263
def reversed(sequence):
307
264
max = len(sequence) - 1
308
265
for i in range(len(sequence)):
309
266
yield sequence[max - i]
311
class ReplaceContents(object):
268
class ReplaceContents:
312
269
"""A contents-replacement framework. It allows a file/directory/symlink to
313
270
be created, deleted, or replaced with another file/directory/symlink.
314
271
Arguments must be callable with (filename, reverse).
411
362
change.apply(filename, conflict_handler, reverse)
414
class Diff3Merge(object):
415
history_based = False
416
def __init__(self, file_id, base, other):
417
self.file_id = file_id
421
def is_creation(self):
424
def is_deletion(self):
366
def __init__(self, base_file, other_file):
367
self.base_file = base_file
368
self.other_file = other_file
427
370
def __eq__(self, other):
428
371
if not isinstance(other, Diff3Merge):
430
return (self.base == other.base and
431
self.other == other.other and self.file_id == other.file_id)
373
return (self.base_file == other.base_file and
374
self.other_file == other.other_file)
433
376
def __ne__(self, other):
434
377
return not (self == other)
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)
444
379
def apply(self, filename, conflict_handler, reverse=False):
446
temp_dir = mkdtemp(prefix="bzr-")
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)
457
status = bzrlib.patch.diff3(new_file, filename, base, other)
459
os.chmod(new_file, os.stat(filename).st_mode)
460
rename(new_file, filename)
464
def get_lines(filename):
465
my_file = file(filename, "rb")
466
lines = my_file.readlines()
469
base_lines = get_lines(base)
470
other_lines = get_lines(other)
471
conflict_handler.merge_conflict(new_file, filename, base_lines,
380
new_file = filename+".new"
382
base = self.base_file
383
other = self.other_file
385
base = self.other_file
386
other = self.base_file
387
status = patch.diff3(new_file, filename, base, other)
389
os.chmod(new_file, os.stat(filename).st_mode)
390
os.rename(new_file, filename)
394
conflict_handler.merge_conflict(new_file, filename, base, other)
930
842
:return: a mapping of id to temporary name
931
843
:rtype: Dictionary
845
temp_dir = os.path.join(dir, "temp")
934
847
for i in range(len(source_entries)):
935
848
entry = source_entries[i]
936
849
if entry.is_deletion(reverse):
937
850
path = os.path.join(dir, inventory[entry.id])
938
851
entry.apply(path, conflict_handler, reverse)
939
temp_name[entry.id] = None
941
elif entry.needs_rename():
942
if entry.is_creation(reverse):
944
to_name = os.path.join(temp_dir, str(i))
854
to_name = temp_dir+"/"+str(i)
945
855
src_path = inventory.get(entry.id)
946
856
if src_path is not None:
947
857
src_path = os.path.join(dir, src_path)
949
rename(src_path, to_name)
859
os.rename(src_path, to_name)
950
860
temp_name[entry.id] = to_name
951
861
except OSError, e:
952
862
if e.errno != errno.ENOENT:
954
if conflict_handler.missing_for_rename(src_path, to_name) \
864
if conflict_handler.missing_for_rename(src_path) == "skip":
961
def rename_to_new_create(changed_inventory, target_entries, inventory,
962
changeset, dir, conflict_handler, reverse):
870
def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
871
conflict_handler, reverse):
963
872
"""Rename entries with temp names to their final names, create new files.
965
:param changed_inventory: A mapping of id to temporary name
966
:type changed_inventory: Dictionary
874
:param temp_name: A mapping of id to temporary name
875
:type temp_name: Dictionary
967
876
:param target_entries: The entries to apply changes to
968
877
:type target_entries: List of `ChangesetEntry`
969
878
:param changeset: The changeset to apply
974
883
:type reverse: bool
976
885
for entry in target_entries:
977
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
978
if new_tree_path is None:
886
new_path = entry.get_new_path(inventory, changeset, reverse)
980
new_path = os.path.join(dir, new_tree_path)
981
old_path = changed_inventory.get(entry.id)
982
if bzrlib.osutils.lexists(new_path):
889
new_path = os.path.join(dir, new_path)
890
old_path = temp_name.get(entry.id)
891
if os.path.exists(new_path):
983
892
if conflict_handler.target_exists(entry, new_path, old_path) == \
986
895
if entry.is_creation(reverse):
987
896
entry.apply(new_path, conflict_handler, reverse)
988
changed_inventory[entry.id] = new_tree_path
989
elif entry.needs_rename():
990
if entry.is_deletion(reverse):
992
898
if old_path is None:
995
mutter('rename %s to final name %s', old_path, new_path)
996
rename(old_path, new_path)
997
changed_inventory[entry.id] = new_tree_path
901
os.rename(old_path, new_path)
998
902
except OSError, e:
999
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
1000
% (old_path, new_path, entry, e))
903
raise Exception ("%s is missing" % new_path)
1002
905
class TargetExists(Exception):
1003
906
def __init__(self, entry, target):
1035
938
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1036
939
self.this_path = this_path
941
class MergePermissionConflict(Exception):
942
def __init__(self, this_path, base_path, other_path):
943
this_perms = os.stat(this_path).st_mode & 0755
944
base_perms = os.stat(base_path).st_mode & 0755
945
other_perms = os.stat(other_path).st_mode & 0755
946
msg = """Conflicting permission for %s
950
""" % (this_path, this_perms, base_perms, other_perms)
951
self.this_path = this_path
952
self.base_path = base_path
953
self.other_path = other_path
954
Exception.__init__(self, msg)
1038
956
class WrongOldContents(Exception):
1039
957
def __init__(self, filename):
1040
958
msg = "Contents mismatch deleting %s" % filename
1041
959
self.filename = filename
1042
960
Exception.__init__(self, msg)
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)
962
class WrongOldPermissions(Exception):
963
def __init__(self, filename, old_perms, new_perms):
964
msg = "Permission missmatch on %s:\n" \
965
"Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
1048
966
self.filename = filename
1049
967
Exception.__init__(self, msg)
1085
1003
class MissingForRename(Exception):
1086
def __init__(self, filename, to_path):
1087
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1004
def __init__(self, filename):
1005
msg = "Attempt to move missing path %s" % (filename)
1088
1006
Exception.__init__(self, msg)
1089
1007
self.filename = filename
1091
class NewContentsConflict(Exception):
1092
def __init__(self, filename):
1093
msg = "Conflicting contents for new file %s" % (filename)
1094
Exception.__init__(self, msg)
1096
class WeaveMergeConflict(Exception):
1097
def __init__(self, filename):
1098
msg = "Conflicting contents for file %s" % (filename)
1099
Exception.__init__(self, msg)
1101
class ThreewayContentsConflict(Exception):
1102
def __init__(self, filename):
1103
msg = "Conflicting contents for file %s" % (filename)
1104
Exception.__init__(self, msg)
1107
class MissingForMerge(Exception):
1108
def __init__(self, filename):
1109
msg = "The file %s was modified, but does not exist in this tree"\
1111
Exception.__init__(self, msg)
1114
class ExceptionConflictHandler(object):
1115
"""Default handler for merge exceptions.
1117
This throws an error on any kind of conflict. Conflict handlers can
1118
descend from this class if they have a better way to handle some or
1119
all types of conflict.
1009
class ExceptionConflictHandler:
1010
def __init__(self, dir):
1121
1013
def missing_parent(self, pathname):
1122
1014
parent = os.path.dirname(pathname)
1123
1015
raise Exception("Parent directory missing for %s" % pathname)
1134
1026
def rename_conflict(self, id, this_name, base_name, other_name):
1135
1027
raise RenameConflict(id, this_name, base_name, other_name)
1137
def move_conflict(self, id, this_dir, base_dir, other_dir):
1029
def move_conflict(self, id, inventory):
1030
this_dir = inventory.this.get_dir(id)
1031
base_dir = inventory.base.get_dir(id)
1032
other_dir = inventory.other.get_dir(id)
1138
1033
raise MoveConflict(id, this_dir, base_dir, other_dir)
1140
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1035
def merge_conflict(self, new_file, this_path, base_path, other_path):
1141
1036
os.unlink(new_file)
1142
1037
raise MergeConflict(this_path)
1039
def permission_conflict(self, this_path, base_path, other_path):
1040
raise MergePermissionConflict(this_path, base_path, other_path)
1144
1042
def wrong_old_contents(self, filename, expected_contents):
1145
1043
raise WrongOldContents(filename)
1147
1045
def rem_contents_conflict(self, filename, this_contents, base_contents):
1148
1046
raise RemoveContentsConflict(filename)
1150
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1151
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1048
def wrong_old_perms(self, filename, old_perms, new_perms):
1049
raise WrongOldPermissions(filename, old_perms, new_perms)
1153
1051
def rmdir_non_empty(self, filename):
1154
1052
raise DeletingNonEmptyDirectory(filename)
1159
1057
def patch_target_missing(self, filename, contents):
1160
1058
raise PatchTargetMissing(filename)
1162
def missing_for_exec_flag(self, filename):
1163
raise MissingForExecFlag(filename)
1060
def missing_for_chmod(self, filename):
1061
raise MissingPermsFile(filename)
1165
1063
def missing_for_rm(self, filename, change):
1166
1064
raise MissingForRm(filename)
1168
def missing_for_rename(self, filename, to_path):
1169
raise MissingForRename(filename, to_path)
1171
def missing_for_merge(self, file_id, other_path):
1172
raise MissingForMerge(other_path)
1174
def new_contents_conflict(self, filename, other_contents):
1175
raise NewContentsConflict(filename)
1177
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1178
raise WeaveMergeConflict(filename)
1180
def threeway_contents_conflict(self, filename, this_contents,
1181
base_contents, other_contents):
1182
raise ThreewayContentsConflict(filename)
1066
def missing_for_rename(self, filename):
1067
raise MissingForRename(filename)
1187
1069
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1188
1070
reverse=False):
1232
1099
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1235
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1236
temp_dir, conflict_handler,
1102
temp_name = rename_to_temp_delete(source_entries, inventory, dir,
1103
conflict_handler, reverse)
1239
rename_to_new_create(changed_inventory, target_entries, inventory,
1240
changeset, dir, conflict_handler, reverse)
1105
rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
1106
conflict_handler, reverse)
1241
1107
os.rmdir(temp_dir)
1242
return changed_inventory
1108
r_inventory = invert_dict(inventory)
1109
new_entries, removed_entries = get_inventory_change(inventory,
1110
r_inventory, changeset, reverse)
1112
for path, file_id in new_entries.iteritems():
1113
new_inventory[file_id] = path
1114
for file_id in removed_entries:
1115
new_inventory[file_id] = None
1116
return new_inventory
1245
1119
def apply_changeset_tree(cset, tree, reverse=False):
1246
1120
r_inventory = {}
1247
1121
for entry in tree.source_inventory().itervalues():
1248
1122
inventory[entry.id] = entry.path
1249
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1123
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1250
1124
reverse=reverse)
1251
1125
new_entries, remove_entries = \
1252
1126
get_inventory_change(inventory, new_inventory, cset, reverse)
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." \
1281
class UnsuppportedFiletype(Exception):
1282
def __init__(self, full_path, stat_result):
1283
msg = "The file \"%s\" is not a supported filetype." % full_path
1407
1284
Exception.__init__(self, msg)
1408
1285
self.full_path = full_path
1411
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1412
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1286
self.stat_result = stat_result
1288
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1289
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1415
1291
class ChangesetGenerator(object):
1416
def __init__(self, tree_a, tree_b, interesting_ids=None):
1292
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1417
1293
object.__init__(self)
1418
1294
self.tree_a = tree_a
1419
1295
self.tree_b = tree_b
1420
self._interesting_ids = interesting_ids
1296
if inventory_a is not None:
1297
self.inventory_a = inventory_a
1299
self.inventory_a = tree_a.inventory()
1300
if inventory_b is not None:
1301
self.inventory_b = inventory_b
1303
self.inventory_b = tree_b.inventory()
1304
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1305
self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1422
def iter_both_tree_ids(self):
1423
for file_id in self.tree_a:
1425
for file_id in self.tree_b:
1426
if file_id not in self.tree_a:
1307
def reverse_inventory(self, inventory):
1309
for entry in inventory.itervalues():
1310
if entry.id is None:
1312
r_inventory[entry.id] = entry
1429
1315
def __call__(self):
1430
1316
cset = Changeset()
1431
for file_id in self.iter_both_tree_ids():
1432
cs_entry = self.make_entry(file_id)
1317
for entry in self.inventory_a.itervalues():
1318
if entry.id is None:
1320
cs_entry = self.make_entry(entry.id)
1433
1321
if cs_entry is not None and not cs_entry.is_boring():
1434
1322
cset.add_entry(cs_entry)
1324
for entry in self.inventory_b.itervalues():
1325
if entry.id is None:
1327
if not self.r_inventory_a.has_key(entry.id):
1328
cs_entry = self.make_entry(entry.id)
1329
if cs_entry is not None and not cs_entry.is_boring():
1330
cset.add_entry(cs_entry)
1436
1331
for entry in list(cset.entries.itervalues()):
1437
1332
if entry.parent != entry.new_parent:
1438
1333
if not cset.entries.has_key(entry.parent) and\
1446
1341
cset.add_entry(parent_entry)
1449
def iter_inventory(self, tree):
1450
for file_id in tree:
1451
yield self.get_entry(file_id, tree)
1453
def get_entry(self, file_id, tree):
1454
if not tree.has_or_had_id(file_id):
1456
return tree.inventory[file_id]
1458
def get_entry_parent(self, entry):
1461
return entry.parent_id
1463
def get_path(self, file_id, tree):
1464
if not tree.has_or_had_id(file_id):
1466
path = tree.id2path(file_id)
1472
def make_basic_entry(self, file_id, only_interesting):
1473
entry_a = self.get_entry(file_id, self.tree_a)
1474
entry_b = self.get_entry(file_id, self.tree_b)
1344
def get_entry_parent(self, entry, inventory):
1347
if entry.path == "./.":
1349
dirname = os.path.dirname(entry.path)
1352
parent = inventory[dirname]
1355
def get_paths(self, entry, tree):
1358
full_path = tree.readonly_path(entry.id)
1359
if entry.path == ".":
1360
return ("", full_path)
1361
return (entry.path, full_path)
1363
def make_basic_entry(self, id, only_interesting):
1364
entry_a = self.r_inventory_a.get(id)
1365
entry_b = self.r_inventory_b.get(id)
1475
1366
if only_interesting and not self.is_interesting(entry_a, entry_b):
1477
parent = self.get_entry_parent(entry_a)
1478
path = self.get_path(file_id, self.tree_a)
1479
cs_entry = ChangesetEntry(file_id, parent, path)
1480
new_parent = self.get_entry_parent(entry_b)
1482
new_path = self.get_path(file_id, self.tree_b)
1367
return (None, None, None)
1368
parent = self.get_entry_parent(entry_a, self.inventory_a)
1369
(path, full_path_a) = self.get_paths(entry_a, self.tree_a)
1370
cs_entry = ChangesetEntry(id, parent, path)
1371
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1374
(new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1484
1376
cs_entry.new_path = new_path
1485
1377
cs_entry.new_parent = new_parent
1378
return (cs_entry, full_path_a, full_path_b)
1488
1380
def is_interesting(self, entry_a, entry_b):
1489
if self._interesting_ids is None:
1491
1381
if entry_a is not None:
1492
file_id = entry_a.file_id
1493
elif entry_b is not None:
1494
file_id = entry_b.file_id
1497
return file_id in self._interesting_ids
1382
if entry_a.interesting:
1384
if entry_b is not None:
1385
if entry_b.interesting:
1499
1389
def make_boring_entry(self, id):
1500
cs_entry = self.make_basic_entry(id, only_interesting=False)
1390
(cs_entry, full_path_a, full_path_b) = \
1391
self.make_basic_entry(id, only_interesting=False)
1501
1392
if cs_entry.is_creation_or_deletion():
1502
1393
return self.make_entry(id, only_interesting=False)
1507
1398
def make_entry(self, id, only_interesting=True):
1508
cs_entry = self.make_basic_entry(id, only_interesting)
1399
(cs_entry, full_path_a, full_path_b) = \
1400
self.make_basic_entry(id, only_interesting)
1510
1402
if cs_entry is None:
1513
cs_entry.metadata_change = self.make_exec_flag_change(id)
1515
if id in self.tree_a and id in self.tree_b:
1516
a_sha1 = self.tree_a.get_file_sha1(id)
1517
b_sha1 = self.tree_b.get_file_sha1(id)
1518
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1521
cs_entry.contents_change = self.make_contents_change(id)
1405
stat_a = self.lstat(full_path_a)
1406
stat_b = self.lstat(full_path_b)
1408
cs_entry.new_parent = None
1409
cs_entry.new_path = None
1411
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1412
cs_entry.contents_change = self.make_contents_change(full_path_a,
1522
1416
return cs_entry
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)
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)
1532
if exec_flag_a == exec_flag_b:
1534
return ChangeExecFlag(exec_flag_a, exec_flag_b)
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)
1418
def make_mode_change(self, stat_a, stat_b):
1420
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1421
mode_a = stat_a.st_mode & 0777
1423
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1424
mode_b = stat_b.st_mode & 0777
1425
if mode_a == mode_b:
1427
return ChangeUnixPermissions(mode_a, mode_b)
1429
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1430
if stat_a is None and stat_b is None:
1432
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1433
stat.S_ISDIR(stat_b.st_mode):
1435
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1436
stat.S_ISREG(stat_b.st_mode):
1437
if stat_a.st_ino == stat_b.st_ino and \
1438
stat_a.st_dev == stat_b.st_dev:
1440
if file(full_path_a, "rb").read() == \
1441
file(full_path_b, "rb").read():
1444
patch_contents = patch.diff(full_path_a,
1445
file(full_path_b, "rb").read())
1446
if patch_contents is None:
1448
return PatchApply(patch_contents)
1450
a_contents = self.get_contents(stat_a, full_path_a)
1451
b_contents = self.get_contents(stat_b, full_path_b)
1539
1452
if a_contents == b_contents:
1541
1454
return ReplaceContents(a_contents, b_contents)
1456
def get_contents(self, stat_result, full_path):
1457
if stat_result is None:
1459
elif stat.S_ISREG(stat_result.st_mode):
1460
return FileCreate(file(full_path, "rb").read())
1461
elif stat.S_ISDIR(stat_result.st_mode):
1463
elif stat.S_ISLNK(stat_result.st_mode):
1464
return SymlinkCreate(os.readlink(full_path))
1466
raise UnsupportedFiletype(full_path, stat_result)
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:
1548
kind = tree.kind(file_id)
1550
return TreeFileCreate(tree, file_id)
1551
elif kind in ("directory", "root_directory"):
1553
elif kind == "symlink":
1554
return SymlinkCreate(tree.get_symlink_target(file_id))
1556
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1468
def lstat(self, full_path):
1470
if full_path is not None:
1472
stat_result = os.lstat(full_path)
1474
if e.errno != errno.ENOENT:
1559
1479
def full_path(entry, tree):
1560
return os.path.join(tree.basedir, entry.path)
1480
return os.path.join(tree.root, entry.path)
1562
1482
def new_delete_entry(entry, tree, inventory, delete):
1563
1483
if entry.path == "":