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
27
class OldFailedTreeOp(Exception):
43
28
def __init__(self):
44
29
Exception.__init__(self, "bzr-tree-change contains files from a"
45
30
" previous failed merge operation.")
48
31
def invert_dict(dict):
50
33
for (key,value) in dict.iteritems():
51
34
newdict[value] = key
55
class ChangeExecFlag(object):
38
class PatchApply(object):
39
"""Patch application as a kind of content change"""
40
def __init__(self, contents):
43
:param contents: The text of the patch to apply
44
:type contents: str"""
45
self.contents = contents
47
def __eq__(self, other):
48
if not isinstance(other, PatchApply):
50
elif self.contents != other.contents:
55
def __ne__(self, other):
56
return not (self == other)
58
def apply(self, filename, conflict_handler, reverse=False):
59
"""Applies the patch to the specified file.
61
:param filename: the file to apply the patch to
63
:param reverse: If true, apply the patch in reverse
66
input_name = filename+".orig"
68
os.rename(filename, input_name)
70
if e.errno != errno.ENOENT:
72
if conflict_handler.patch_target_missing(filename, self.contents)\
75
os.rename(filename, input_name)
78
status = patch.patch(self.contents, input_name, filename,
80
os.chmod(filename, os.stat(input_name).st_mode)
84
conflict_handler.failed_hunks(filename)
87
class ChangeUnixPermissions(object):
56
88
"""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
90
def __init__(self, old_mode, new_mode):
91
self.old_mode = old_mode
92
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
94
def apply(self, filename, conflict_handler, reverse=False):
96
from_mode = self.old_mode
97
to_mode = self.new_mode
99
from_mode = self.new_mode
100
to_mode = self.old_mode
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
102
current_mode = os.stat(filename).st_mode &0777
67
103
except OSError, e:
68
104
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
105
if conflict_handler.missing_for_chmod(filename) == "skip":
72
current_exec_flag = from_exec_flag
108
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":
110
if from_mode is not None and current_mode != from_mode:
111
if conflict_handler.wrong_old_perms(filename, from_mode,
112
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
115
if to_mode is not None:
93
117
os.chmod(filename, to_mode)
94
118
except IOError, e:
95
119
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
120
conflict_handler.missing_for_chmod(filename)
98
122
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)
123
if not isinstance(other, ChangeUnixPermissions):
125
elif self.old_mode != other.old_mode:
127
elif self.new_mode != other.new_mode:
103
132
def __ne__(self, other):
104
133
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse=False):
135
def dir_create(filename, conflict_handler, reverse):
108
136
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
137
used with ReplaceContents.
235
261
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":
266
def reversed(sequence):
267
max = len(sequence) - 1
268
for i in range(len(sequence)):
269
yield sequence[max - i]
304
271
class ReplaceContents(object):
305
272
"""A contents-replacement framework. It allows a file/directory/symlink to
359
332
undo(filename, conflict_handler, reverse=True)
360
333
if perform is not None:
361
perform(filename, conflict_handler)
334
perform(filename, conflict_handler, reverse=False)
362
335
if mode is not None:
363
336
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
338
class ApplySequence(object):
339
def __init__(self, changes=None):
341
if changes is not None:
342
self.changes.extend(changes)
344
def __eq__(self, other):
345
if not isinstance(other, ApplySequence):
347
elif len(other.changes) != len(self.changes):
350
for i in range(len(self.changes)):
351
if self.changes[i] != other.changes[i]:
355
def __ne__(self, other):
356
return not (self == other)
359
def apply(self, filename, conflict_handler, reverse=False):
363
iter = reversed(self.changes)
365
change.apply(filename, conflict_handler, reverse)
372
368
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):
369
def __init__(self, base_file, other_file):
370
self.base_file = base_file
371
self.other_file = other_file
385
373
def __eq__(self, other):
386
374
if not isinstance(other, Diff3Merge):
388
return (self.base == other.base and
389
self.other == other.other and self.file_id == other.file_id)
376
return (self.base_file == other.base_file and
377
self.other_file == other.other_file)
391
379
def __ne__(self, other):
392
380
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,
382
def apply(self, filename, conflict_handler, reverse=False):
383
new_file = filename+".new"
385
base = self.base_file
386
other = self.other_file
388
base = self.other_file
389
other = self.base_file
390
status = patch.diff3(new_file, filename, base, other)
392
os.chmod(new_file, os.stat(filename).st_mode)
393
os.rename(new_file, filename)
397
conflict_handler.merge_conflict(new_file, filename, base, other)
706
def summarize_name(self):
659
def summarize_name(self, changeset, reverse=False):
707
660
"""Produce a one-line summary of the filename. Indicates renames as
708
661
old => new, indicates creation as None => new, indicates deletion as
664
:param changeset: The changeset to get paths from
665
:type changeset: `Changeset`
666
:param reverse: If true, reverse the names in the output
713
670
orig_path = self.get_cset_path(False)
714
671
mod_path = self.get_cset_path(True)
715
if orig_path and orig_path.startswith('./'):
672
if orig_path is not None:
716
673
orig_path = orig_path[2:]
717
if mod_path and mod_path.startswith('./'):
674
if mod_path is not None:
718
675
mod_path = mod_path[2:]
719
676
if orig_path == mod_path:
722
return "%s => %s" % (orig_path, mod_path)
724
def get_new_path(self, id_map, changeset):
680
return "%s => %s" % (orig_path, mod_path)
682
return "%s => %s" % (mod_path, orig_path)
685
def get_new_path(self, id_map, changeset, reverse=False):
725
686
"""Determine the full pathname to rename to
727
688
:param id_map: The map of ids to filenames for the tree
728
689
:type id_map: Dictionary
729
690
:param changeset: The changeset to get data from
730
691
:type changeset: `Changeset`
692
: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
699
from_dir = self.new_dir
701
from_name = self.new_name
703
parent = self.new_parent
704
to_dir = self.new_dir
706
to_name = self.new_name
707
from_name = self.name
740
709
if to_name is None:
743
712
if parent == NULL_ID or parent is None:
745
714
raise SourceRootHasName(self, to_name)
748
parent_entry = changeset.entries.get(parent)
749
if parent_entry is None:
717
if from_dir == to_dir:
750
718
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)
720
parent_entry = changeset.entries[parent]
721
dir = parent_entry.get_new_path(id_map, changeset, reverse)
754
722
if from_name == to_name:
755
723
name = os.path.basename(id_map[self.id])
758
726
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
759
return pathjoin(dir, name)
727
return os.path.join(dir, name)
761
729
def is_boring(self):
762
730
"""Determines whether the entry does nothing
856
840
:type inventory: Dictionary
857
841
:param dir: The directory to apply changes to
843
:param reverse: Apply changes in reverse
859
845
:return: a mapping of id to temporary name
860
846
:rtype: Dictionary
863
849
for i in range(len(source_entries)):
864
850
entry = source_entries[i]
865
if entry.is_deletion():
866
path = pathjoin(dir, inventory[entry.id])
867
entry.apply(path, conflict_handler)
851
if entry.is_deletion(reverse):
852
path = os.path.join(dir, inventory[entry.id])
853
entry.apply(path, conflict_handler, reverse)
868
854
temp_name[entry.id] = None
870
elif entry.needs_rename():
871
if entry.is_creation():
873
to_name = pathjoin(temp_dir, str(i))
857
to_name = os.path.join(temp_dir, str(i))
874
858
src_path = inventory.get(entry.id)
875
859
if src_path is not None:
876
src_path = pathjoin(dir, src_path)
860
src_path = os.path.join(dir, src_path)
878
rename(src_path, to_name)
862
os.rename(src_path, to_name)
879
863
temp_name[entry.id] = to_name
880
864
except OSError, e:
881
865
if e.errno != errno.ENOENT:
883
if conflict_handler.missing_for_rename(src_path, to_name) \
867
if conflict_handler.missing_for_rename(src_path) == "skip":
890
873
def rename_to_new_create(changed_inventory, target_entries, inventory,
891
changeset, dir, conflict_handler):
874
changeset, dir, conflict_handler, reverse):
892
875
"""Rename entries with temp names to their final names, create new files.
894
877
:param changed_inventory: A mapping of id to temporary name
899
882
:type changeset: `Changeset`
900
883
:param dir: The directory to apply changes to
885
:param reverse: If true, apply changes in reverse
903
888
for entry in target_entries:
904
new_tree_path = entry.get_new_path(inventory, changeset)
889
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
905
890
if new_tree_path is None:
907
new_path = pathjoin(dir, new_tree_path)
892
new_path = os.path.join(dir, new_tree_path)
908
893
old_path = changed_inventory.get(entry.id)
909
if bzrlib.osutils.lexists(new_path):
894
if os.path.exists(new_path):
910
895
if conflict_handler.target_exists(entry, new_path, old_path) == \
913
if entry.is_creation():
914
entry.apply(new_path, conflict_handler)
898
if entry.is_creation(reverse):
899
entry.apply(new_path, conflict_handler, reverse)
915
900
changed_inventory[entry.id] = new_tree_path
916
elif entry.needs_rename():
917
if entry.is_deletion():
919
902
if old_path is None:
922
mutter('rename %s to final name %s', old_path, new_path)
923
rename(old_path, new_path)
905
os.rename(old_path, new_path)
924
906
changed_inventory[entry.id] = new_tree_path
925
907
except OSError, e:
926
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
927
% (old_path, new_path, entry, e))
908
raise Exception ("%s is missing" % new_path)
930
910
class TargetExists(Exception):
931
911
def __init__(self, entry, target):
1074
1044
def rename_conflict(self, id, this_name, base_name, other_name):
1075
1045
raise RenameConflict(id, this_name, base_name, other_name)
1077
def move_conflict(self, id, this_dir, base_dir, other_dir):
1047
def move_conflict(self, id, inventory):
1048
this_dir = inventory.this.get_dir(id)
1049
base_dir = inventory.base.get_dir(id)
1050
other_dir = inventory.other.get_dir(id)
1078
1051
raise MoveConflict(id, this_dir, base_dir, other_dir)
1080
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1053
def merge_conflict(self, new_file, this_path, base_path, other_path):
1081
1054
os.unlink(new_file)
1082
1055
raise MergeConflict(this_path)
1057
def permission_conflict(self, this_path, base_path, other_path):
1058
raise MergePermissionConflict(this_path, base_path, other_path)
1084
1060
def wrong_old_contents(self, filename, expected_contents):
1085
1061
raise WrongOldContents(filename)
1087
1063
def rem_contents_conflict(self, filename, this_contents, base_contents):
1088
1064
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)
1066
def wrong_old_perms(self, filename, old_perms, new_perms):
1067
raise WrongOldPermissions(filename, old_perms, new_perms)
1093
1069
def rmdir_non_empty(self, filename):
1094
1070
raise DeletingNonEmptyDirectory(filename)
1099
1075
def patch_target_missing(self, filename, contents):
1100
1076
raise PatchTargetMissing(filename)
1102
def missing_for_exec_flag(self, filename):
1103
raise MissingForExecFlag(filename)
1078
def missing_for_chmod(self, filename):
1079
raise MissingPermsFile(filename)
1105
1081
def missing_for_rm(self, filename, change):
1106
1082
raise MissingForRm(filename)
1108
def missing_for_rename(self, filename, to_path):
1109
raise MissingForRename(filename, to_path)
1084
def missing_for_rename(self, filename):
1085
raise MissingForRename(filename)
1111
def missing_for_merge(self, file_id, other_path):
1112
raise MissingForMerge(other_path)
1087
def missing_for_merge(self, file_id, inventory):
1088
raise MissingForMerge(inventory.other.get_path(file_id))
1114
1090
def new_contents_conflict(self, filename, other_contents):
1115
1091
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)
1128
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1096
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1129
1098
"""Apply a changeset to a directory.
1131
1100
:param changeset: The changes to perform
1156
1127
#apply changes that don't affect filenames
1157
1128
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)
1129
if not entry.is_creation_or_deletion():
1130
path = os.path.join(dir, inventory[entry.id])
1131
entry.apply(path, conflict_handler, reverse)
1166
1133
# Apply renames in stages, to minimize conflicts:
1167
1134
# Only files whose name or parent change are interesting, because their
1168
1135
# target name may exist in the source tree. If a directory's name changes,
1169
1136
# that doesn't make its children interesting.
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1137
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1172
1140
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1173
temp_dir, conflict_handler)
1141
temp_dir, conflict_handler,
1175
1144
rename_to_new_create(changed_inventory, target_entries, inventory,
1176
changeset, dir, conflict_handler)
1145
changeset, dir, conflict_handler, reverse)
1177
1146
os.rmdir(temp_dir)
1178
1147
return changed_inventory
1150
def apply_changeset_tree(cset, tree, reverse=False):
1152
for entry in tree.source_inventory().itervalues():
1153
inventory[entry.id] = entry.path
1154
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1156
new_entries, remove_entries = \
1157
get_inventory_change(inventory, new_inventory, cset, reverse)
1158
tree.update_source_inventory(new_entries, remove_entries)
1161
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1164
for entry in cset.entries.itervalues():
1165
if entry.needs_rename():
1166
new_path = entry.get_new_path(inventory, cset)
1167
if new_path is None:
1168
remove_entries.append(entry.id)
1170
new_entries[new_path] = entry.id
1171
return new_entries, remove_entries
1181
1174
def print_changeset(cset):
1182
1175
"""Print all non-boring changeset entries
1191
1184
print entry.summarize_name(cset)
1194
class UnsupportedFiletype(Exception):
1195
def __init__(self, kind, full_path):
1196
msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1186
class CompositionFailure(Exception):
1187
def __init__(self, old_entry, new_entry, problem):
1188
msg = "Unable to conpose entries.\n %s" % problem
1189
Exception.__init__(self, msg)
1191
class IDMismatch(CompositionFailure):
1192
def __init__(self, old_entry, new_entry):
1193
problem = "Attempt to compose entries with different ids: %s and %s" %\
1194
(old_entry.id, new_entry.id)
1195
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1197
def compose_changesets(old_cset, new_cset):
1198
"""Combine two changesets into one. This works well for exact patching.
1199
Otherwise, not so well.
1201
:param old_cset: The first changeset that would be applied
1202
:type old_cset: `Changeset`
1203
:param new_cset: The second changeset that would be applied
1204
:type new_cset: `Changeset`
1205
:return: A changeset that combines the changes in both changesets
1208
composed = Changeset()
1209
for old_entry in old_cset.entries.itervalues():
1210
new_entry = new_cset.entries.get(old_entry.id)
1211
if new_entry is None:
1212
composed.add_entry(old_entry)
1214
composed_entry = compose_entries(old_entry, new_entry)
1215
if composed_entry.parent is not None or\
1216
composed_entry.new_parent is not None:
1217
composed.add_entry(composed_entry)
1218
for new_entry in new_cset.entries.itervalues():
1219
if not old_cset.entries.has_key(new_entry.id):
1220
composed.add_entry(new_entry)
1223
def compose_entries(old_entry, new_entry):
1224
"""Combine two entries into one.
1226
:param old_entry: The first entry that would be applied
1227
:type old_entry: ChangesetEntry
1228
:param old_entry: The second entry that would be applied
1229
:type old_entry: ChangesetEntry
1230
:return: A changeset entry combining both entries
1231
:rtype: `ChangesetEntry`
1233
if old_entry.id != new_entry.id:
1234
raise IDMismatch(old_entry, new_entry)
1235
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1237
if (old_entry.parent != old_entry.new_parent or
1238
new_entry.parent != new_entry.new_parent):
1239
output.new_parent = new_entry.new_parent
1241
if (old_entry.path != old_entry.new_path or
1242
new_entry.path != new_entry.new_path):
1243
output.new_path = new_entry.new_path
1245
output.contents_change = compose_contents(old_entry, new_entry)
1246
output.metadata_change = compose_metadata(old_entry, new_entry)
1249
def compose_contents(old_entry, new_entry):
1250
"""Combine the contents of two changeset entries. Entries are combined
1251
intelligently where possible, but the fallback behavior returns an
1254
:param old_entry: The first entry that would be applied
1255
:type old_entry: `ChangesetEntry`
1256
:param new_entry: The second entry that would be applied
1257
:type new_entry: `ChangesetEntry`
1258
:return: A combined contents change
1259
:rtype: anything supporting the apply(reverse=False) method
1261
old_contents = old_entry.contents_change
1262
new_contents = new_entry.contents_change
1263
if old_entry.contents_change is None:
1264
return new_entry.contents_change
1265
elif new_entry.contents_change is None:
1266
return old_entry.contents_change
1267
elif isinstance(old_contents, ReplaceContents) and \
1268
isinstance(new_contents, ReplaceContents):
1269
if old_contents.old_contents == new_contents.new_contents:
1272
return ReplaceContents(old_contents.old_contents,
1273
new_contents.new_contents)
1274
elif isinstance(old_contents, ApplySequence):
1275
output = ApplySequence(old_contents.changes)
1276
if isinstance(new_contents, ApplySequence):
1277
output.changes.extend(new_contents.changes)
1279
output.changes.append(new_contents)
1281
elif isinstance(new_contents, ApplySequence):
1282
output = ApplySequence((old_contents.changes,))
1283
output.extend(new_contents.changes)
1286
return ApplySequence((old_contents, new_contents))
1288
def compose_metadata(old_entry, new_entry):
1289
old_meta = old_entry.metadata_change
1290
new_meta = new_entry.metadata_change
1291
if old_meta is None:
1293
elif new_meta is None:
1295
elif isinstance(old_meta, ChangeUnixPermissions) and \
1296
isinstance(new_meta, ChangeUnixPermissions):
1297
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1299
return ApplySequence(old_meta, new_meta)
1302
def changeset_is_null(changeset):
1303
for entry in changeset.entries.itervalues():
1304
if not entry.is_boring():
1308
class UnsuppportedFiletype(Exception):
1309
def __init__(self, full_path, stat_result):
1310
msg = "The file \"%s\" is not a supported filetype." % full_path
1198
1311
Exception.__init__(self, msg)
1199
1312
self.full_path = full_path
1203
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1204
return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1313
self.stat_result = stat_result
1315
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1316
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1207
1318
class ChangesetGenerator(object):
1208
def __init__(self, tree_a, tree_b, interesting_ids=None):
1319
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1209
1320
object.__init__(self)
1210
1321
self.tree_a = tree_a
1211
1322
self.tree_b = tree_b
1212
self._interesting_ids = interesting_ids
1323
if inventory_a is not None:
1324
self.inventory_a = inventory_a
1326
self.inventory_a = tree_a.inventory()
1327
if inventory_b is not None:
1328
self.inventory_b = inventory_b
1330
self.inventory_b = tree_b.inventory()
1331
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1332
self.r_inventory_b = self.reverse_inventory(self.inventory_b)
1214
def iter_both_tree_ids(self):
1215
for file_id in self.tree_a:
1217
for file_id in self.tree_b:
1218
if file_id not in self.tree_a:
1334
def reverse_inventory(self, inventory):
1336
for entry in inventory.itervalues():
1337
if entry.id is None:
1339
r_inventory[entry.id] = entry
1221
1342
def __call__(self):
1222
1343
cset = Changeset()
1223
for file_id in self.iter_both_tree_ids():
1224
cs_entry = self.make_entry(file_id)
1344
for entry in self.inventory_a.itervalues():
1345
if entry.id is None:
1347
cs_entry = self.make_entry(entry.id)
1225
1348
if cs_entry is not None and not cs_entry.is_boring():
1226
1349
cset.add_entry(cs_entry)
1351
for entry in self.inventory_b.itervalues():
1352
if entry.id is None:
1354
if not self.r_inventory_a.has_key(entry.id):
1355
cs_entry = self.make_entry(entry.id)
1356
if cs_entry is not None and not cs_entry.is_boring():
1357
cset.add_entry(cs_entry)
1228
1358
for entry in list(cset.entries.itervalues()):
1229
1359
if entry.parent != entry.new_parent:
1230
1360
if not cset.entries.has_key(entry.parent) and\
1238
1368
cset.add_entry(parent_entry)
1241
def iter_inventory(self, tree):
1242
for file_id in tree:
1243
yield self.get_entry(file_id, tree)
1245
def get_entry(self, file_id, tree):
1246
if not tree.has_or_had_id(file_id):
1248
return tree.inventory[file_id]
1250
def get_entry_parent(self, entry):
1253
return entry.parent_id
1255
def get_path(self, file_id, tree):
1256
if not tree.has_or_had_id(file_id):
1258
path = tree.id2path(file_id)
1264
def make_basic_entry(self, file_id, only_interesting):
1265
entry_a = self.get_entry(file_id, self.tree_a)
1266
entry_b = self.get_entry(file_id, self.tree_b)
1371
def get_entry_parent(self, entry, inventory):
1374
if entry.path == "./.":
1376
dirname = os.path.dirname(entry.path)
1379
parent = inventory[dirname]
1382
def get_paths(self, entry, tree):
1385
full_path = tree.readonly_path(entry.id)
1386
if entry.path == ".":
1387
return ("", full_path)
1388
return (entry.path, full_path)
1390
def make_basic_entry(self, id, only_interesting):
1391
entry_a = self.r_inventory_a.get(id)
1392
entry_b = self.r_inventory_b.get(id)
1267
1393
if only_interesting and not self.is_interesting(entry_a, entry_b):
1269
parent = self.get_entry_parent(entry_a)
1270
path = self.get_path(file_id, self.tree_a)
1271
cs_entry = ChangesetEntry(file_id, parent, path)
1272
new_parent = self.get_entry_parent(entry_b)
1274
new_path = self.get_path(file_id, self.tree_b)
1394
return (None, None, None)
1395
parent = self.get_entry_parent(entry_a, self.inventory_a)
1396
(path, full_path_a) = self.get_paths(entry_a, self.tree_a)
1397
cs_entry = ChangesetEntry(id, parent, path)
1398
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1401
(new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1276
1403
cs_entry.new_path = new_path
1277
1404
cs_entry.new_parent = new_parent
1405
return (cs_entry, full_path_a, full_path_b)
1280
1407
def is_interesting(self, entry_a, entry_b):
1281
if self._interesting_ids is None:
1283
1408
if entry_a is not None:
1284
file_id = entry_a.file_id
1285
elif entry_b is not None:
1286
file_id = entry_b.file_id
1289
return file_id in self._interesting_ids
1409
if entry_a.interesting:
1411
if entry_b is not None:
1412
if entry_b.interesting:
1291
1416
def make_boring_entry(self, id):
1292
cs_entry = self.make_basic_entry(id, only_interesting=False)
1417
(cs_entry, full_path_a, full_path_b) = \
1418
self.make_basic_entry(id, only_interesting=False)
1293
1419
if cs_entry.is_creation_or_deletion():
1294
1420
return self.make_entry(id, only_interesting=False)
1296
1422
return cs_entry
1298
1425
def make_entry(self, id, only_interesting=True):
1299
cs_entry = self.make_basic_entry(id, only_interesting)
1426
(cs_entry, full_path_a, full_path_b) = \
1427
self.make_basic_entry(id, only_interesting)
1301
1429
if cs_entry is None:
1304
cs_entry.metadata_change = self.make_exec_flag_change(id)
1306
if id in self.tree_a and id in self.tree_b:
1307
a_sha1 = self.tree_a.get_file_sha1(id)
1308
b_sha1 = self.tree_b.get_file_sha1(id)
1309
if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1312
cs_entry.contents_change = self.make_contents_change(id)
1432
stat_a = self.lstat(full_path_a)
1433
stat_b = self.lstat(full_path_b)
1435
cs_entry.new_parent = None
1436
cs_entry.new_path = None
1438
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1439
cs_entry.contents_change = self.make_contents_change(full_path_a,
1313
1443
return cs_entry
1315
def make_exec_flag_change(self, file_id):
1316
exec_flag_a = exec_flag_b = None
1317
if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1318
exec_flag_a = self.tree_a.is_executable(file_id)
1320
if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1321
exec_flag_b = self.tree_b.is_executable(file_id)
1323
if exec_flag_a == exec_flag_b:
1325
return ChangeExecFlag(exec_flag_a, exec_flag_b)
1327
def make_contents_change(self, file_id):
1328
a_contents = get_contents(self.tree_a, file_id)
1329
b_contents = get_contents(self.tree_b, file_id)
1445
def make_mode_change(self, stat_a, stat_b):
1447
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1448
mode_a = stat_a.st_mode & 0777
1450
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1451
mode_b = stat_b.st_mode & 0777
1452
if mode_a == mode_b:
1454
return ChangeUnixPermissions(mode_a, mode_b)
1456
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1457
if stat_a is None and stat_b is None:
1459
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1460
stat.S_ISDIR(stat_b.st_mode):
1462
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1463
stat.S_ISREG(stat_b.st_mode):
1464
if stat_a.st_ino == stat_b.st_ino and \
1465
stat_a.st_dev == stat_b.st_dev:
1467
if file(full_path_a, "rb").read() == \
1468
file(full_path_b, "rb").read():
1471
patch_contents = patch.diff(full_path_a,
1472
file(full_path_b, "rb").read())
1473
if patch_contents is None:
1475
return PatchApply(patch_contents)
1477
a_contents = self.get_contents(stat_a, full_path_a)
1478
b_contents = self.get_contents(stat_b, full_path_b)
1330
1479
if a_contents == b_contents:
1332
1481
return ReplaceContents(a_contents, b_contents)
1483
def get_contents(self, stat_result, full_path):
1484
if stat_result is None:
1486
elif stat.S_ISREG(stat_result.st_mode):
1487
return FileCreate(file(full_path, "rb").read())
1488
elif stat.S_ISDIR(stat_result.st_mode):
1490
elif stat.S_ISLNK(stat_result.st_mode):
1491
return SymlinkCreate(os.readlink(full_path))
1493
raise UnsupportedFiletype(full_path, stat_result)
1335
def get_contents(tree, file_id):
1336
"""Return the appropriate contents to create a copy of file_id from tree"""
1337
if file_id not in tree:
1339
kind = tree.kind(file_id)
1341
return TreeFileCreate(tree, file_id)
1342
elif kind in ("directory", "root_directory"):
1344
elif kind == "symlink":
1345
return SymlinkCreate(tree.get_symlink_target(file_id))
1347
raise UnsupportedFiletype(kind, tree.id2path(file_id))
1495
def lstat(self, full_path):
1497
if full_path is not None:
1499
stat_result = os.lstat(full_path)
1501
if e.errno != errno.ENOENT:
1350
1506
def full_path(entry, tree):
1351
return pathjoin(tree.basedir, entry.path)
1507
return os.path.join(tree.root, entry.path)
1354
1509
def new_delete_entry(entry, tree, inventory, delete):
1355
1510
if entry.path == "":