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
21
Represent and apply a changeset
36
23
__docformat__ = "restructuredtext"
42
class OldFailedTreeOp(Exception):
44
Exception.__init__(self, "bzr-tree-change contains files from a"
45
" previous failed merge operation.")
48
28
def invert_dict(dict):
50
30
for (key,value) in dict.iteritems():
51
31
newdict[value] = key
55
class ChangeExecFlag(object):
35
class PatchApply(object):
36
"""Patch application as a kind of content change"""
37
def __init__(self, contents):
40
:param contents: The text of the patch to apply
41
:type contents: str"""
42
self.contents = contents
44
def __eq__(self, other):
45
if not isinstance(other, PatchApply):
47
elif self.contents != other.contents:
52
def __ne__(self, other):
53
return not (self == other)
55
def apply(self, filename, conflict_handler, reverse=False):
56
"""Applies the patch to the specified file.
58
:param filename: the file to apply the patch to
60
:param reverse: If true, apply the patch in reverse
63
input_name = filename+".orig"
65
os.rename(filename, input_name)
67
if e.errno != errno.ENOENT:
69
if conflict_handler.patch_target_missing(filename, self.contents)\
72
os.rename(filename, input_name)
75
status = patch.patch(self.contents, input_name, filename,
77
os.chmod(filename, os.stat(input_name).st_mode)
81
conflict_handler.failed_hunks(filename)
84
class ChangeUnixPermissions(object):
56
85
"""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
87
def __init__(self, old_mode, new_mode):
88
self.old_mode = old_mode
89
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
91
def apply(self, filename, conflict_handler, reverse=False):
93
from_mode = self.old_mode
94
to_mode = self.new_mode
96
from_mode = self.new_mode
97
to_mode = self.old_mode
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
99
current_mode = os.stat(filename).st_mode &0777
67
100
except OSError, e:
68
101
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
102
if conflict_handler.missing_for_chmod(filename) == "skip":
72
current_exec_flag = from_exec_flag
105
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":
107
if from_mode is not None and current_mode != from_mode:
108
if conflict_handler.wrong_old_perms(filename, from_mode,
109
current_mode) != "continue":
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
112
if to_mode is not None:
93
114
os.chmod(filename, to_mode)
94
115
except IOError, e:
95
116
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
117
conflict_handler.missing_for_chmod(filename)
98
119
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)
120
if not isinstance(other, ChangeUnixPermissions):
122
elif self.old_mode != other.old_mode:
124
elif self.new_mode != other.new_mode:
103
129
def __ne__(self, other):
104
130
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse=False):
132
def dir_create(filename, conflict_handler, reverse):
108
133
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
134
used with ReplaceContents.
235
258
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":
263
def reversed(sequence):
264
max = len(sequence) - 1
265
for i in range(len(sequence)):
266
yield sequence[max - i]
304
268
class ReplaceContents(object):
305
269
"""A contents-replacement framework. It allows a file/directory/symlink to
359
329
undo(filename, conflict_handler, reverse=True)
360
330
if perform is not None:
361
perform(filename, conflict_handler)
331
perform(filename, conflict_handler, reverse=False)
362
332
if mode is not None:
363
333
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
335
class ApplySequence(object):
336
def __init__(self, changes=None):
338
if changes is not None:
339
self.changes.extend(changes)
341
def __eq__(self, other):
342
if not isinstance(other, ApplySequence):
344
elif len(other.changes) != len(self.changes):
347
for i in range(len(self.changes)):
348
if self.changes[i] != other.changes[i]:
352
def __ne__(self, other):
353
return not (self == other)
356
def apply(self, filename, conflict_handler, reverse=False):
360
iter = reversed(self.changes)
362
change.apply(filename, conflict_handler, reverse)
372
365
class Diff3Merge(object):
373
history_based = False
374
def __init__(self, file_id, base, other):
375
self.file_id = file_id
379
def is_creation(self):
382
def is_deletion(self):
366
def __init__(self, base_file, other_file):
367
self.base_file = base_file
368
self.other_file = other_file
385
370
def __eq__(self, other):
386
371
if not isinstance(other, Diff3Merge):
388
return (self.base == other.base and
389
self.other == other.other and self.file_id == other.file_id)
373
return (self.base_file == other.base_file and
374
self.other_file == other.other_file)
391
376
def __ne__(self, other):
392
377
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)
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,
379
def apply(self, filename, conflict_handler, reverse=False):
380
new_file = filename+".new"
382
base = self.base_file
383
other = self.other_file
385
base = self.other_file
386
other = self.base_file
387
status = patch.diff3(new_file, filename, base, other)
389
os.chmod(new_file, os.stat(filename).st_mode)
390
os.rename(new_file, filename)
394
conflict_handler.merge_conflict(new_file, filename, base, other)
706
def summarize_name(self):
656
def summarize_name(self, changeset, reverse=False):
707
657
"""Produce a one-line summary of the filename. Indicates renames as
708
658
old => new, indicates creation as None => new, indicates deletion as
661
:param changeset: The changeset to get paths from
662
:type changeset: `Changeset`
663
:param reverse: If true, reverse the names in the output
713
667
orig_path = self.get_cset_path(False)
714
668
mod_path = self.get_cset_path(True)
715
if orig_path and orig_path.startswith('./'):
669
if orig_path is not None:
716
670
orig_path = orig_path[2:]
717
if mod_path and mod_path.startswith('./'):
671
if mod_path is not None:
718
672
mod_path = mod_path[2:]
719
673
if orig_path == mod_path:
722
return "%s => %s" % (orig_path, mod_path)
724
def get_new_path(self, id_map, changeset):
677
return "%s => %s" % (orig_path, mod_path)
679
return "%s => %s" % (mod_path, orig_path)
682
def get_new_path(self, id_map, changeset, reverse=False):
725
683
"""Determine the full pathname to rename to
727
685
:param id_map: The map of ids to filenames for the tree
728
686
:type id_map: Dictionary
729
687
:param changeset: The changeset to get data from
730
688
:type changeset: `Changeset`
689
: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
696
from_dir = self.new_dir
698
from_name = self.new_name
700
parent = self.new_parent
701
to_dir = self.new_dir
703
to_name = self.new_name
704
from_name = self.name
740
706
if to_name is None:
743
709
if parent == NULL_ID or parent is None:
745
711
raise SourceRootHasName(self, to_name)
748
parent_entry = changeset.entries.get(parent)
749
if parent_entry is None:
714
if from_dir == to_dir:
750
715
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)
717
parent_entry = changeset.entries[parent]
718
dir = parent_entry.get_new_path(id_map, changeset, reverse)
754
719
if from_name == to_name:
755
720
name = os.path.basename(id_map[self.id])
758
723
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
759
return pathjoin(dir, name)
724
return os.path.join(dir, name)
761
726
def is_boring(self):
762
727
"""Determines whether the entry does nothing
856
837
:type inventory: Dictionary
857
838
:param dir: The directory to apply changes to
840
:param reverse: Apply changes in reverse
859
842
:return: a mapping of id to temporary name
860
843
:rtype: Dictionary
845
temp_dir = os.path.join(dir, "temp")
863
847
for i in range(len(source_entries)):
864
848
entry = source_entries[i]
865
if entry.is_deletion():
866
path = pathjoin(dir, inventory[entry.id])
867
entry.apply(path, conflict_handler)
868
temp_name[entry.id] = None
849
if entry.is_deletion(reverse):
850
path = os.path.join(dir, inventory[entry.id])
851
entry.apply(path, conflict_handler, reverse)
870
elif entry.needs_rename():
871
if entry.is_creation():
873
to_name = pathjoin(temp_dir, str(i))
854
to_name = temp_dir+"/"+str(i)
874
855
src_path = inventory.get(entry.id)
875
856
if src_path is not None:
876
src_path = pathjoin(dir, src_path)
857
src_path = os.path.join(dir, src_path)
878
rename(src_path, to_name)
859
os.rename(src_path, to_name)
879
860
temp_name[entry.id] = to_name
880
861
except OSError, e:
881
862
if e.errno != errno.ENOENT:
883
if conflict_handler.missing_for_rename(src_path, to_name) \
864
if conflict_handler.missing_for_rename(src_path) == "skip":
890
def rename_to_new_create(changed_inventory, target_entries, inventory,
891
changeset, dir, conflict_handler):
870
def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
871
conflict_handler, reverse):
892
872
"""Rename entries with temp names to their final names, create new files.
894
:param changed_inventory: A mapping of id to temporary name
895
:type changed_inventory: Dictionary
874
:param temp_name: A mapping of id to temporary name
875
:type temp_name: Dictionary
896
876
:param target_entries: The entries to apply changes to
897
877
:type target_entries: List of `ChangesetEntry`
898
878
:param changeset: The changeset to apply
899
879
:type changeset: `Changeset`
900
880
:param dir: The directory to apply changes to
882
:param reverse: If true, apply changes in reverse
903
885
for entry in target_entries:
904
new_tree_path = entry.get_new_path(inventory, changeset)
905
if new_tree_path is None:
886
new_path = entry.get_new_path(inventory, changeset, reverse)
907
new_path = pathjoin(dir, new_tree_path)
908
old_path = changed_inventory.get(entry.id)
909
if bzrlib.osutils.lexists(new_path):
889
new_path = os.path.join(dir, new_path)
890
old_path = temp_name.get(entry.id)
891
if os.path.exists(new_path):
910
892
if conflict_handler.target_exists(entry, new_path, old_path) == \
913
if entry.is_creation():
914
entry.apply(new_path, conflict_handler)
915
changed_inventory[entry.id] = new_tree_path
916
elif entry.needs_rename():
917
if entry.is_deletion():
895
if entry.is_creation(reverse):
896
entry.apply(new_path, conflict_handler, reverse)
919
898
if old_path is None:
922
mutter('rename %s to final name %s', old_path, new_path)
923
rename(old_path, new_path)
924
changed_inventory[entry.id] = new_tree_path
901
os.rename(old_path, new_path)
925
902
except OSError, e:
926
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
927
% (old_path, new_path, entry, e))
903
raise Exception ("%s is missing" % new_path)
930
905
class TargetExists(Exception):
931
906
def __init__(self, entry, target):
1022
1003
class MissingForRename(Exception):
1023
def __init__(self, filename, to_path):
1024
msg = "Attempt to move missing path %s to %s" % (filename, to_path)
1004
def __init__(self, filename):
1005
msg = "Attempt to move missing path %s" % (filename)
1025
1006
Exception.__init__(self, msg)
1026
1007
self.filename = filename
1029
class NewContentsConflict(Exception):
1030
def __init__(self, filename):
1031
msg = "Conflicting contents for new file %s" % (filename)
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)
1047
class MissingForMerge(Exception):
1048
def __init__(self, filename):
1049
msg = "The file %s was modified, but does not exist in this tree"\
1051
Exception.__init__(self, msg)
1054
1009
class ExceptionConflictHandler(object):
1055
"""Default handler for merge exceptions.
1057
This throws an error on any kind of conflict. Conflict handlers can
1058
descend from this class if they have a better way to handle some or
1059
all types of conflict.
1010
def __init__(self, dir):
1061
1013
def missing_parent(self, pathname):
1062
1014
parent = os.path.dirname(pathname)
1063
1015
raise Exception("Parent directory missing for %s" % pathname)
1074
1026
def rename_conflict(self, id, this_name, base_name, other_name):
1075
1027
raise RenameConflict(id, this_name, base_name, other_name)
1077
def move_conflict(self, id, this_dir, base_dir, other_dir):
1029
def move_conflict(self, id, inventory):
1030
this_dir = inventory.this.get_dir(id)
1031
base_dir = inventory.base.get_dir(id)
1032
other_dir = inventory.other.get_dir(id)
1078
1033
raise MoveConflict(id, this_dir, base_dir, other_dir)
1080
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1035
def merge_conflict(self, new_file, this_path, base_path, other_path):
1081
1036
os.unlink(new_file)
1082
1037
raise MergeConflict(this_path)
1039
def permission_conflict(self, this_path, base_path, other_path):
1040
raise MergePermissionConflict(this_path, base_path, other_path)
1084
1042
def wrong_old_contents(self, filename, expected_contents):
1085
1043
raise WrongOldContents(filename)
1087
1045
def rem_contents_conflict(self, filename, this_contents, base_contents):
1088
1046
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)
1048
def wrong_old_perms(self, filename, old_perms, new_perms):
1049
raise WrongOldPermissions(filename, old_perms, new_perms)
1093
1051
def rmdir_non_empty(self, filename):
1094
1052
raise DeletingNonEmptyDirectory(filename)
1099
1057
def patch_target_missing(self, filename, contents):
1100
1058
raise PatchTargetMissing(filename)
1102
def missing_for_exec_flag(self, filename):
1103
raise MissingForExecFlag(filename)
1060
def missing_for_chmod(self, filename):
1061
raise MissingPermsFile(filename)
1105
1063
def missing_for_rm(self, filename, change):
1106
1064
raise MissingForRm(filename)
1108
def missing_for_rename(self, filename, to_path):
1109
raise MissingForRename(filename, to_path)
1111
def missing_for_merge(self, file_id, other_path):
1112
raise MissingForMerge(other_path)
1114
def new_contents_conflict(self, filename, other_contents):
1115
raise NewContentsConflict(filename)
1117
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1118
raise WeaveMergeConflict(filename)
1120
def threeway_contents_conflict(self, filename, this_contents,
1121
base_contents, other_contents):
1122
raise ThreewayContentsConflict(filename)
1066
def missing_for_rename(self, filename):
1067
raise MissingForRename(filename)
1128
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1072
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1129
1074
"""Apply a changeset to a directory.
1131
1076
:param changeset: The changes to perform
1134
1079
:type inventory: Dictionary
1135
1080
:param dir: The path of the directory to apply the changes to
1082
:param reverse: If true, apply the changes in reverse
1137
1084
:return: The mapping of the changed entries
1138
1085
:rtype: Dictionary
1140
1087
if conflict_handler is None:
1141
conflict_handler = ExceptionConflictHandler()
1142
temp_dir = pathjoin(dir, "bzr-tree-change")
1146
if e.errno == errno.EEXIST:
1150
if e.errno == errno.ENOTEMPTY:
1151
raise OldFailedTreeOp()
1088
conflict_handler = ExceptionConflictHandler(dir)
1089
temp_dir = dir+"/temp"
1156
1092
#apply changes that don't affect filenames
1157
1093
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)
1094
if not entry.is_creation_or_deletion():
1095
path = os.path.join(dir, inventory[entry.id])
1096
entry.apply(path, conflict_handler, reverse)
1166
1098
# Apply renames in stages, to minimize conflicts:
1167
1099
# Only files whose name or parent change are interesting, because their
1168
1100
# target name may exist in the source tree. If a directory's name changes,
1169
1101
# that doesn't make its children interesting.
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1172
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1173
temp_dir, conflict_handler)
1175
rename_to_new_create(changed_inventory, target_entries, inventory,
1176
changeset, dir, conflict_handler)
1102
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1105
temp_name = rename_to_temp_delete(source_entries, inventory, dir,
1106
conflict_handler, reverse)
1108
rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
1109
conflict_handler, reverse)
1177
1110
os.rmdir(temp_dir)
1178
return changed_inventory
1181
def apply_changeset_tree(cset, tree):
1111
r_inventory = invert_dict(inventory)
1112
new_entries, removed_entries = get_inventory_change(inventory,
1113
r_inventory, changeset, reverse)
1115
for path, file_id in new_entries.iteritems():
1116
new_inventory[file_id] = path
1117
for file_id in removed_entries:
1118
new_inventory[file_id] = None
1119
return new_inventory
1122
def apply_changeset_tree(cset, tree, reverse=False):
1182
1123
r_inventory = {}
1183
1124
for entry in tree.source_inventory().itervalues():
1184
1125
inventory[entry.id] = entry.path
1185
new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1126
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1186
1128
new_entries, remove_entries = \
1187
get_inventory_change(inventory, new_inventory, cset)
1129
get_inventory_change(inventory, new_inventory, cset, reverse)
1188
1130
tree.update_source_inventory(new_entries, remove_entries)
1191
def get_inventory_change(inventory, new_inventory, cset):
1133
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1192
1134
new_entries = {}
1193
1135
remove_entries = []
1136
r_inventory = invert_dict(inventory)
1137
r_new_inventory = invert_dict(new_inventory)
1194
1138
for entry in cset.entries.itervalues():
1195
1139
if entry.needs_rename():
1196
new_path = entry.get_new_path(inventory, cset)
1197
if new_path is None:
1198
remove_entries.append(entry.id)
1140
old_path = r_inventory.get(entry.id)
1141
if old_path is not None:
1142
remove_entries.append(old_path)
1200
new_entries[new_path] = entry.id
1144
new_path = entry.get_new_path(inventory, cset)
1145
if new_path is not None:
1146
new_entries[new_path] = entry.id
1201
1147
return new_entries, remove_entries
1214
1160
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." \
1162
class CompositionFailure(Exception):
1163
def __init__(self, old_entry, new_entry, problem):
1164
msg = "Unable to conpose entries.\n %s" % problem
1165
Exception.__init__(self, msg)
1167
class IDMismatch(CompositionFailure):
1168
def __init__(self, old_entry, new_entry):
1169
problem = "Attempt to compose entries with different ids: %s and %s" %\
1170
(old_entry.id, new_entry.id)
1171
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1173
def compose_changesets(old_cset, new_cset):
1174
"""Combine two changesets into one. This works well for exact patching.
1175
Otherwise, not so well.
1177
:param old_cset: The first changeset that would be applied
1178
:type old_cset: `Changeset`
1179
:param new_cset: The second changeset that would be applied
1180
:type new_cset: `Changeset`
1181
:return: A changeset that combines the changes in both changesets
1184
composed = Changeset()
1185
for old_entry in old_cset.entries.itervalues():
1186
new_entry = new_cset.entries.get(old_entry.id)
1187
if new_entry is None:
1188
composed.add_entry(old_entry)
1190
composed_entry = compose_entries(old_entry, new_entry)
1191
if composed_entry.parent is not None or\
1192
composed_entry.new_parent is not None:
1193
composed.add_entry(composed_entry)
1194
for new_entry in new_cset.entries.itervalues():
1195
if not old_cset.entries.has_key(new_entry.id):
1196
composed.add_entry(new_entry)
1199
def compose_entries(old_entry, new_entry):
1200
"""Combine two entries into one.
1202
:param old_entry: The first entry that would be applied
1203
:type old_entry: ChangesetEntry
1204
:param old_entry: The second entry that would be applied
1205
:type old_entry: ChangesetEntry
1206
:return: A changeset entry combining both entries
1207
:rtype: `ChangesetEntry`
1209
if old_entry.id != new_entry.id:
1210
raise IDMismatch(old_entry, new_entry)
1211
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1213
if (old_entry.parent != old_entry.new_parent or
1214
new_entry.parent != new_entry.new_parent):
1215
output.new_parent = new_entry.new_parent
1217
if (old_entry.path != old_entry.new_path or
1218
new_entry.path != new_entry.new_path):
1219
output.new_path = new_entry.new_path
1221
output.contents_change = compose_contents(old_entry, new_entry)
1222
output.metadata_change = compose_metadata(old_entry, new_entry)
1225
def compose_contents(old_entry, new_entry):
1226
"""Combine the contents of two changeset entries. Entries are combined
1227
intelligently where possible, but the fallback behavior returns an
1230
:param old_entry: The first entry that would be applied
1231
:type old_entry: `ChangesetEntry`
1232
:param new_entry: The second entry that would be applied
1233
:type new_entry: `ChangesetEntry`
1234
:return: A combined contents change
1235
:rtype: anything supporting the apply(reverse=False) method
1237
old_contents = old_entry.contents_change
1238
new_contents = new_entry.contents_change
1239
if old_entry.contents_change is None:
1240
return new_entry.contents_change
1241
elif new_entry.contents_change is None:
1242
return old_entry.contents_change
1243
elif isinstance(old_contents, ReplaceContents) and \
1244
isinstance(new_contents, ReplaceContents):
1245
if old_contents.old_contents == new_contents.new_contents:
1248
return ReplaceContents(old_contents.old_contents,
1249
new_contents.new_contents)
1250
elif isinstance(old_contents, ApplySequence):
1251
output = ApplySequence(old_contents.changes)
1252
if isinstance(new_contents, ApplySequence):
1253
output.changes.extend(new_contents.changes)
1255
output.changes.append(new_contents)
1257
elif isinstance(new_contents, ApplySequence):
1258
output = ApplySequence((old_contents.changes,))
1259
output.extend(new_contents.changes)
1262
return ApplySequence((old_contents, new_contents))
1264
def compose_metadata(old_entry, new_entry):
1265
old_meta = old_entry.metadata_change
1266
new_meta = new_entry.metadata_change
1267
if old_meta is None:
1269
elif new_meta is None:
1271
elif isinstance(old_meta, ChangeUnixPermissions) and \
1272
isinstance(new_meta, ChangeUnixPermissions):
1273
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1275
return ApplySequence(old_meta, new_meta)
1278
def changeset_is_null(changeset):
1279
for entry in changeset.entries.itervalues():
1280
if not entry.is_boring():
1284
class UnsuppportedFiletype(Exception):
1285
def __init__(self, full_path, stat_result):
1286
msg = "The file \"%s\" is not a supported filetype." % full_path
1221
1287
Exception.__init__(self, msg)
1222
1288
self.full_path = full_path
1226
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1227
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1289
self.stat_result = stat_result
1291
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1292
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1230
1294
class ChangesetGenerator(object):
1231
def __init__(self, tree_a, tree_b, interesting_ids=None):
1295
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1232
1296
object.__init__(self)
1233
1297
self.tree_a = tree_a
1234
1298
self.tree_b = tree_b
1235
self._interesting_ids = interesting_ids
1299
if inventory_a is not None:
1300
self.inventory_a = inventory_a
1302
self.inventory_a = tree_a.inventory()
1303
if inventory_b is not None:
1304
self.inventory_b = inventory_b
1306
self.inventory_b = tree_b.inventory()
1307
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1308
self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1237
def iter_both_tree_ids(self):
1238
for file_id in self.tree_a:
1240
for file_id in self.tree_b:
1241
if file_id not in self.tree_a:
1310
def reverse_inventory(self, inventory):
1312
for entry in inventory.itervalues():
1313
if entry.id is None:
1315
r_inventory[entry.id] = entry
1244
1318
def __call__(self):
1245
1319
cset = Changeset()
1246
for file_id in self.iter_both_tree_ids():
1247
cs_entry = self.make_entry(file_id)
1320
for entry in self.inventory_a.itervalues():
1321
if entry.id is None:
1323
cs_entry = self.make_entry(entry.id)
1248
1324
if cs_entry is not None and not cs_entry.is_boring():
1249
1325
cset.add_entry(cs_entry)
1327
for entry in self.inventory_b.itervalues():
1328
if entry.id is None:
1330
if not self.r_inventory_a.has_key(entry.id):
1331
cs_entry = self.make_entry(entry.id)
1332
if cs_entry is not None and not cs_entry.is_boring():
1333
cset.add_entry(cs_entry)
1251
1334
for entry in list(cset.entries.itervalues()):
1252
1335
if entry.parent != entry.new_parent:
1253
1336
if not cset.entries.has_key(entry.parent) and\
1261
1344
cset.add_entry(parent_entry)
1264
def iter_inventory(self, tree):
1265
for file_id in tree:
1266
yield self.get_entry(file_id, tree)
1268
def get_entry(self, file_id, tree):
1269
if not tree.has_or_had_id(file_id):
1271
return tree.inventory[file_id]
1273
def get_entry_parent(self, entry):
1276
return entry.parent_id
1278
def get_path(self, file_id, tree):
1279
if not tree.has_or_had_id(file_id):
1281
path = tree.id2path(file_id)
1287
def make_basic_entry(self, file_id, only_interesting):
1288
entry_a = self.get_entry(file_id, self.tree_a)
1289
entry_b = self.get_entry(file_id, self.tree_b)
1347
def get_entry_parent(self, entry, inventory):
1350
if entry.path == "./.":
1352
dirname = os.path.dirname(entry.path)
1355
parent = inventory[dirname]
1358
def get_paths(self, entry, tree):
1361
full_path = tree.readonly_path(entry.id)
1362
if entry.path == ".":
1363
return ("", full_path)
1364
return (entry.path, full_path)
1366
def make_basic_entry(self, id, only_interesting):
1367
entry_a = self.r_inventory_a.get(id)
1368
entry_b = self.r_inventory_b.get(id)
1290
1369
if only_interesting and not self.is_interesting(entry_a, entry_b):
1292
parent = self.get_entry_parent(entry_a)
1293
path = self.get_path(file_id, self.tree_a)
1294
cs_entry = ChangesetEntry(file_id, parent, path)
1295
new_parent = self.get_entry_parent(entry_b)
1297
new_path = self.get_path(file_id, self.tree_b)
1370
return (None, None, None)
1371
parent = self.get_entry_parent(entry_a, self.inventory_a)
1372
(path, full_path_a) = self.get_paths(entry_a, self.tree_a)
1373
cs_entry = ChangesetEntry(id, parent, path)
1374
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1377
(new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1299
1379
cs_entry.new_path = new_path
1300
1380
cs_entry.new_parent = new_parent
1381
return (cs_entry, full_path_a, full_path_b)
1303
1383
def is_interesting(self, entry_a, entry_b):
1304
if self._interesting_ids is None:
1306
1384
if entry_a is not None:
1307
file_id = entry_a.file_id
1308
elif entry_b is not None:
1309
file_id = entry_b.file_id
1312
return file_id in self._interesting_ids
1385
if entry_a.interesting:
1387
if entry_b is not None:
1388
if entry_b.interesting:
1314
1392
def make_boring_entry(self, id):
1315
cs_entry = self.make_basic_entry(id, only_interesting=False)
1393
(cs_entry, full_path_a, full_path_b) = \
1394
self.make_basic_entry(id, only_interesting=False)
1316
1395
if cs_entry.is_creation_or_deletion():
1317
1396
return self.make_entry(id, only_interesting=False)
1319
1398
return cs_entry
1321
1401
def make_entry(self, id, only_interesting=True):
1322
cs_entry = self.make_basic_entry(id, only_interesting)
1402
(cs_entry, full_path_a, full_path_b) = \
1403
self.make_basic_entry(id, only_interesting)
1324
1405
if cs_entry is None:
1327
cs_entry.metadata_change = self.make_exec_flag_change(id)
1329
if id in self.tree_a and id in self.tree_b:
1330
a_sha1 = self.tree_a.get_file_sha1(id)
1331
b_sha1 = self.tree_b.get_file_sha1(id)
1332
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1335
cs_entry.contents_change = self.make_contents_change(id)
1408
stat_a = self.lstat(full_path_a)
1409
stat_b = self.lstat(full_path_b)
1411
cs_entry.new_parent = None
1412
cs_entry.new_path = None
1414
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1415
cs_entry.contents_change = self.make_contents_change(full_path_a,
1336
1419
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)
1421
def make_mode_change(self, stat_a, stat_b):
1423
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1424
mode_a = stat_a.st_mode & 0777
1426
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1427
mode_b = stat_b.st_mode & 0777
1428
if mode_a == mode_b:
1430
return ChangeUnixPermissions(mode_a, mode_b)
1432
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1433
if stat_a is None and stat_b is None:
1435
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1436
stat.S_ISDIR(stat_b.st_mode):
1438
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1439
stat.S_ISREG(stat_b.st_mode):
1440
if stat_a.st_ino == stat_b.st_ino and \
1441
stat_a.st_dev == stat_b.st_dev:
1443
if file(full_path_a, "rb").read() == \
1444
file(full_path_b, "rb").read():
1447
patch_contents = patch.diff(full_path_a,
1448
file(full_path_b, "rb").read())
1449
if patch_contents is None:
1451
return PatchApply(patch_contents)
1453
a_contents = self.get_contents(stat_a, full_path_a)
1454
b_contents = self.get_contents(stat_b, full_path_b)
1353
1455
if a_contents == b_contents:
1355
1457
return ReplaceContents(a_contents, b_contents)
1459
def get_contents(self, stat_result, full_path):
1460
if stat_result is None:
1462
elif stat.S_ISREG(stat_result.st_mode):
1463
return FileCreate(file(full_path, "rb").read())
1464
elif stat.S_ISDIR(stat_result.st_mode):
1466
elif stat.S_ISLNK(stat_result.st_mode):
1467
return SymlinkCreate(os.readlink(full_path))
1469
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))
1471
def lstat(self, full_path):
1473
if full_path is not None:
1475
stat_result = os.lstat(full_path)
1477
if e.errno != errno.ENOENT:
1373
1482
def full_path(entry, tree):
1374
return pathjoin(tree.basedir, entry.path)
1483
return os.path.join(tree.root, entry.path)
1377
1485
def new_delete_entry(entry, tree, inventory, delete):
1378
1486
if entry.path == "":