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 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
34
from bzrlib.errors import BzrCheckError
20
from bzrlib.trace import mutter
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
26
"""Represent and apply a changeset
28
Conflicts in applying a changeset are represented as exceptions.
36
31
__docformat__ = "restructuredtext"
42
35
class OldFailedTreeOp(Exception):
43
36
def __init__(self):
44
37
Exception.__init__(self, "bzr-tree-change contains files from a"
45
38
" previous failed merge operation.")
48
39
def invert_dict(dict):
50
41
for (key,value) in dict.iteritems():
51
42
newdict[value] = key
55
class ChangeExecFlag(object):
47
class ChangeUnixPermissions(object):
56
48
"""This is two-way change, suitable for file modification, creation,
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
50
def __init__(self, old_mode, new_mode):
51
self.old_mode = old_mode
52
self.new_mode = new_mode
62
def apply(self, filename, conflict_handler):
63
from_exec_flag = self.old_exec_flag
64
to_exec_flag = self.new_exec_flag
54
def apply(self, filename, conflict_handler, reverse=False):
56
from_mode = self.old_mode
57
to_mode = self.new_mode
59
from_mode = self.new_mode
60
to_mode = self.old_mode
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
62
current_mode = os.stat(filename).st_mode &0777
68
64
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
65
if conflict_handler.missing_for_chmod(filename) == "skip":
72
current_exec_flag = from_exec_flag
68
current_mode = from_mode
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":
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":
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
75
if to_mode is not None:
93
77
os.chmod(filename, to_mode)
95
79
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
80
conflict_handler.missing_for_chmod(filename)
98
82
def __eq__(self, other):
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)
83
if not isinstance(other, ChangeUnixPermissions):
85
elif self.old_mode != other.old_mode:
87
elif self.new_mode != other.new_mode:
103
92
def __ne__(self, other):
104
93
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse=False):
95
def dir_create(filename, conflict_handler, reverse):
108
96
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
97
used with ReplaceContents.
235
221
if conflict_handler.missing_for_rm(filename, undo) == "skip":
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":
226
def reversed(sequence):
227
max = len(sequence) - 1
228
for i in range(len(sequence)):
229
yield sequence[max - i]
304
231
class ReplaceContents(object):
305
232
"""A contents-replacement framework. It allows a file/directory/symlink to
359
292
undo(filename, conflict_handler, reverse=True)
360
293
if perform is not None:
361
perform(filename, conflict_handler)
294
perform(filename, conflict_handler, reverse=False)
362
295
if mode is not None:
363
296
os.chmod(filename, mode)
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
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)
372
328
class Diff3Merge(object):
373
history_based = False
374
329
def __init__(self, file_id, base, other):
375
330
self.file_id = file_id
377
332
self.other = other
379
def is_creation(self):
382
def is_deletion(self):
385
334
def __eq__(self, other):
386
335
if not isinstance(other, Diff3Merge):
391
340
def __ne__(self, other):
392
341
return not (self == other)
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-")
406
new_file = filename+".new"
407
base_file = self.dump_file(temp_dir, "base", self.base)
408
other_file = self.dump_file(temp_dir, "other", 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)
410
349
other = other_file
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,
353
status = patch.diff3(new_file, filename, base, other)
355
os.chmod(new_file, os.stat(filename).st_mode)
356
os.rename(new_file, filename)
360
def get_lines(filename):
361
my_file = file(base, "rb")
362
lines = my_file.readlines()
364
base_lines = get_lines(base)
365
other_lines = get_lines(other)
366
conflict_handler.merge_conflict(new_file, filename, base_lines,
651
577
return (self.parent != self.new_parent or self.name != self.new_name)
653
def is_deletion(self, reverse=False):
579
def is_deletion(self, reverse):
654
580
"""Return true if applying the entry would delete a file/directory.
656
582
:param reverse: if true, the changeset is being applied in reverse
659
return self.is_creation(not reverse)
585
return ((self.new_parent is None and not reverse) or
586
(self.parent is None and reverse))
661
def is_creation(self, reverse=False):
588
def is_creation(self, reverse):
662
589
"""Return true if applying the entry would create a file/directory.
664
591
:param reverse: if true, the changeset is being applied in reverse
667
if self.contents_change is None:
670
return self.contents_change.is_deletion()
672
return self.contents_change.is_creation()
594
return ((self.parent is None and not reverse) or
595
(self.new_parent is None and reverse))
674
597
def is_creation_or_deletion(self):
675
598
"""Return true if applying the entry would create or delete a
706
def summarize_name(self):
629
def summarize_name(self, reverse=False):
707
630
"""Produce a one-line summary of the filename. Indicates renames as
708
631
old => new, indicates creation as None => new, indicates deletion as
634
:param changeset: The changeset to get paths from
635
:type changeset: `Changeset`
636
:param reverse: If true, reverse the names in the output
713
640
orig_path = self.get_cset_path(False)
714
641
mod_path = self.get_cset_path(True)
715
if orig_path and orig_path.startswith('./'):
642
if orig_path is not None:
716
643
orig_path = orig_path[2:]
717
if mod_path and mod_path.startswith('./'):
644
if mod_path is not None:
718
645
mod_path = mod_path[2:]
719
646
if orig_path == mod_path:
722
return "%s => %s" % (orig_path, mod_path)
724
def get_new_path(self, id_map, changeset):
650
return "%s => %s" % (orig_path, mod_path)
652
return "%s => %s" % (mod_path, orig_path)
655
def get_new_path(self, id_map, changeset, reverse=False):
725
656
"""Determine the full pathname to rename to
727
658
:param id_map: The map of ids to filenames for the tree
728
659
:type id_map: Dictionary
729
660
:param changeset: The changeset to get data from
730
661
:type changeset: `Changeset`
662
:param reverse: If true, we're applying the changeset in reverse
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
666
mutter("Finding new path for %s" % self.summarize_name())
670
from_dir = self.new_dir
672
from_name = self.new_name
674
parent = self.new_parent
675
to_dir = self.new_dir
677
to_name = self.new_name
678
from_name = self.name
740
680
if to_name is None:
743
683
if parent == NULL_ID or parent is None:
745
685
raise SourceRootHasName(self, to_name)
748
parent_entry = changeset.entries.get(parent)
749
if parent_entry is None:
688
if from_dir == to_dir:
750
689
dir = os.path.dirname(id_map[self.id])
752
mutter("path, new_path: %r %r", self.path, self.new_path)
753
dir = parent_entry.get_new_path(id_map, changeset)
691
mutter("path, new_path: %r %r" % (self.path, self.new_path))
692
parent_entry = changeset.entries[parent]
693
dir = parent_entry.get_new_path(id_map, changeset, reverse)
754
694
if from_name == to_name:
755
695
name = os.path.basename(id_map[self.id])
758
698
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
759
return pathjoin(dir, name)
699
return os.path.join(dir, name)
761
701
def is_boring(self):
762
702
"""Determines whether the entry does nothing
778
def apply(self, filename, conflict_handler):
718
def apply(self, filename, conflict_handler, reverse=False):
779
719
"""Applies the file content and/or metadata changes.
781
721
:param filename: the filename of the entry
782
722
:type filename: str
723
:param reverse: If true, apply the changes in reverse
784
if self.is_deletion() and self.metadata_change is not None:
785
self.metadata_change.apply(filename, conflict_handler)
726
if self.is_deletion(reverse) and self.metadata_change is not None:
727
self.metadata_change.apply(filename, conflict_handler, reverse)
786
728
if self.contents_change is not None:
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)
729
self.contents_change.apply(filename, conflict_handler, reverse)
730
if not self.is_deletion(reverse) and self.metadata_change is not None:
731
self.metadata_change.apply(filename, conflict_handler, reverse)
792
733
class IDPresent(Exception):
793
734
def __init__(self, id):
833
source_entries.sort(None, longest_to_shortest, True)
790
my_sort(source_entries, longest_to_shortest, reverse=True)
835
792
target_entries = source_entries[:]
836
793
# These are done from shortest to longest path, to avoid creating a
837
794
# child before its parent has been created/renamed
838
795
def shortest_to_longest(entry):
839
path = entry.get_new_path(inventory, changeset)
796
path = entry.get_new_path(inventory, changeset, reverse)
844
target_entries.sort(None, shortest_to_longest)
801
my_sort(target_entries, shortest_to_longest)
845
802
return (source_entries, target_entries)
848
804
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
805
conflict_handler, reverse):
850
806
"""Delete and rename entries as appropriate. Entries are renamed to temp
851
807
names. A map of id -> temp name (or None, for deletions) is returned.
856
812
:type inventory: Dictionary
857
813
:param dir: The directory to apply changes to
815
:param reverse: Apply changes in reverse
859
817
:return: a mapping of id to temporary name
860
818
:rtype: Dictionary
863
821
for i in range(len(source_entries)):
864
822
entry = source_entries[i]
865
if entry.is_deletion():
866
path = pathjoin(dir, inventory[entry.id])
867
entry.apply(path, conflict_handler)
823
if entry.is_deletion(reverse):
824
path = os.path.join(dir, inventory[entry.id])
825
entry.apply(path, conflict_handler, reverse)
868
826
temp_name[entry.id] = None
870
elif entry.needs_rename():
871
if entry.is_creation():
873
to_name = pathjoin(temp_dir, str(i))
829
to_name = os.path.join(temp_dir, str(i))
874
830
src_path = inventory.get(entry.id)
875
831
if src_path is not None:
876
src_path = pathjoin(dir, src_path)
832
src_path = os.path.join(dir, src_path)
878
rename(src_path, to_name)
834
os.rename(src_path, to_name)
879
835
temp_name[entry.id] = to_name
880
836
except OSError, e:
881
837
if e.errno != errno.ENOENT:
883
if conflict_handler.missing_for_rename(src_path, to_name) \
839
if conflict_handler.missing_for_rename(src_path) == "skip":
890
845
def rename_to_new_create(changed_inventory, target_entries, inventory,
891
changeset, dir, conflict_handler):
846
changeset, dir, conflict_handler, reverse):
892
847
"""Rename entries with temp names to their final names, create new files.
894
849
:param changed_inventory: A mapping of id to temporary name
899
854
:type changeset: `Changeset`
900
855
:param dir: The directory to apply changes to
857
:param reverse: If true, apply changes in reverse
903
860
for entry in target_entries:
904
new_tree_path = entry.get_new_path(inventory, changeset)
861
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
905
862
if new_tree_path is None:
907
new_path = pathjoin(dir, new_tree_path)
864
new_path = os.path.join(dir, new_tree_path)
908
865
old_path = changed_inventory.get(entry.id)
909
if bzrlib.osutils.lexists(new_path):
866
if os.path.exists(new_path):
910
867
if conflict_handler.target_exists(entry, new_path, old_path) == \
913
if entry.is_creation():
914
entry.apply(new_path, conflict_handler)
870
if entry.is_creation(reverse):
871
entry.apply(new_path, conflict_handler, reverse)
915
872
changed_inventory[entry.id] = new_tree_path
916
elif entry.needs_rename():
917
if entry.is_deletion():
919
874
if old_path is None:
922
mutter('rename %s to final name %s', old_path, new_path)
923
rename(old_path, new_path)
877
os.rename(old_path, new_path)
924
878
changed_inventory[entry.id] = new_tree_path
925
879
except OSError, e:
926
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
927
% (old_path, new_path, entry, e))
880
raise Exception ("%s is missing" % new_path)
930
882
class TargetExists(Exception):
931
883
def __init__(self, entry, target):
960
910
self.base_parent = base_parent
961
911
self_other_parent = other_parent
964
913
class MergeConflict(Exception):
965
914
def __init__(self, this_path):
966
915
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
967
916
self.this_path = this_path
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
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)
970
933
class WrongOldContents(Exception):
971
934
def __init__(self, filename):
1022
980
class MissingForRename(Exception):
1023
def __init__(self, filename, to_path):
1024
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)
1025
983
Exception.__init__(self, msg)
1026
984
self.filename = filename
1029
986
class NewContentsConflict(Exception):
1030
987
def __init__(self, filename):
1031
988
msg = "Conflicting contents for new file %s" % (filename)
1032
989
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)
1047
992
class MissingForMerge(Exception):
1048
993
def __init__(self, filename):
1049
994
msg = "The file %s was modified, but does not exist in this tree"\
1081
1029
os.unlink(new_file)
1082
1030
raise MergeConflict(this_path)
1032
def permission_conflict(self, this_path, base_path, other_path):
1033
raise MergePermissionConflict(this_path, base_path, other_path)
1084
1035
def wrong_old_contents(self, filename, expected_contents):
1085
1036
raise WrongOldContents(filename)
1087
1038
def rem_contents_conflict(self, filename, this_contents, base_contents):
1088
1039
raise RemoveContentsConflict(filename)
1090
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1091
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)
1093
1044
def rmdir_non_empty(self, filename):
1094
1045
raise DeletingNonEmptyDirectory(filename)
1156
1102
#apply changes that don't affect filenames
1157
1103
for entry in changeset.entries.itervalues():
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)
1104
if not entry.is_creation_or_deletion():
1105
path = os.path.join(dir, inventory[entry.id])
1106
entry.apply(path, conflict_handler, reverse)
1166
1108
# Apply renames in stages, to minimize conflicts:
1167
1109
# Only files whose name or parent change are interesting, because their
1168
1110
# target name may exist in the source tree. If a directory's name changes,
1169
1111
# that doesn't make its children interesting.
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1112
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1172
1115
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1173
temp_dir, conflict_handler)
1116
temp_dir, conflict_handler,
1175
1119
rename_to_new_create(changed_inventory, target_entries, inventory,
1176
changeset, dir, conflict_handler)
1120
changeset, dir, conflict_handler, reverse)
1177
1121
os.rmdir(temp_dir)
1178
1122
return changed_inventory
1181
def apply_changeset_tree(cset, tree):
1125
def apply_changeset_tree(cset, tree, reverse=False):
1182
1126
r_inventory = {}
1183
1127
for entry in tree.source_inventory().itervalues():
1184
1128
inventory[entry.id] = entry.path
1185
new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1129
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1186
1131
new_entries, remove_entries = \
1187
get_inventory_change(inventory, new_inventory, cset)
1132
get_inventory_change(inventory, new_inventory, cset, reverse)
1188
1133
tree.update_source_inventory(new_entries, remove_entries)
1191
def get_inventory_change(inventory, new_inventory, cset):
1136
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1192
1137
new_entries = {}
1193
1138
remove_entries = []
1194
1139
for entry in cset.entries.itervalues():
1214
1159
print entry.summarize_name(cset)
1217
class UnsupportedFiletype(Exception):
1218
def __init__(self, kind, full_path):
1219
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1161
class CompositionFailure(Exception):
1162
def __init__(self, old_entry, new_entry, problem):
1163
msg = "Unable to conpose entries.\n %s" % problem
1164
Exception.__init__(self, msg)
1166
class IDMismatch(CompositionFailure):
1167
def __init__(self, old_entry, new_entry):
1168
problem = "Attempt to compose entries with different ids: %s and %s" %\
1169
(old_entry.id, new_entry.id)
1170
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1172
def compose_changesets(old_cset, new_cset):
1173
"""Combine two changesets into one. This works well for exact patching.
1174
Otherwise, not so well.
1176
:param old_cset: The first changeset that would be applied
1177
:type old_cset: `Changeset`
1178
:param new_cset: The second changeset that would be applied
1179
:type new_cset: `Changeset`
1180
:return: A changeset that combines the changes in both changesets
1183
composed = Changeset()
1184
for old_entry in old_cset.entries.itervalues():
1185
new_entry = new_cset.entries.get(old_entry.id)
1186
if new_entry is None:
1187
composed.add_entry(old_entry)
1189
composed_entry = compose_entries(old_entry, new_entry)
1190
if composed_entry.parent is not None or\
1191
composed_entry.new_parent is not None:
1192
composed.add_entry(composed_entry)
1193
for new_entry in new_cset.entries.itervalues():
1194
if not old_cset.entries.has_key(new_entry.id):
1195
composed.add_entry(new_entry)
1198
def compose_entries(old_entry, new_entry):
1199
"""Combine two entries into one.
1201
:param old_entry: The first entry that would be applied
1202
:type old_entry: ChangesetEntry
1203
:param old_entry: The second entry that would be applied
1204
:type old_entry: ChangesetEntry
1205
:return: A changeset entry combining both entries
1206
:rtype: `ChangesetEntry`
1208
if old_entry.id != new_entry.id:
1209
raise IDMismatch(old_entry, new_entry)
1210
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1212
if (old_entry.parent != old_entry.new_parent or
1213
new_entry.parent != new_entry.new_parent):
1214
output.new_parent = new_entry.new_parent
1216
if (old_entry.path != old_entry.new_path or
1217
new_entry.path != new_entry.new_path):
1218
output.new_path = new_entry.new_path
1220
output.contents_change = compose_contents(old_entry, new_entry)
1221
output.metadata_change = compose_metadata(old_entry, new_entry)
1224
def compose_contents(old_entry, new_entry):
1225
"""Combine the contents of two changeset entries. Entries are combined
1226
intelligently where possible, but the fallback behavior returns an
1229
:param old_entry: The first entry that would be applied
1230
:type old_entry: `ChangesetEntry`
1231
:param new_entry: The second entry that would be applied
1232
:type new_entry: `ChangesetEntry`
1233
:return: A combined contents change
1234
:rtype: anything supporting the apply(reverse=False) method
1236
old_contents = old_entry.contents_change
1237
new_contents = new_entry.contents_change
1238
if old_entry.contents_change is None:
1239
return new_entry.contents_change
1240
elif new_entry.contents_change is None:
1241
return old_entry.contents_change
1242
elif isinstance(old_contents, ReplaceContents) and \
1243
isinstance(new_contents, ReplaceContents):
1244
if old_contents.old_contents == new_contents.new_contents:
1247
return ReplaceContents(old_contents.old_contents,
1248
new_contents.new_contents)
1249
elif isinstance(old_contents, ApplySequence):
1250
output = ApplySequence(old_contents.changes)
1251
if isinstance(new_contents, ApplySequence):
1252
output.changes.extend(new_contents.changes)
1254
output.changes.append(new_contents)
1256
elif isinstance(new_contents, ApplySequence):
1257
output = ApplySequence((old_contents.changes,))
1258
output.extend(new_contents.changes)
1261
return ApplySequence((old_contents, new_contents))
1263
def compose_metadata(old_entry, new_entry):
1264
old_meta = old_entry.metadata_change
1265
new_meta = new_entry.metadata_change
1266
if old_meta is None:
1268
elif new_meta is None:
1270
elif isinstance(old_meta, ChangeUnixPermissions) and \
1271
isinstance(new_meta, ChangeUnixPermissions):
1272
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1274
return ApplySequence(old_meta, new_meta)
1277
def changeset_is_null(changeset):
1278
for entry in changeset.entries.itervalues():
1279
if not entry.is_boring():
1283
class UnsuppportedFiletype(Exception):
1284
def __init__(self, full_path, stat_result):
1285
msg = "The file \"%s\" is not a supported filetype." % full_path
1221
1286
Exception.__init__(self, msg)
1222
1287
self.full_path = full_path
1288
self.stat_result = stat_result
1226
1290
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1227
1291
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1230
1293
class ChangesetGenerator(object):
1231
1294
def __init__(self, tree_a, tree_b, interesting_ids=None):
1232
1295
object.__init__(self)
1317
1380
return self.make_entry(id, only_interesting=False)
1319
1382
return cs_entry
1321
1385
def make_entry(self, id, only_interesting=True):
1322
1386
cs_entry = self.make_basic_entry(id, only_interesting)
1324
1388
if cs_entry is None:
1327
cs_entry.metadata_change = self.make_exec_flag_change(id)
1329
1390
if id in self.tree_a and id in self.tree_b:
1330
1391
a_sha1 = self.tree_a.get_file_sha1(id)
1331
1392
b_sha1 = self.tree_b.get_file_sha1(id)
1332
1393
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1333
1394
return cs_entry
1335
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)
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,
1336
1406
return cs_entry
1338
def make_exec_flag_change(self, file_id):
1339
exec_flag_a = exec_flag_b = None
1340
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1341
exec_flag_a = self.tree_a.is_executable(file_id)
1343
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1344
exec_flag_b = self.tree_b.is_executable(file_id)
1346
if exec_flag_a == exec_flag_b:
1348
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1350
def make_contents_change(self, file_id):
1351
a_contents = get_contents(self.tree_a, file_id)
1352
b_contents = get_contents(self.tree_b, file_id)
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)
1353
1433
if a_contents == b_contents:
1355
1435
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)
1358
def get_contents(tree, file_id):
1359
"""Return the appropriate contents to create a copy of file_id from tree"""
1360
if file_id not in tree:
1362
kind = tree.kind(file_id)
1364
return TreeFileCreate(tree, file_id)
1365
elif kind in ("directory", "root_directory"):
1367
elif kind == "symlink":
1368
return SymlinkCreate(tree.get_symlink_target(file_id))
1370
raise UnsupportedFiletype(kind, tree.id2path(file_id))
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:
1373
1460
def full_path(entry, tree):
1374
return pathjoin(tree.basedir, entry.path)
1461
return os.path.join(tree.root, entry.path)
1377
1463
def new_delete_entry(entry, tree, inventory, delete):
1378
1464
if entry.path == "":