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):
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:
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":
304
class ReplaceContents(object):
263
def reversed(sequence):
264
max = len(sequence) - 1
265
for i in range(len(sequence)):
266
yield sequence[max - i]
268
class ReplaceContents:
305
269
"""A contents-replacement framework. It allows a file/directory/symlink to
306
270
be created, deleted, or replaced with another file/directory/symlink.
307
271
Arguments must be callable with (filename, reverse).
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
372
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):
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)
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-", 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)
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)
705
def summarize_name(self):
656
def summarize_name(self, changeset, reverse=False):
706
657
"""Produce a one-line summary of the filename. Indicates renames as
707
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
712
667
orig_path = self.get_cset_path(False)
713
668
mod_path = self.get_cset_path(True)
714
if orig_path and orig_path.startswith('./'):
669
if orig_path is not None:
715
670
orig_path = orig_path[2:]
716
if mod_path and mod_path.startswith('./'):
671
if mod_path is not None:
717
672
mod_path = mod_path[2:]
718
673
if orig_path == mod_path:
721
return "%s => %s" % (orig_path, mod_path)
723
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):
724
683
"""Determine the full pathname to rename to
726
685
:param id_map: The map of ids to filenames for the tree
727
686
:type id_map: Dictionary
728
687
:param changeset: The changeset to get data from
729
688
:type changeset: `Changeset`
689
: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
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
739
706
if to_name is None:
742
709
if parent == NULL_ID or parent is None:
744
711
raise SourceRootHasName(self, to_name)
747
parent_entry = changeset.entries.get(parent)
748
if parent_entry is None:
714
if from_dir == to_dir:
749
715
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)
717
parent_entry = changeset.entries[parent]
718
dir = parent_entry.get_new_path(id_map, changeset, reverse)
753
719
if from_name == to_name:
754
720
name = os.path.basename(id_map[self.id])
757
723
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
758
return pathjoin(dir, name)
724
return os.path.join(dir, name)
760
726
def is_boring(self):
761
727
"""Determines whether the entry does nothing
855
837
:type inventory: Dictionary
856
838
:param dir: The directory to apply changes to
840
:param reverse: Apply changes in reverse
858
842
:return: a mapping of id to temporary name
859
843
:rtype: Dictionary
845
temp_dir = os.path.join(dir, "temp")
862
847
for i in range(len(source_entries)):
863
848
entry = source_entries[i]
864
if entry.is_deletion():
865
path = pathjoin(dir, inventory[entry.id])
866
entry.apply(path, conflict_handler)
867
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)
869
elif entry.needs_rename():
870
if entry.is_creation():
872
to_name = pathjoin(temp_dir, str(i))
854
to_name = temp_dir+"/"+str(i)
873
855
src_path = inventory.get(entry.id)
874
856
if src_path is not None:
875
src_path = pathjoin(dir, src_path)
857
src_path = os.path.join(dir, src_path)
877
rename(src_path, to_name)
859
os.rename(src_path, to_name)
878
860
temp_name[entry.id] = to_name
879
861
except OSError, e:
880
862
if e.errno != errno.ENOENT:
882
if conflict_handler.missing_for_rename(src_path, to_name) \
864
if conflict_handler.missing_for_rename(src_path) == "skip":
889
def rename_to_new_create(changed_inventory, target_entries, inventory,
890
changeset, dir, conflict_handler):
870
def rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
871
conflict_handler, reverse):
891
872
"""Rename entries with temp names to their final names, create new files.
893
:param changed_inventory: A mapping of id to temporary name
894
:type changed_inventory: Dictionary
874
:param temp_name: A mapping of id to temporary name
875
:type temp_name: Dictionary
895
876
:param target_entries: The entries to apply changes to
896
877
:type target_entries: List of `ChangesetEntry`
897
878
:param changeset: The changeset to apply
898
879
:type changeset: `Changeset`
899
880
:param dir: The directory to apply changes to
882
:param reverse: If true, apply changes in reverse
902
885
for entry in target_entries:
903
new_tree_path = entry.get_new_path(inventory, changeset)
904
if new_tree_path is None:
886
new_path = entry.get_new_path(inventory, changeset, reverse)
906
new_path = pathjoin(dir, new_tree_path)
907
old_path = changed_inventory.get(entry.id)
908
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):
909
892
if conflict_handler.target_exists(entry, new_path, old_path) == \
912
if entry.is_creation():
913
entry.apply(new_path, conflict_handler)
914
changed_inventory[entry.id] = new_tree_path
915
elif entry.needs_rename():
916
if entry.is_deletion():
895
if entry.is_creation(reverse):
896
entry.apply(new_path, conflict_handler, reverse)
918
898
if old_path is None:
921
mutter('rename %s to final name %s', old_path, new_path)
922
rename(old_path, new_path)
923
changed_inventory[entry.id] = new_tree_path
901
os.rename(old_path, new_path)
924
902
except OSError, e:
925
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
926
% (old_path, new_path, entry, e))
903
raise Exception ("%s is missing" % new_path)
929
905
class TargetExists(Exception):
930
906
def __init__(self, entry, target):
1021
1003
class MissingForRename(Exception):
1022
def __init__(self, filename, to_path):
1023
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)
1024
1006
Exception.__init__(self, msg)
1025
1007
self.filename = filename
1028
class NewContentsConflict(Exception):
1029
def __init__(self, filename):
1030
msg = "Conflicting contents for new file %s" % (filename)
1031
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
class MissingForMerge(Exception):
1047
def __init__(self, filename):
1048
msg = "The file %s was modified, but does not exist in this tree"\
1050
Exception.__init__(self, msg)
1053
class ExceptionConflictHandler(object):
1054
"""Default handler for merge exceptions.
1056
This throws an error on any kind of conflict. Conflict handlers can
1057
descend from this class if they have a better way to handle some or
1058
all types of conflict.
1009
class ExceptionConflictHandler:
1010
def __init__(self, dir):
1060
1013
def missing_parent(self, pathname):
1061
1014
parent = os.path.dirname(pathname)
1062
1015
raise Exception("Parent directory missing for %s" % pathname)
1073
1026
def rename_conflict(self, id, this_name, base_name, other_name):
1074
1027
raise RenameConflict(id, this_name, base_name, other_name)
1076
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)
1077
1033
raise MoveConflict(id, this_dir, base_dir, other_dir)
1079
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):
1080
1036
os.unlink(new_file)
1081
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)
1083
1042
def wrong_old_contents(self, filename, expected_contents):
1084
1043
raise WrongOldContents(filename)
1086
1045
def rem_contents_conflict(self, filename, this_contents, base_contents):
1087
1046
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)
1048
def wrong_old_perms(self, filename, old_perms, new_perms):
1049
raise WrongOldPermissions(filename, old_perms, new_perms)
1092
1051
def rmdir_non_empty(self, filename):
1093
1052
raise DeletingNonEmptyDirectory(filename)
1098
1057
def patch_target_missing(self, filename, contents):
1099
1058
raise PatchTargetMissing(filename)
1101
def missing_for_exec_flag(self, filename):
1102
raise MissingForExecFlag(filename)
1060
def missing_for_chmod(self, filename):
1061
raise MissingPermsFile(filename)
1104
1063
def missing_for_rm(self, filename, change):
1105
1064
raise MissingForRm(filename)
1107
def missing_for_rename(self, filename, to_path):
1108
raise MissingForRename(filename, to_path)
1110
def missing_for_merge(self, file_id, other_path):
1111
raise MissingForMerge(other_path)
1113
def new_contents_conflict(self, filename, other_contents):
1114
raise NewContentsConflict(filename)
1116
def weave_merge_conflict(self, filename, weave, other_i, out_file):
1117
raise WeaveMergeConflict(filename)
1119
def threeway_contents_conflict(self, filename, this_contents,
1120
base_contents, other_contents):
1121
raise ThreewayContentsConflict(filename)
1127
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1066
def missing_for_rename(self, filename):
1067
raise MissingForRename(filename)
1069
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1128
1071
"""Apply a changeset to a directory.
1130
1073
:param changeset: The changes to perform
1133
1076
:type inventory: Dictionary
1134
1077
:param dir: The path of the directory to apply the changes to
1079
:param reverse: If true, apply the changes in reverse
1136
1081
:return: The mapping of the changed entries
1137
1082
:rtype: Dictionary
1139
1084
if conflict_handler is None:
1140
conflict_handler = ExceptionConflictHandler()
1141
temp_dir = pathjoin(dir, "bzr-tree-change")
1145
if e.errno == errno.EEXIST:
1149
if e.errno == errno.ENOTEMPTY:
1150
raise OldFailedTreeOp()
1085
conflict_handler = ExceptionConflictHandler(dir)
1086
temp_dir = dir+"/temp"
1155
1089
#apply changes that don't affect filenames
1156
1090
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)
1091
if not entry.is_creation_or_deletion():
1092
path = os.path.join(dir, inventory[entry.id])
1093
entry.apply(path, conflict_handler, reverse)
1165
1095
# Apply renames in stages, to minimize conflicts:
1166
1096
# Only files whose name or parent change are interesting, because their
1167
1097
# target name may exist in the source tree. If a directory's name changes,
1168
1098
# that doesn't make its children interesting.
1169
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1171
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1172
temp_dir, conflict_handler)
1174
rename_to_new_create(changed_inventory, target_entries, inventory,
1175
changeset, dir, conflict_handler)
1099
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1102
temp_name = rename_to_temp_delete(source_entries, inventory, dir,
1103
conflict_handler, reverse)
1105
rename_to_new_create(temp_name, target_entries, inventory, changeset, dir,
1106
conflict_handler, reverse)
1176
1107
os.rmdir(temp_dir)
1177
return changed_inventory
1108
r_inventory = invert_dict(inventory)
1109
new_entries, removed_entries = get_inventory_change(inventory,
1110
r_inventory, changeset, reverse)
1112
for path, file_id in new_entries.iteritems():
1113
new_inventory[file_id] = path
1114
for file_id in removed_entries:
1115
new_inventory[file_id] = None
1116
return new_inventory
1119
def apply_changeset_tree(cset, tree, reverse=False):
1121
for entry in tree.source_inventory().itervalues():
1122
inventory[entry.id] = entry.path
1123
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1125
new_entries, remove_entries = \
1126
get_inventory_change(inventory, new_inventory, cset, reverse)
1127
tree.update_source_inventory(new_entries, remove_entries)
1130
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1133
r_inventory = invert_dict(inventory)
1134
r_new_inventory = invert_dict(new_inventory)
1135
for entry in cset.entries.itervalues():
1136
if entry.needs_rename():
1137
old_path = r_inventory.get(entry.id)
1138
if old_path is not None:
1139
remove_entries.append(old_path)
1141
new_path = entry.get_new_path(inventory, cset)
1142
if new_path is not None:
1143
new_entries[new_path] = entry.id
1144
return new_entries, remove_entries
1180
1147
def print_changeset(cset):
1190
1157
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." \
1159
class CompositionFailure(Exception):
1160
def __init__(self, old_entry, new_entry, problem):
1161
msg = "Unable to conpose entries.\n %s" % problem
1162
Exception.__init__(self, msg)
1164
class IDMismatch(CompositionFailure):
1165
def __init__(self, old_entry, new_entry):
1166
problem = "Attempt to compose entries with different ids: %s and %s" %\
1167
(old_entry.id, new_entry.id)
1168
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1170
def compose_changesets(old_cset, new_cset):
1171
"""Combine two changesets into one. This works well for exact patching.
1172
Otherwise, not so well.
1174
:param old_cset: The first changeset that would be applied
1175
:type old_cset: `Changeset`
1176
:param new_cset: The second changeset that would be applied
1177
:type new_cset: `Changeset`
1178
:return: A changeset that combines the changes in both changesets
1181
composed = Changeset()
1182
for old_entry in old_cset.entries.itervalues():
1183
new_entry = new_cset.entries.get(old_entry.id)
1184
if new_entry is None:
1185
composed.add_entry(old_entry)
1187
composed_entry = compose_entries(old_entry, new_entry)
1188
if composed_entry.parent is not None or\
1189
composed_entry.new_parent is not None:
1190
composed.add_entry(composed_entry)
1191
for new_entry in new_cset.entries.itervalues():
1192
if not old_cset.entries.has_key(new_entry.id):
1193
composed.add_entry(new_entry)
1196
def compose_entries(old_entry, new_entry):
1197
"""Combine two entries into one.
1199
:param old_entry: The first entry that would be applied
1200
:type old_entry: ChangesetEntry
1201
:param old_entry: The second entry that would be applied
1202
:type old_entry: ChangesetEntry
1203
:return: A changeset entry combining both entries
1204
:rtype: `ChangesetEntry`
1206
if old_entry.id != new_entry.id:
1207
raise IDMismatch(old_entry, new_entry)
1208
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1210
if (old_entry.parent != old_entry.new_parent or
1211
new_entry.parent != new_entry.new_parent):
1212
output.new_parent = new_entry.new_parent
1214
if (old_entry.path != old_entry.new_path or
1215
new_entry.path != new_entry.new_path):
1216
output.new_path = new_entry.new_path
1218
output.contents_change = compose_contents(old_entry, new_entry)
1219
output.metadata_change = compose_metadata(old_entry, new_entry)
1222
def compose_contents(old_entry, new_entry):
1223
"""Combine the contents of two changeset entries. Entries are combined
1224
intelligently where possible, but the fallback behavior returns an
1227
:param old_entry: The first entry that would be applied
1228
:type old_entry: `ChangesetEntry`
1229
:param new_entry: The second entry that would be applied
1230
:type new_entry: `ChangesetEntry`
1231
:return: A combined contents change
1232
:rtype: anything supporting the apply(reverse=False) method
1234
old_contents = old_entry.contents_change
1235
new_contents = new_entry.contents_change
1236
if old_entry.contents_change is None:
1237
return new_entry.contents_change
1238
elif new_entry.contents_change is None:
1239
return old_entry.contents_change
1240
elif isinstance(old_contents, ReplaceContents) and \
1241
isinstance(new_contents, ReplaceContents):
1242
if old_contents.old_contents == new_contents.new_contents:
1245
return ReplaceContents(old_contents.old_contents,
1246
new_contents.new_contents)
1247
elif isinstance(old_contents, ApplySequence):
1248
output = ApplySequence(old_contents.changes)
1249
if isinstance(new_contents, ApplySequence):
1250
output.changes.extend(new_contents.changes)
1252
output.changes.append(new_contents)
1254
elif isinstance(new_contents, ApplySequence):
1255
output = ApplySequence((old_contents.changes,))
1256
output.extend(new_contents.changes)
1259
return ApplySequence((old_contents, new_contents))
1261
def compose_metadata(old_entry, new_entry):
1262
old_meta = old_entry.metadata_change
1263
new_meta = new_entry.metadata_change
1264
if old_meta is None:
1266
elif new_meta is None:
1268
elif isinstance(old_meta, ChangeUnixPermissions) and \
1269
isinstance(new_meta, ChangeUnixPermissions):
1270
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1272
return ApplySequence(old_meta, new_meta)
1275
def changeset_is_null(changeset):
1276
for entry in changeset.entries.itervalues():
1277
if not entry.is_boring():
1281
class UnsuppportedFiletype(Exception):
1282
def __init__(self, full_path, stat_result):
1283
msg = "The file \"%s\" is not a supported filetype." % full_path
1197
1284
Exception.__init__(self, msg)
1198
1285
self.full_path = full_path
1202
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1203
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1286
self.stat_result = stat_result
1288
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1289
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1206
1291
class ChangesetGenerator(object):
1207
def __init__(self, tree_a, tree_b, interesting_ids=None):
1292
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1208
1293
object.__init__(self)
1209
1294
self.tree_a = tree_a
1210
1295
self.tree_b = tree_b
1211
self._interesting_ids = interesting_ids
1296
if inventory_a is not None:
1297
self.inventory_a = inventory_a
1299
self.inventory_a = tree_a.inventory()
1300
if inventory_b is not None:
1301
self.inventory_b = inventory_b
1303
self.inventory_b = tree_b.inventory()
1304
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1305
self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1213
def iter_both_tree_ids(self):
1214
for file_id in self.tree_a:
1216
for file_id in self.tree_b:
1217
if file_id not in self.tree_a:
1307
def reverse_inventory(self, inventory):
1309
for entry in inventory.itervalues():
1310
if entry.id is None:
1312
r_inventory[entry.id] = entry
1220
1315
def __call__(self):
1221
1316
cset = Changeset()
1222
for file_id in self.iter_both_tree_ids():
1223
cs_entry = self.make_entry(file_id)
1317
for entry in self.inventory_a.itervalues():
1318
if entry.id is None:
1320
cs_entry = self.make_entry(entry.id)
1224
1321
if cs_entry is not None and not cs_entry.is_boring():
1225
1322
cset.add_entry(cs_entry)
1324
for entry in self.inventory_b.itervalues():
1325
if entry.id is None:
1327
if not self.r_inventory_a.has_key(entry.id):
1328
cs_entry = self.make_entry(entry.id)
1329
if cs_entry is not None and not cs_entry.is_boring():
1330
cset.add_entry(cs_entry)
1227
1331
for entry in list(cset.entries.itervalues()):
1228
1332
if entry.parent != entry.new_parent:
1229
1333
if not cset.entries.has_key(entry.parent) and\
1237
1341
cset.add_entry(parent_entry)
1240
def iter_inventory(self, tree):
1241
for file_id in tree:
1242
yield self.get_entry(file_id, tree)
1244
def get_entry(self, file_id, tree):
1245
if not tree.has_or_had_id(file_id):
1247
return tree.inventory[file_id]
1249
def get_entry_parent(self, entry):
1252
return entry.parent_id
1254
def get_path(self, file_id, tree):
1255
if not tree.has_or_had_id(file_id):
1257
path = tree.id2path(file_id)
1263
def make_basic_entry(self, file_id, only_interesting):
1264
entry_a = self.get_entry(file_id, self.tree_a)
1265
entry_b = self.get_entry(file_id, self.tree_b)
1344
def get_entry_parent(self, entry, inventory):
1347
if entry.path == "./.":
1349
dirname = os.path.dirname(entry.path)
1352
parent = inventory[dirname]
1355
def get_paths(self, entry, tree):
1358
full_path = tree.readonly_path(entry.id)
1359
if entry.path == ".":
1360
return ("", full_path)
1361
return (entry.path, full_path)
1363
def make_basic_entry(self, id, only_interesting):
1364
entry_a = self.r_inventory_a.get(id)
1365
entry_b = self.r_inventory_b.get(id)
1266
1366
if only_interesting and not self.is_interesting(entry_a, entry_b):
1268
parent = self.get_entry_parent(entry_a)
1269
path = self.get_path(file_id, self.tree_a)
1270
cs_entry = ChangesetEntry(file_id, parent, path)
1271
new_parent = self.get_entry_parent(entry_b)
1273
new_path = self.get_path(file_id, self.tree_b)
1367
return (None, None, None)
1368
parent = self.get_entry_parent(entry_a, self.inventory_a)
1369
(path, full_path_a) = self.get_paths(entry_a, self.tree_a)
1370
cs_entry = ChangesetEntry(id, parent, path)
1371
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1374
(new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1275
1376
cs_entry.new_path = new_path
1276
1377
cs_entry.new_parent = new_parent
1378
return (cs_entry, full_path_a, full_path_b)
1279
1380
def is_interesting(self, entry_a, entry_b):
1280
if self._interesting_ids is None:
1282
1381
if entry_a is not None:
1283
file_id = entry_a.file_id
1284
elif entry_b is not None:
1285
file_id = entry_b.file_id
1288
return file_id in self._interesting_ids
1382
if entry_a.interesting:
1384
if entry_b is not None:
1385
if entry_b.interesting:
1290
1389
def make_boring_entry(self, id):
1291
cs_entry = self.make_basic_entry(id, only_interesting=False)
1390
(cs_entry, full_path_a, full_path_b) = \
1391
self.make_basic_entry(id, only_interesting=False)
1292
1392
if cs_entry.is_creation_or_deletion():
1293
1393
return self.make_entry(id, only_interesting=False)
1295
1395
return cs_entry
1297
1398
def make_entry(self, id, only_interesting=True):
1298
cs_entry = self.make_basic_entry(id, only_interesting)
1399
(cs_entry, full_path_a, full_path_b) = \
1400
self.make_basic_entry(id, only_interesting)
1300
1402
if cs_entry is None:
1303
cs_entry.metadata_change = self.make_exec_flag_change(id)
1305
if id in self.tree_a and id in self.tree_b:
1306
a_sha1 = self.tree_a.get_file_sha1(id)
1307
b_sha1 = self.tree_b.get_file_sha1(id)
1308
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1311
cs_entry.contents_change = self.make_contents_change(id)
1405
stat_a = self.lstat(full_path_a)
1406
stat_b = self.lstat(full_path_b)
1408
cs_entry.new_parent = None
1409
cs_entry.new_path = None
1411
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1412
cs_entry.contents_change = self.make_contents_change(full_path_a,
1312
1416
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)
1418
def make_mode_change(self, stat_a, stat_b):
1420
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1421
mode_a = stat_a.st_mode & 0777
1423
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1424
mode_b = stat_b.st_mode & 0777
1425
if mode_a == mode_b:
1427
return ChangeUnixPermissions(mode_a, mode_b)
1429
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1430
if stat_a is None and stat_b is None:
1432
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1433
stat.S_ISDIR(stat_b.st_mode):
1435
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1436
stat.S_ISREG(stat_b.st_mode):
1437
if stat_a.st_ino == stat_b.st_ino and \
1438
stat_a.st_dev == stat_b.st_dev:
1440
if file(full_path_a, "rb").read() == \
1441
file(full_path_b, "rb").read():
1444
patch_contents = patch.diff(full_path_a,
1445
file(full_path_b, "rb").read())
1446
if patch_contents is None:
1448
return PatchApply(patch_contents)
1450
a_contents = self.get_contents(stat_a, full_path_a)
1451
b_contents = self.get_contents(stat_b, full_path_b)
1329
1452
if a_contents == b_contents:
1331
1454
return ReplaceContents(a_contents, b_contents)
1456
def get_contents(self, stat_result, full_path):
1457
if stat_result is None:
1459
elif stat.S_ISREG(stat_result.st_mode):
1460
return FileCreate(file(full_path, "rb").read())
1461
elif stat.S_ISDIR(stat_result.st_mode):
1463
elif stat.S_ISLNK(stat_result.st_mode):
1464
return SymlinkCreate(os.readlink(full_path))
1466
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))
1468
def lstat(self, full_path):
1470
if full_path is not None:
1472
stat_result = os.lstat(full_path)
1474
if e.errno != errno.ENOENT:
1349
1479
def full_path(entry, tree):
1350
return pathjoin(tree.basedir, entry.path)
1480
return os.path.join(tree.root, entry.path)
1353
1482
def new_delete_entry(entry, tree, inventory, delete):
1354
1483
if entry.path == "":