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.
20
from bzrlib.trace import mutter
21
from bzrlib.osutils import rename
28
from shutil import rmtree
29
from itertools import izip
31
from bzrlib.trace import mutter, warning
32
from bzrlib.osutils import rename, sha_file, pathjoin, mkdtemp
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
28
"""Represent and apply a changeset
30
Conflicts in applying a changeset are represented as exceptions.
34
from bzrlib.errors import BzrCheckError
33
36
__docformat__ = "restructuredtext"
37
42
class OldFailedTreeOp(Exception):
38
43
def __init__(self):
39
44
Exception.__init__(self, "bzr-tree-change contains files from a"
40
45
" previous failed merge operation.")
41
48
def invert_dict(dict):
43
50
for (key,value) in dict.iteritems():
48
class ChangeUnixPermissions(object):
55
class ChangeExecFlag(object):
49
56
"""This is two-way change, suitable for file modification, creation,
51
def __init__(self, old_mode, new_mode):
52
self.old_mode = old_mode
53
self.new_mode = new_mode
58
def __init__(self, old_exec_flag, new_exec_flag):
59
self.old_exec_flag = old_exec_flag
60
self.new_exec_flag = new_exec_flag
55
def apply(self, filename, conflict_handler, reverse=False):
57
from_mode = self.old_mode
58
to_mode = self.new_mode
60
from_mode = self.new_mode
61
to_mode = self.old_mode
62
def apply(self, filename, conflict_handler):
63
from_exec_flag = self.old_exec_flag
64
to_exec_flag = self.new_exec_flag
63
current_mode = os.stat(filename).st_mode &0777
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
65
68
if e.errno == errno.ENOENT:
66
if conflict_handler.missing_for_chmod(filename) == "skip":
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
69
current_mode = from_mode
72
current_exec_flag = from_exec_flag
71
if from_mode is not None and current_mode != from_mode:
72
if conflict_handler.wrong_old_perms(filename, from_mode,
73
current_mode) != "continue":
74
if from_exec_flag is not None and current_exec_flag != from_exec_flag:
75
if conflict_handler.wrong_old_exec_flag(filename,
76
from_exec_flag, current_exec_flag) != "continue":
76
if to_mode is not None:
79
if to_exec_flag is not None:
80
current_mode = os.stat(filename).st_mode
84
to_mode = current_mode | (0100 & ~umask)
85
# Enable x-bit for others only if they can read it.
86
if current_mode & 0004:
87
to_mode |= 0001 & ~umask
88
if current_mode & 0040:
89
to_mode |= 0010 & ~umask
91
to_mode = current_mode & ~0111
78
93
os.chmod(filename, to_mode)
80
95
if e.errno == errno.ENOENT:
81
conflict_handler.missing_for_chmod(filename)
96
conflict_handler.missing_for_exec_flag(filename)
83
98
def __eq__(self, other):
84
if not isinstance(other, ChangeUnixPermissions):
86
elif self.old_mode != other.old_mode:
88
elif self.new_mode != other.new_mode:
99
return (isinstance(other, ChangeExecFlag) and
100
self.old_exec_flag == other.old_exec_flag and
101
self.new_exec_flag == other.new_exec_flag)
93
103
def __ne__(self, other):
94
104
return not (self == other)
97
def dir_create(filename, conflict_handler, reverse):
107
def dir_create(filename, conflict_handler, reverse=False):
98
108
"""Creates the directory, or deletes it if reverse is true. Intended to be
99
109
used with ReplaceContents.
221
235
if conflict_handler.missing_for_rm(filename, undo) == "skip":
226
def reversed(sequence):
227
max = len(sequence) - 1
228
for i in range(len(sequence)):
229
yield sequence[max - i]
239
class TreeFileCreate(object):
240
"""Create or delete a file (for use with ReplaceContents)"""
241
def __init__(self, tree, file_id):
244
:param contents: The contents of the file to write
248
self.file_id = file_id
251
return "TreeFileCreate(%s)" % self.file_id
253
def __eq__(self, other):
254
if not isinstance(other, TreeFileCreate):
256
return self.tree.get_file_sha1(self.file_id) == \
257
other.tree.get_file_sha1(other.file_id)
259
def __ne__(self, other):
260
return not (self == other)
262
def write_file(self, filename):
263
outfile = file(filename, "wb")
264
for line in self.tree.get_file(self.file_id):
267
def same_text(self, filename):
268
in_file = file(filename, "rb")
269
return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
271
def __call__(self, filename, conflict_handler, reverse=False):
272
"""Create or delete a file
274
:param filename: The name of the file to create
276
:param reverse: Delete the file instead of creating it
281
self.write_file(filename)
283
if e.errno == errno.ENOENT:
284
if conflict_handler.missing_parent(filename)=="continue":
285
self.write_file(filename)
291
if not self.same_text(filename):
292
direction = conflict_handler.wrong_old_contents(filename,
293
self.tree.get_file(self.file_id).read())
294
if direction != "continue":
298
if e.errno != errno.ENOENT:
300
if conflict_handler.missing_for_rm(filename, undo) == "skip":
231
304
class ReplaceContents(object):
232
305
"""A contents-replacement framework. It allows a file/directory/symlink to
292
359
undo(filename, conflict_handler, reverse=True)
293
360
if perform is not None:
294
perform(filename, conflict_handler, reverse=False)
361
perform(filename, conflict_handler)
295
362
if mode is not None:
296
363
os.chmod(filename, mode)
298
class ApplySequence(object):
299
def __init__(self, changes=None):
301
if changes is not None:
302
self.changes.extend(changes)
304
def __eq__(self, other):
305
if not isinstance(other, ApplySequence):
307
elif len(other.changes) != len(self.changes):
310
for i in range(len(self.changes)):
311
if self.changes[i] != other.changes[i]:
315
def __ne__(self, other):
316
return not (self == other)
319
def apply(self, filename, conflict_handler, reverse=False):
323
iter = reversed(self.changes)
325
change.apply(filename, conflict_handler, reverse)
365
def is_creation(self):
366
return self.new_contents is not None and self.old_contents is None
368
def is_deletion(self):
369
return self.old_contents is not None and self.new_contents is None
328
372
class Diff3Merge(object):
373
history_based = False
329
374
def __init__(self, file_id, base, other):
330
375
self.file_id = file_id
332
377
self.other = other
379
def is_creation(self):
382
def is_deletion(self):
334
385
def __eq__(self, other):
335
386
if not isinstance(other, Diff3Merge):
340
391
def __ne__(self, other):
341
392
return not (self == other)
343
def apply(self, filename, conflict_handler, reverse=False):
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)
394
def dump_file(self, temp_dir, name, tree):
395
out_path = pathjoin(temp_dir, name)
396
out_file = file(out_path, "wb")
397
in_file = tree.get_file(self.file_id)
402
def apply(self, filename, conflict_handler):
404
temp_dir = mkdtemp(prefix="bzr-", dir=os.path.dirname(filename))
406
new_file = os.path.join(temp_dir, filename)
407
base_file = self.dump_file(temp_dir, "base", self.base)
408
other_file = self.dump_file(temp_dir, "other", self.other)
349
410
other = other_file
353
status = patch.diff3(new_file, filename, base, other)
355
os.chmod(new_file, os.stat(filename).st_mode)
356
rename(new_file, filename)
360
def get_lines(filename):
361
my_file = file(filename, "rb")
362
lines = my_file.readlines()
365
base_lines = get_lines(base)
366
other_lines = get_lines(other)
367
conflict_handler.merge_conflict(new_file, filename, base_lines,
411
status = bzrlib.patch.diff3(new_file, filename, base, other)
413
os.chmod(new_file, os.stat(filename).st_mode)
414
rename(new_file, filename)
418
def get_lines(filename):
419
my_file = file(filename, "rb")
420
lines = my_file.readlines()
423
base_lines = get_lines(base)
424
other_lines = get_lines(other)
425
conflict_handler.merge_conflict(new_file, filename, base_lines,
578
651
return (self.parent != self.new_parent or self.name != self.new_name)
580
def is_deletion(self, reverse):
653
def is_deletion(self, reverse=False):
581
654
"""Return true if applying the entry would delete a file/directory.
583
656
:param reverse: if true, the changeset is being applied in reverse
586
return ((self.new_parent is None and not reverse) or
587
(self.parent is None and reverse))
659
return self.is_creation(not reverse)
589
def is_creation(self, reverse):
661
def is_creation(self, reverse=False):
590
662
"""Return true if applying the entry would create a file/directory.
592
664
:param reverse: if true, the changeset is being applied in reverse
595
return ((self.parent is None and not reverse) or
596
(self.new_parent is None and reverse))
667
if self.contents_change is None:
670
return self.contents_change.is_deletion()
672
return self.contents_change.is_creation()
598
674
def is_creation_or_deletion(self):
599
675
"""Return true if applying the entry would create or delete a
630
def summarize_name(self, reverse=False):
706
def summarize_name(self):
631
707
"""Produce a one-line summary of the filename. Indicates renames as
632
708
old => new, indicates creation as None => new, indicates deletion as
635
:param changeset: The changeset to get paths from
636
:type changeset: `Changeset`
637
:param reverse: If true, reverse the names in the output
641
713
orig_path = self.get_cset_path(False)
642
714
mod_path = self.get_cset_path(True)
643
if orig_path is not None:
715
if orig_path and orig_path.startswith('./'):
644
716
orig_path = orig_path[2:]
645
if mod_path is not None:
717
if mod_path and mod_path.startswith('./'):
646
718
mod_path = mod_path[2:]
647
719
if orig_path == mod_path:
651
return "%s => %s" % (orig_path, mod_path)
653
return "%s => %s" % (mod_path, orig_path)
656
def get_new_path(self, id_map, changeset, reverse=False):
722
return "%s => %s" % (orig_path, mod_path)
724
def get_new_path(self, id_map, changeset):
657
725
"""Determine the full pathname to rename to
659
727
:param id_map: The map of ids to filenames for the tree
660
728
:type id_map: Dictionary
661
729
:param changeset: The changeset to get data from
662
730
:type changeset: `Changeset`
663
:param reverse: If true, we're applying the changeset in reverse
667
mutter("Finding new path for %s" % self.summarize_name())
671
from_dir = self.new_dir
673
from_name = self.new_name
675
parent = self.new_parent
676
to_dir = self.new_dir
678
to_name = self.new_name
679
from_name = self.name
733
mutter("Finding new path for %s", self.summarize_name())
734
parent = self.new_parent
735
to_dir = self.new_dir
737
to_name = self.new_name
738
from_name = self.name
681
740
if to_name is None:
684
743
if parent == NULL_ID or parent is None:
686
745
raise SourceRootHasName(self, to_name)
689
if from_dir == to_dir:
748
parent_entry = changeset.entries.get(parent)
749
if parent_entry is None:
690
750
dir = os.path.dirname(id_map[self.id])
692
mutter("path, new_path: %r %r" % (self.path, self.new_path))
693
parent_entry = changeset.entries[parent]
694
dir = parent_entry.get_new_path(id_map, changeset, reverse)
752
mutter("path, new_path: %r %r", self.path, self.new_path)
753
dir = parent_entry.get_new_path(id_map, changeset)
695
754
if from_name == to_name:
696
755
name = os.path.basename(id_map[self.id])
699
758
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
700
return os.path.join(dir, name)
759
return pathjoin(dir, name)
702
761
def is_boring(self):
703
762
"""Determines whether the entry does nothing
719
def apply(self, filename, conflict_handler, reverse=False):
778
def apply(self, filename, conflict_handler):
720
779
"""Applies the file content and/or metadata changes.
722
781
:param filename: the filename of the entry
723
782
:type filename: str
724
:param reverse: If true, apply the changes in reverse
727
if self.is_deletion(reverse) and self.metadata_change is not None:
728
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)
729
786
if self.contents_change is not None:
730
self.contents_change.apply(filename, conflict_handler, reverse)
731
if not self.is_deletion(reverse) and self.metadata_change is not None:
732
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)
734
792
class IDPresent(Exception):
735
793
def __init__(self, id):
791
my_sort(source_entries, longest_to_shortest, reverse=True)
833
source_entries.sort(None, longest_to_shortest, True)
793
835
target_entries = source_entries[:]
794
836
# These are done from shortest to longest path, to avoid creating a
795
837
# child before its parent has been created/renamed
796
838
def shortest_to_longest(entry):
797
path = entry.get_new_path(inventory, changeset, reverse)
839
path = entry.get_new_path(inventory, changeset)
802
my_sort(target_entries, shortest_to_longest)
844
target_entries.sort(None, shortest_to_longest)
803
845
return (source_entries, target_entries)
805
848
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
806
conflict_handler, reverse):
807
850
"""Delete and rename entries as appropriate. Entries are renamed to temp
808
851
names. A map of id -> temp name (or None, for deletions) is returned.
813
856
:type inventory: Dictionary
814
857
:param dir: The directory to apply changes to
816
:param reverse: Apply changes in reverse
818
859
:return: a mapping of id to temporary name
819
860
:rtype: Dictionary
822
863
for i in range(len(source_entries)):
823
864
entry = source_entries[i]
824
if entry.is_deletion(reverse):
825
path = os.path.join(dir, inventory[entry.id])
826
entry.apply(path, conflict_handler, reverse)
865
if entry.is_deletion():
866
path = pathjoin(dir, inventory[entry.id])
867
entry.apply(path, conflict_handler)
827
868
temp_name[entry.id] = None
830
to_name = os.path.join(temp_dir, str(i))
870
elif entry.needs_rename():
871
if entry.is_creation():
873
to_name = pathjoin(temp_dir, str(i))
831
874
src_path = inventory.get(entry.id)
832
875
if src_path is not None:
833
src_path = os.path.join(dir, src_path)
876
src_path = pathjoin(dir, src_path)
835
878
rename(src_path, to_name)
836
879
temp_name[entry.id] = to_name
837
880
except OSError, e:
838
881
if e.errno != errno.ENOENT:
840
if conflict_handler.missing_for_rename(src_path) == "skip":
883
if conflict_handler.missing_for_rename(src_path, to_name) \
846
890
def rename_to_new_create(changed_inventory, target_entries, inventory,
847
changeset, dir, conflict_handler, reverse):
891
changeset, dir, conflict_handler):
848
892
"""Rename entries with temp names to their final names, create new files.
850
894
:param changed_inventory: A mapping of id to temporary name
855
899
:type changeset: `Changeset`
856
900
:param dir: The directory to apply changes to
858
:param reverse: If true, apply changes in reverse
861
903
for entry in target_entries:
862
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
904
new_tree_path = entry.get_new_path(inventory, changeset)
863
905
if new_tree_path is None:
865
new_path = os.path.join(dir, new_tree_path)
907
new_path = pathjoin(dir, new_tree_path)
866
908
old_path = changed_inventory.get(entry.id)
867
909
if bzrlib.osutils.lexists(new_path):
868
910
if conflict_handler.target_exists(entry, new_path, old_path) == \
871
if entry.is_creation(reverse):
872
entry.apply(new_path, conflict_handler, reverse)
913
if entry.is_creation():
914
entry.apply(new_path, conflict_handler)
873
915
changed_inventory[entry.id] = new_tree_path
916
elif entry.needs_rename():
917
if entry.is_deletion():
875
919
if old_path is None:
922
mutter('rename %s to final name %s', old_path, new_path)
878
923
rename(old_path, new_path)
879
924
changed_inventory[entry.id] = new_tree_path
880
925
except OSError, e:
881
raise Exception ("%s is missing" % new_path)
926
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
927
% (old_path, new_path, entry, e))
883
930
class TargetExists(Exception):
884
931
def __init__(self, entry, target):
911
960
self.base_parent = base_parent
912
961
self_other_parent = other_parent
914
964
class MergeConflict(Exception):
915
965
def __init__(self, this_path):
916
966
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
917
967
self.this_path = this_path
919
class MergePermissionConflict(Exception):
920
def __init__(self, this_path, base_path, other_path):
921
this_perms = os.stat(this_path).st_mode & 0755
922
base_perms = os.stat(base_path).st_mode & 0755
923
other_perms = os.stat(other_path).st_mode & 0755
924
msg = """Conflicting permission for %s
928
""" % (this_path, this_perms, base_perms, other_perms)
929
self.this_path = this_path
930
self.base_path = base_path
931
self.other_path = other_path
932
Exception.__init__(self, msg)
934
970
class WrongOldContents(Exception):
935
971
def __init__(self, filename):
981
1022
class MissingForRename(Exception):
982
def __init__(self, filename):
983
msg = "Attempt to move missing path %s" % (filename)
1023
def __init__(self, filename, to_path):
1024
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
984
1025
Exception.__init__(self, msg)
985
1026
self.filename = filename
987
1029
class NewContentsConflict(Exception):
988
1030
def __init__(self, filename):
989
1031
msg = "Conflicting contents for new file %s" % (filename)
990
1032
Exception.__init__(self, msg)
1035
class WeaveMergeConflict(Exception):
1036
def __init__(self, filename):
1037
msg = "Conflicting contents for file %s" % (filename)
1038
Exception.__init__(self, msg)
1041
class ThreewayContentsConflict(Exception):
1042
def __init__(self, filename):
1043
msg = "Conflicting contents for file %s" % (filename)
1044
Exception.__init__(self, msg)
993
1047
class MissingForMerge(Exception):
994
1048
def __init__(self, filename):
995
1049
msg = "The file %s was modified, but does not exist in this tree"\
1027
1081
os.unlink(new_file)
1028
1082
raise MergeConflict(this_path)
1030
def permission_conflict(self, this_path, base_path, other_path):
1031
raise MergePermissionConflict(this_path, base_path, other_path)
1033
1084
def wrong_old_contents(self, filename, expected_contents):
1034
1085
raise WrongOldContents(filename)
1036
1087
def rem_contents_conflict(self, filename, this_contents, base_contents):
1037
1088
raise RemoveContentsConflict(filename)
1039
def wrong_old_perms(self, filename, old_perms, new_perms):
1040
raise WrongOldPermissions(filename, old_perms, new_perms)
1090
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1091
raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1042
1093
def rmdir_non_empty(self, filename):
1043
1094
raise DeletingNonEmptyDirectory(filename)
1100
1156
#apply changes that don't affect filenames
1101
1157
for entry in changeset.entries.itervalues():
1102
if not entry.is_creation_or_deletion():
1103
path = os.path.join(dir, inventory[entry.id])
1104
entry.apply(path, conflict_handler, reverse)
1158
if not entry.is_creation_or_deletion() and not entry.is_boring():
1159
if entry.id not in inventory:
1160
warning("entry {%s} no longer present, can't be updated",
1163
path = pathjoin(dir, inventory[entry.id])
1164
entry.apply(path, conflict_handler)
1106
1166
# Apply renames in stages, to minimize conflicts:
1107
1167
# Only files whose name or parent change are interesting, because their
1108
1168
# target name may exist in the source tree. If a directory's name changes,
1109
1169
# that doesn't make its children interesting.
1110
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1113
1172
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1114
temp_dir, conflict_handler,
1173
temp_dir, conflict_handler)
1117
1175
rename_to_new_create(changed_inventory, target_entries, inventory,
1118
changeset, dir, conflict_handler, reverse)
1176
changeset, dir, conflict_handler)
1119
1177
os.rmdir(temp_dir)
1120
1178
return changed_inventory
1123
def apply_changeset_tree(cset, tree, reverse=False):
1125
for entry in tree.source_inventory().itervalues():
1126
inventory[entry.id] = entry.path
1127
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1129
new_entries, remove_entries = \
1130
get_inventory_change(inventory, new_inventory, cset, reverse)
1131
tree.update_source_inventory(new_entries, remove_entries)
1134
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1137
for entry in cset.entries.itervalues():
1138
if entry.needs_rename():
1139
new_path = entry.get_new_path(inventory, cset)
1140
if new_path is None:
1141
remove_entries.append(entry.id)
1143
new_entries[new_path] = entry.id
1144
return new_entries, remove_entries
1147
1181
def print_changeset(cset):
1148
1182
"""Print all non-boring changeset entries
1157
1191
print entry.summarize_name(cset)
1159
class CompositionFailure(Exception):
1160
def __init__(self, old_entry, new_entry, problem):
1161
msg = "Unable to conpose entries.\n %s" % problem
1162
Exception.__init__(self, msg)
1164
class IDMismatch(CompositionFailure):
1165
def __init__(self, old_entry, new_entry):
1166
problem = "Attempt to compose entries with different ids: %s and %s" %\
1167
(old_entry.id, new_entry.id)
1168
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1170
def compose_changesets(old_cset, new_cset):
1171
"""Combine two changesets into one. This works well for exact patching.
1172
Otherwise, not so well.
1174
:param old_cset: The first changeset that would be applied
1175
:type old_cset: `Changeset`
1176
:param new_cset: The second changeset that would be applied
1177
:type new_cset: `Changeset`
1178
:return: A changeset that combines the changes in both changesets
1181
composed = Changeset()
1182
for old_entry in old_cset.entries.itervalues():
1183
new_entry = new_cset.entries.get(old_entry.id)
1184
if new_entry is None:
1185
composed.add_entry(old_entry)
1187
composed_entry = compose_entries(old_entry, new_entry)
1188
if composed_entry.parent is not None or\
1189
composed_entry.new_parent is not None:
1190
composed.add_entry(composed_entry)
1191
for new_entry in new_cset.entries.itervalues():
1192
if not old_cset.entries.has_key(new_entry.id):
1193
composed.add_entry(new_entry)
1196
def compose_entries(old_entry, new_entry):
1197
"""Combine two entries into one.
1199
:param old_entry: The first entry that would be applied
1200
:type old_entry: ChangesetEntry
1201
:param old_entry: The second entry that would be applied
1202
:type old_entry: ChangesetEntry
1203
:return: A changeset entry combining both entries
1204
:rtype: `ChangesetEntry`
1206
if old_entry.id != new_entry.id:
1207
raise IDMismatch(old_entry, new_entry)
1208
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1210
if (old_entry.parent != old_entry.new_parent or
1211
new_entry.parent != new_entry.new_parent):
1212
output.new_parent = new_entry.new_parent
1214
if (old_entry.path != old_entry.new_path or
1215
new_entry.path != new_entry.new_path):
1216
output.new_path = new_entry.new_path
1218
output.contents_change = compose_contents(old_entry, new_entry)
1219
output.metadata_change = compose_metadata(old_entry, new_entry)
1222
def compose_contents(old_entry, new_entry):
1223
"""Combine the contents of two changeset entries. Entries are combined
1224
intelligently where possible, but the fallback behavior returns an
1227
:param old_entry: The first entry that would be applied
1228
:type old_entry: `ChangesetEntry`
1229
:param new_entry: The second entry that would be applied
1230
:type new_entry: `ChangesetEntry`
1231
:return: A combined contents change
1232
:rtype: anything supporting the apply(reverse=False) method
1234
old_contents = old_entry.contents_change
1235
new_contents = new_entry.contents_change
1236
if old_entry.contents_change is None:
1237
return new_entry.contents_change
1238
elif new_entry.contents_change is None:
1239
return old_entry.contents_change
1240
elif isinstance(old_contents, ReplaceContents) and \
1241
isinstance(new_contents, ReplaceContents):
1242
if old_contents.old_contents == new_contents.new_contents:
1245
return ReplaceContents(old_contents.old_contents,
1246
new_contents.new_contents)
1247
elif isinstance(old_contents, ApplySequence):
1248
output = ApplySequence(old_contents.changes)
1249
if isinstance(new_contents, ApplySequence):
1250
output.changes.extend(new_contents.changes)
1252
output.changes.append(new_contents)
1254
elif isinstance(new_contents, ApplySequence):
1255
output = ApplySequence((old_contents.changes,))
1256
output.extend(new_contents.changes)
1259
return ApplySequence((old_contents, new_contents))
1261
def compose_metadata(old_entry, new_entry):
1262
old_meta = old_entry.metadata_change
1263
new_meta = new_entry.metadata_change
1264
if old_meta is None:
1266
elif new_meta is None:
1268
elif isinstance(old_meta, ChangeUnixPermissions) and \
1269
isinstance(new_meta, ChangeUnixPermissions):
1270
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1272
return ApplySequence(old_meta, new_meta)
1275
def changeset_is_null(changeset):
1276
for entry in changeset.entries.itervalues():
1277
if not entry.is_boring():
1281
class UnsuppportedFiletype(Exception):
1282
def __init__(self, full_path, stat_result):
1283
msg = "The file \"%s\" is not a supported filetype." % full_path
1194
class UnsupportedFiletype(Exception):
1195
def __init__(self, kind, full_path):
1196
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1284
1198
Exception.__init__(self, msg)
1285
1199
self.full_path = full_path
1286
self.stat_result = stat_result
1288
1203
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1289
1204
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1291
1207
class ChangesetGenerator(object):
1292
1208
def __init__(self, tree_a, tree_b, interesting_ids=None):
1293
1209
object.__init__(self)
1399
1309
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1400
1310
return cs_entry
1402
cs_entry.contents_change = self.make_contents_change(full_path_a,
1312
cs_entry.contents_change = self.make_contents_change(id)
1406
1313
return cs_entry
1408
def make_mode_change(self, stat_a, stat_b):
1410
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1411
mode_a = stat_a.st_mode & 0777
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:
1417
return ChangeUnixPermissions(mode_a, mode_b)
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:
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):
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:
1431
a_contents = self.get_contents(stat_a, full_path_a)
1432
b_contents = self.get_contents(stat_b, full_path_b)
1315
def make_exec_flag_change(self, file_id):
1316
exec_flag_a = exec_flag_b = None
1317
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1318
exec_flag_a = self.tree_a.is_executable(file_id)
1320
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1321
exec_flag_b = self.tree_b.is_executable(file_id)
1323
if exec_flag_a == exec_flag_b:
1325
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1327
def make_contents_change(self, file_id):
1328
a_contents = get_contents(self.tree_a, file_id)
1329
b_contents = get_contents(self.tree_b, file_id)
1433
1330
if a_contents == b_contents:
1435
1332
return ReplaceContents(a_contents, b_contents)
1437
def get_contents(self, stat_result, full_path):
1438
if stat_result is 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):
1444
elif stat.S_ISLNK(stat_result.st_mode):
1445
return SymlinkCreate(os.readlink(full_path))
1447
raise UnsupportedFiletype(full_path, stat_result)
1449
def lstat(self, full_path):
1451
if full_path is not None:
1453
stat_result = os.lstat(full_path)
1455
if e.errno != errno.ENOENT:
1335
def get_contents(tree, file_id):
1336
"""Return the appropriate contents to create a copy of file_id from tree"""
1337
if file_id not in tree:
1339
kind = tree.kind(file_id)
1341
return TreeFileCreate(tree, file_id)
1342
elif kind in ("directory", "root_directory"):
1344
elif kind == "symlink":
1345
return SymlinkCreate(tree.get_symlink_target(file_id))
1347
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1460
1350
def full_path(entry, tree):
1461
return os.path.join(tree.root, entry.path)
1351
return pathjoin(tree.basedir, entry.path)
1463
1354
def new_delete_entry(entry, tree, inventory, delete):
1464
1355
if entry.path == "":