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-", 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)
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,
650
577
return (self.parent != self.new_parent or self.name != self.new_name)
652
def is_deletion(self, reverse=False):
579
def is_deletion(self, reverse):
653
580
"""Return true if applying the entry would delete a file/directory.
655
582
:param reverse: if true, the changeset is being applied in reverse
658
return self.is_creation(not reverse)
585
return ((self.new_parent is None and not reverse) or
586
(self.parent is None and reverse))
660
def is_creation(self, reverse=False):
588
def is_creation(self, reverse):
661
589
"""Return true if applying the entry would create a file/directory.
663
591
:param reverse: if true, the changeset is being applied in reverse
666
if self.contents_change is None:
669
return self.contents_change.is_deletion()
671
return self.contents_change.is_creation()
594
return ((self.parent is None and not reverse) or
595
(self.new_parent is None and reverse))
673
597
def is_creation_or_deletion(self):
674
598
"""Return true if applying the entry would create or delete a
705
def summarize_name(self):
629
def summarize_name(self, reverse=False):
706
630
"""Produce a one-line summary of the filename. Indicates renames as
707
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
712
640
orig_path = self.get_cset_path(False)
713
641
mod_path = self.get_cset_path(True)
714
if orig_path and orig_path.startswith('./'):
642
if orig_path is not None:
715
643
orig_path = orig_path[2:]
716
if mod_path and mod_path.startswith('./'):
644
if mod_path is not None:
717
645
mod_path = mod_path[2:]
718
646
if orig_path == mod_path:
721
return "%s => %s" % (orig_path, mod_path)
723
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):
724
656
"""Determine the full pathname to rename to
726
658
:param id_map: The map of ids to filenames for the tree
727
659
:type id_map: Dictionary
728
660
:param changeset: The changeset to get data from
729
661
:type changeset: `Changeset`
662
:param reverse: If true, we're applying the changeset in reverse
732
mutter("Finding new path for %s", self.summarize_name())
733
parent = self.new_parent
734
to_dir = self.new_dir
736
to_name = self.new_name
737
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
739
680
if to_name is None:
742
683
if parent == NULL_ID or parent is None:
744
685
raise SourceRootHasName(self, to_name)
747
parent_entry = changeset.entries.get(parent)
748
if parent_entry is None:
688
if from_dir == to_dir:
749
689
dir = os.path.dirname(id_map[self.id])
751
mutter("path, new_path: %r %r", self.path, self.new_path)
752
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)
753
694
if from_name == to_name:
754
695
name = os.path.basename(id_map[self.id])
757
698
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
758
return pathjoin(dir, name)
699
return os.path.join(dir, name)
760
701
def is_boring(self):
761
702
"""Determines whether the entry does nothing
777
def apply(self, filename, conflict_handler):
718
def apply(self, filename, conflict_handler, reverse=False):
778
719
"""Applies the file content and/or metadata changes.
780
721
:param filename: the filename of the entry
781
722
:type filename: str
723
:param reverse: If true, apply the changes in reverse
783
if self.is_deletion() and self.metadata_change is not None:
784
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)
785
728
if self.contents_change is not None:
786
self.contents_change.apply(filename, conflict_handler)
787
if not self.is_deletion() and self.metadata_change is not None:
788
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)
791
733
class IDPresent(Exception):
792
734
def __init__(self, id):
832
source_entries.sort(None, longest_to_shortest, True)
790
my_sort(source_entries, longest_to_shortest, reverse=True)
834
792
target_entries = source_entries[:]
835
793
# These are done from shortest to longest path, to avoid creating a
836
794
# child before its parent has been created/renamed
837
795
def shortest_to_longest(entry):
838
path = entry.get_new_path(inventory, changeset)
796
path = entry.get_new_path(inventory, changeset, reverse)
843
target_entries.sort(None, shortest_to_longest)
801
my_sort(target_entries, shortest_to_longest)
844
802
return (source_entries, target_entries)
847
804
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
805
conflict_handler, reverse):
849
806
"""Delete and rename entries as appropriate. Entries are renamed to temp
850
807
names. A map of id -> temp name (or None, for deletions) is returned.
855
812
:type inventory: Dictionary
856
813
:param dir: The directory to apply changes to
815
:param reverse: Apply changes in reverse
858
817
:return: a mapping of id to temporary name
859
818
:rtype: Dictionary
862
821
for i in range(len(source_entries)):
863
822
entry = source_entries[i]
864
if entry.is_deletion():
865
path = pathjoin(dir, inventory[entry.id])
866
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)
867
826
temp_name[entry.id] = None
869
elif entry.needs_rename():
870
if entry.is_creation():
872
to_name = pathjoin(temp_dir, str(i))
829
to_name = os.path.join(temp_dir, str(i))
873
830
src_path = inventory.get(entry.id)
874
831
if src_path is not None:
875
src_path = pathjoin(dir, src_path)
832
src_path = os.path.join(dir, src_path)
877
rename(src_path, to_name)
834
os.rename(src_path, to_name)
878
835
temp_name[entry.id] = to_name
879
836
except OSError, e:
880
837
if e.errno != errno.ENOENT:
882
if conflict_handler.missing_for_rename(src_path, to_name) \
839
if conflict_handler.missing_for_rename(src_path) == "skip":
889
845
def rename_to_new_create(changed_inventory, target_entries, inventory,
890
changeset, dir, conflict_handler):
846
changeset, dir, conflict_handler, reverse):
891
847
"""Rename entries with temp names to their final names, create new files.
893
849
:param changed_inventory: A mapping of id to temporary name
898
854
:type changeset: `Changeset`
899
855
:param dir: The directory to apply changes to
857
:param reverse: If true, apply changes in reverse
902
860
for entry in target_entries:
903
new_tree_path = entry.get_new_path(inventory, changeset)
861
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
904
862
if new_tree_path is None:
906
new_path = pathjoin(dir, new_tree_path)
864
new_path = os.path.join(dir, new_tree_path)
907
865
old_path = changed_inventory.get(entry.id)
908
if bzrlib.osutils.lexists(new_path):
866
if os.path.exists(new_path):
909
867
if conflict_handler.target_exists(entry, new_path, old_path) == \
912
if entry.is_creation():
913
entry.apply(new_path, conflict_handler)
870
if entry.is_creation(reverse):
871
entry.apply(new_path, conflict_handler, reverse)
914
872
changed_inventory[entry.id] = new_tree_path
915
elif entry.needs_rename():
916
if entry.is_deletion():
918
874
if old_path is None:
921
mutter('rename %s to final name %s', old_path, new_path)
922
rename(old_path, new_path)
877
os.rename(old_path, new_path)
923
878
changed_inventory[entry.id] = new_tree_path
924
879
except OSError, e:
925
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
926
% (old_path, new_path, entry, e))
880
raise Exception ("%s is missing" % new_path)
929
882
class TargetExists(Exception):
930
883
def __init__(self, entry, target):
959
910
self.base_parent = base_parent
960
911
self_other_parent = other_parent
963
913
class MergeConflict(Exception):
964
914
def __init__(self, this_path):
965
915
Exception.__init__(self, "Conflict applying changes to %s" % this_path)
966
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)
969
933
class WrongOldContents(Exception):
970
934
def __init__(self, filename):
1021
980
class MissingForRename(Exception):
1022
def __init__(self, filename, to_path):
1023
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)
1024
983
Exception.__init__(self, msg)
1025
984
self.filename = filename
1028
986
class NewContentsConflict(Exception):
1029
987
def __init__(self, filename):
1030
988
msg = "Conflicting contents for new file %s" % (filename)
1031
989
Exception.__init__(self, msg)
1034
class WeaveMergeConflict(Exception):
1035
def __init__(self, filename):
1036
msg = "Conflicting contents for file %s" % (filename)
1037
Exception.__init__(self, msg)
1040
class ThreewayContentsConflict(Exception):
1041
def __init__(self, filename):
1042
msg = "Conflicting contents for file %s" % (filename)
1043
Exception.__init__(self, msg)
1046
992
class MissingForMerge(Exception):
1047
993
def __init__(self, filename):
1048
994
msg = "The file %s was modified, but does not exist in this tree"\
1080
1026
os.unlink(new_file)
1081
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)
1083
1032
def wrong_old_contents(self, filename, expected_contents):
1084
1033
raise WrongOldContents(filename)
1086
1035
def rem_contents_conflict(self, filename, this_contents, base_contents):
1087
1036
raise RemoveContentsConflict(filename)
1089
def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1090
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)
1092
1041
def rmdir_non_empty(self, filename):
1093
1042
raise DeletingNonEmptyDirectory(filename)
1155
1099
#apply changes that don't affect filenames
1156
1100
for entry in changeset.entries.itervalues():
1157
if not entry.is_creation_or_deletion() and not entry.is_boring():
1158
if entry.id not in inventory:
1159
warning("entry {%s} no longer present, can't be updated",
1162
path = pathjoin(dir, inventory[entry.id])
1163
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)
1165
1105
# Apply renames in stages, to minimize conflicts:
1166
1106
# Only files whose name or parent change are interesting, because their
1167
1107
# target name may exist in the source tree. If a directory's name changes,
1168
1108
# that doesn't make its children interesting.
1169
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1109
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1171
1112
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1172
temp_dir, conflict_handler)
1113
temp_dir, conflict_handler,
1174
1116
rename_to_new_create(changed_inventory, target_entries, inventory,
1175
changeset, dir, conflict_handler)
1117
changeset, dir, conflict_handler, reverse)
1176
1118
os.rmdir(temp_dir)
1177
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
1180
1146
def print_changeset(cset):
1181
1147
"""Print all non-boring changeset entries
1190
1156
print entry.summarize_name(cset)
1193
class UnsupportedFiletype(Exception):
1194
def __init__(self, kind, full_path):
1195
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
1197
1283
Exception.__init__(self, msg)
1198
1284
self.full_path = full_path
1285
self.stat_result = stat_result
1202
1287
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1203
1288
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1206
1290
class ChangesetGenerator(object):
1207
1291
def __init__(self, tree_a, tree_b, interesting_ids=None):
1208
1292
object.__init__(self)
1293
1377
return self.make_entry(id, only_interesting=False)
1295
1379
return cs_entry
1297
1382
def make_entry(self, id, only_interesting=True):
1298
1383
cs_entry = self.make_basic_entry(id, only_interesting)
1300
1385
if cs_entry is None:
1303
cs_entry.metadata_change = self.make_exec_flag_change(id)
1305
1387
if id in self.tree_a and id in self.tree_b:
1306
1388
a_sha1 = self.tree_a.get_file_sha1(id)
1307
1389
b_sha1 = self.tree_b.get_file_sha1(id)
1308
1390
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1309
1391
return cs_entry
1311
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,
1312
1403
return cs_entry
1314
def make_exec_flag_change(self, file_id):
1315
exec_flag_a = exec_flag_b = None
1316
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1317
exec_flag_a = self.tree_a.is_executable(file_id)
1319
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1320
exec_flag_b = self.tree_b.is_executable(file_id)
1322
if exec_flag_a == exec_flag_b:
1324
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1326
def make_contents_change(self, file_id):
1327
a_contents = get_contents(self.tree_a, file_id)
1328
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)
1329
1430
if a_contents == b_contents:
1331
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)
1334
def get_contents(tree, file_id):
1335
"""Return the appropriate contents to create a copy of file_id from tree"""
1336
if file_id not in tree:
1338
kind = tree.kind(file_id)
1340
return TreeFileCreate(tree, file_id)
1341
elif kind in ("directory", "root_directory"):
1343
elif kind == "symlink":
1344
return SymlinkCreate(tree.get_symlink_target(file_id))
1346
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:
1349
1457
def full_path(entry, tree):
1350
return pathjoin(tree.basedir, entry.path)
1458
return os.path.join(tree.root, entry.path)
1353
1460
def new_delete_entry(entry, tree, inventory, delete):
1354
1461
if entry.path == "":