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
1026
os.unlink(new_file)
1082
1027
raise MergeConflict(this_path)
1029
def permission_conflict(self, this_path, base_path, other_path):
1030
raise MergePermissionConflict(this_path, base_path, other_path)
1084
1032
def wrong_old_contents(self, filename, expected_contents):
1085
1033
raise WrongOldContents(filename)
1087
1035
def rem_contents_conflict(self, filename, this_contents, base_contents):
1088
1036
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)
1038
def wrong_old_perms(self, filename, old_perms, new_perms):
1039
raise WrongOldPermissions(filename, old_perms, new_perms)
1093
1041
def rmdir_non_empty(self, filename):
1094
1042
raise DeletingNonEmptyDirectory(filename)
1156
1099
#apply changes that don't affect filenames
1157
1100
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)
1101
if not entry.is_creation_or_deletion():
1102
path = os.path.join(dir, inventory[entry.id])
1103
entry.apply(path, conflict_handler, reverse)
1166
1105
# Apply renames in stages, to minimize conflicts:
1167
1106
# Only files whose name or parent change are interesting, because their
1168
1107
# target name may exist in the source tree. If a directory's name changes,
1169
1108
# that doesn't make its children interesting.
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1109
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1172
1112
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1173
temp_dir, conflict_handler)
1113
temp_dir, conflict_handler,
1175
1116
rename_to_new_create(changed_inventory, target_entries, inventory,
1176
changeset, dir, conflict_handler)
1117
changeset, dir, conflict_handler, reverse)
1177
1118
os.rmdir(temp_dir)
1178
1119
return changed_inventory
1122
def apply_changeset_tree(cset, tree, reverse=False):
1124
for entry in tree.source_inventory().itervalues():
1125
inventory[entry.id] = entry.path
1126
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1128
new_entries, remove_entries = \
1129
get_inventory_change(inventory, new_inventory, cset, reverse)
1130
tree.update_source_inventory(new_entries, remove_entries)
1133
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1136
for entry in cset.entries.itervalues():
1137
if entry.needs_rename():
1138
new_path = entry.get_new_path(inventory, cset)
1139
if new_path is None:
1140
remove_entries.append(entry.id)
1142
new_entries[new_path] = entry.id
1143
return new_entries, remove_entries
1181
1146
def print_changeset(cset):
1182
1147
"""Print all non-boring changeset entries
1191
1156
print entry.summarize_name(cset)
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." \
1158
class CompositionFailure(Exception):
1159
def __init__(self, old_entry, new_entry, problem):
1160
msg = "Unable to conpose entries.\n %s" % problem
1161
Exception.__init__(self, msg)
1163
class IDMismatch(CompositionFailure):
1164
def __init__(self, old_entry, new_entry):
1165
problem = "Attempt to compose entries with different ids: %s and %s" %\
1166
(old_entry.id, new_entry.id)
1167
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1169
def compose_changesets(old_cset, new_cset):
1170
"""Combine two changesets into one. This works well for exact patching.
1171
Otherwise, not so well.
1173
:param old_cset: The first changeset that would be applied
1174
:type old_cset: `Changeset`
1175
:param new_cset: The second changeset that would be applied
1176
:type new_cset: `Changeset`
1177
:return: A changeset that combines the changes in both changesets
1180
composed = Changeset()
1181
for old_entry in old_cset.entries.itervalues():
1182
new_entry = new_cset.entries.get(old_entry.id)
1183
if new_entry is None:
1184
composed.add_entry(old_entry)
1186
composed_entry = compose_entries(old_entry, new_entry)
1187
if composed_entry.parent is not None or\
1188
composed_entry.new_parent is not None:
1189
composed.add_entry(composed_entry)
1190
for new_entry in new_cset.entries.itervalues():
1191
if not old_cset.entries.has_key(new_entry.id):
1192
composed.add_entry(new_entry)
1195
def compose_entries(old_entry, new_entry):
1196
"""Combine two entries into one.
1198
:param old_entry: The first entry that would be applied
1199
:type old_entry: ChangesetEntry
1200
:param old_entry: The second entry that would be applied
1201
:type old_entry: ChangesetEntry
1202
:return: A changeset entry combining both entries
1203
:rtype: `ChangesetEntry`
1205
if old_entry.id != new_entry.id:
1206
raise IDMismatch(old_entry, new_entry)
1207
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1209
if (old_entry.parent != old_entry.new_parent or
1210
new_entry.parent != new_entry.new_parent):
1211
output.new_parent = new_entry.new_parent
1213
if (old_entry.path != old_entry.new_path or
1214
new_entry.path != new_entry.new_path):
1215
output.new_path = new_entry.new_path
1217
output.contents_change = compose_contents(old_entry, new_entry)
1218
output.metadata_change = compose_metadata(old_entry, new_entry)
1221
def compose_contents(old_entry, new_entry):
1222
"""Combine the contents of two changeset entries. Entries are combined
1223
intelligently where possible, but the fallback behavior returns an
1226
:param old_entry: The first entry that would be applied
1227
:type old_entry: `ChangesetEntry`
1228
:param new_entry: The second entry that would be applied
1229
:type new_entry: `ChangesetEntry`
1230
:return: A combined contents change
1231
:rtype: anything supporting the apply(reverse=False) method
1233
old_contents = old_entry.contents_change
1234
new_contents = new_entry.contents_change
1235
if old_entry.contents_change is None:
1236
return new_entry.contents_change
1237
elif new_entry.contents_change is None:
1238
return old_entry.contents_change
1239
elif isinstance(old_contents, ReplaceContents) and \
1240
isinstance(new_contents, ReplaceContents):
1241
if old_contents.old_contents == new_contents.new_contents:
1244
return ReplaceContents(old_contents.old_contents,
1245
new_contents.new_contents)
1246
elif isinstance(old_contents, ApplySequence):
1247
output = ApplySequence(old_contents.changes)
1248
if isinstance(new_contents, ApplySequence):
1249
output.changes.extend(new_contents.changes)
1251
output.changes.append(new_contents)
1253
elif isinstance(new_contents, ApplySequence):
1254
output = ApplySequence((old_contents.changes,))
1255
output.extend(new_contents.changes)
1258
return ApplySequence((old_contents, new_contents))
1260
def compose_metadata(old_entry, new_entry):
1261
old_meta = old_entry.metadata_change
1262
new_meta = new_entry.metadata_change
1263
if old_meta is None:
1265
elif new_meta is None:
1267
elif isinstance(old_meta, ChangeUnixPermissions) and \
1268
isinstance(new_meta, ChangeUnixPermissions):
1269
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1271
return ApplySequence(old_meta, new_meta)
1274
def changeset_is_null(changeset):
1275
for entry in changeset.entries.itervalues():
1276
if not entry.is_boring():
1280
class UnsuppportedFiletype(Exception):
1281
def __init__(self, full_path, stat_result):
1282
msg = "The file \"%s\" is not a supported filetype." % full_path
1198
1283
Exception.__init__(self, msg)
1199
1284
self.full_path = full_path
1285
self.stat_result = stat_result
1203
1287
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1204
1288
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1207
1290
class ChangesetGenerator(object):
1208
1291
def __init__(self, tree_a, tree_b, interesting_ids=None):
1209
1292
object.__init__(self)
1294
1377
return self.make_entry(id, only_interesting=False)
1296
1379
return cs_entry
1298
1382
def make_entry(self, id, only_interesting=True):
1299
1383
cs_entry = self.make_basic_entry(id, only_interesting)
1301
1385
if cs_entry is None:
1304
cs_entry.metadata_change = self.make_exec_flag_change(id)
1306
1387
if id in self.tree_a and id in self.tree_b:
1307
1388
a_sha1 = self.tree_a.get_file_sha1(id)
1308
1389
b_sha1 = self.tree_b.get_file_sha1(id)
1309
1390
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1310
1391
return cs_entry
1312
cs_entry.contents_change = self.make_contents_change(id)
1393
full_path_a = self.tree_a.readonly_path(id)
1394
full_path_b = self.tree_b.readonly_path(id)
1395
stat_a = self.lstat(full_path_a)
1396
stat_b = self.lstat(full_path_b)
1398
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1399
cs_entry.contents_change = self.make_contents_change(full_path_a,
1313
1403
return cs_entry
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)
1405
def make_mode_change(self, stat_a, stat_b):
1407
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1408
mode_a = stat_a.st_mode & 0777
1410
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1411
mode_b = stat_b.st_mode & 0777
1412
if mode_a == mode_b:
1414
return ChangeUnixPermissions(mode_a, mode_b)
1416
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1417
if stat_a is None and stat_b is None:
1419
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1420
stat.S_ISDIR(stat_b.st_mode):
1422
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1423
stat.S_ISREG(stat_b.st_mode):
1424
if stat_a.st_ino == stat_b.st_ino and \
1425
stat_a.st_dev == stat_b.st_dev:
1428
a_contents = self.get_contents(stat_a, full_path_a)
1429
b_contents = self.get_contents(stat_b, full_path_b)
1330
1430
if a_contents == b_contents:
1332
1432
return ReplaceContents(a_contents, b_contents)
1434
def get_contents(self, stat_result, full_path):
1435
if stat_result is None:
1437
elif stat.S_ISREG(stat_result.st_mode):
1438
return FileCreate(file(full_path, "rb").read())
1439
elif stat.S_ISDIR(stat_result.st_mode):
1441
elif stat.S_ISLNK(stat_result.st_mode):
1442
return SymlinkCreate(os.readlink(full_path))
1444
raise UnsupportedFiletype(full_path, stat_result)
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))
1446
def lstat(self, full_path):
1448
if full_path is not None:
1450
stat_result = os.lstat(full_path)
1452
if e.errno != errno.ENOENT:
1350
1457
def full_path(entry, tree):
1351
return pathjoin(tree.basedir, entry.path)
1458
return os.path.join(tree.root, entry.path)
1354
1460
def new_delete_entry(entry, tree, inventory, delete):
1355
1461
if entry.path == "":