13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Represent and apply a changeset.
19
Conflicts in applying a changeset are represented as exceptions.
21
This only handles the in-memory objects representing changesets, which are
22
primarily used by the merge code.
28
from shutil import rmtree
29
from itertools import izip
31
from bzrlib.trace import mutter, warning
32
from bzrlib.osutils import rename, sha_file, pathjoin, mkdtemp
34
from bzrlib.errors import BzrCheckError
20
from bzrlib.trace import mutter
22
Represent and apply a changeset
36
24
__docformat__ = "restructuredtext"
42
28
class OldFailedTreeOp(Exception):
43
29
def __init__(self):
44
30
Exception.__init__(self, "bzr-tree-change contains files from a"
45
31
" previous failed merge operation.")
48
32
def invert_dict(dict):
50
34
for (key,value) in dict.iteritems():
51
35
newdict[value] = key
55
class ChangeExecFlag(object):
39
class PatchApply(object):
40
"""Patch application as a kind of content change"""
41
def __init__(self, contents):
44
:param contents: The text of the patch to apply
45
:type contents: str"""
46
self.contents = contents
48
def __eq__(self, other):
49
if not isinstance(other, PatchApply):
51
elif self.contents != other.contents:
56
def __ne__(self, other):
57
return not (self == other)
59
def apply(self, filename, conflict_handler, reverse=False):
60
"""Applies the patch to the specified file.
62
:param filename: the file to apply the patch to
64
:param reverse: If true, apply the patch in reverse
67
input_name = filename+".orig"
69
os.rename(filename, input_name)
71
if e.errno != errno.ENOENT:
73
if conflict_handler.patch_target_missing(filename, self.contents)\
76
os.rename(filename, input_name)
79
status = patch.patch(self.contents, input_name, filename,
81
os.chmod(filename, os.stat(input_name).st_mode)
85
conflict_handler.failed_hunks(filename)
88
class ChangeUnixPermissions(object):
56
89
"""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
91
def __init__(self, old_mode, new_mode):
92
self.old_mode = old_mode
93
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
95
def apply(self, filename, conflict_handler, reverse=False):
97
from_mode = self.old_mode
98
to_mode = self.new_mode
100
from_mode = self.new_mode
101
to_mode = self.old_mode
66
current_exec_flag = bool(os.stat(filename).st_mode & 0111)
103
current_mode = os.stat(filename).st_mode &0777
67
104
except OSError, e:
68
105
if e.errno == errno.ENOENT:
69
if conflict_handler.missing_for_exec_flag(filename) == "skip":
106
if conflict_handler.missing_for_chmod(filename) == "skip":
72
current_exec_flag = from_exec_flag
109
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":
111
if from_mode is not None and current_mode != from_mode:
112
if conflict_handler.wrong_old_perms(filename, from_mode,
113
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
116
if to_mode is not None:
93
118
os.chmod(filename, to_mode)
94
119
except IOError, e:
95
120
if e.errno == errno.ENOENT:
96
conflict_handler.missing_for_exec_flag(filename)
121
conflict_handler.missing_for_chmod(filename)
98
123
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)
124
if not isinstance(other, ChangeUnixPermissions):
126
elif self.old_mode != other.old_mode:
128
elif self.new_mode != other.new_mode:
103
133
def __ne__(self, other):
104
134
return not (self == other)
107
def dir_create(filename, conflict_handler, reverse=False):
136
def dir_create(filename, conflict_handler, reverse):
108
137
"""Creates the directory, or deletes it if reverse is true. Intended to be
109
138
used with ReplaceContents.
235
262
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":
267
def reversed(sequence):
268
max = len(sequence) - 1
269
for i in range(len(sequence)):
270
yield sequence[max - i]
304
272
class ReplaceContents(object):
305
273
"""A contents-replacement framework. It allows a file/directory/symlink to
359
333
undo(filename, conflict_handler, reverse=True)
360
334
if perform is not None:
361
perform(filename, conflict_handler)
335
perform(filename, conflict_handler, reverse=False)
362
336
if mode is not None:
363
337
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
339
class ApplySequence(object):
340
def __init__(self, changes=None):
342
if changes is not None:
343
self.changes.extend(changes)
345
def __eq__(self, other):
346
if not isinstance(other, ApplySequence):
348
elif len(other.changes) != len(self.changes):
351
for i in range(len(self.changes)):
352
if self.changes[i] != other.changes[i]:
356
def __ne__(self, other):
357
return not (self == other)
360
def apply(self, filename, conflict_handler, reverse=False):
364
iter = reversed(self.changes)
366
change.apply(filename, conflict_handler, reverse)
372
369
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):
370
def __init__(self, base_file, other_file):
371
self.base_file = base_file
372
self.other_file = other_file
385
374
def __eq__(self, other):
386
375
if not isinstance(other, Diff3Merge):
388
return (self.base == other.base and
389
self.other == other.other and self.file_id == other.file_id)
377
return (self.base_file == other.base_file and
378
self.other_file == other.other_file)
391
380
def __ne__(self, other):
392
381
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,
383
def apply(self, filename, conflict_handler, reverse=False):
384
new_file = filename+".new"
386
base = self.base_file
387
other = self.other_file
389
base = self.other_file
390
other = self.base_file
391
status = patch.diff3(new_file, filename, base, other)
393
os.chmod(new_file, os.stat(filename).st_mode)
394
os.rename(new_file, filename)
398
conflict_handler.merge_conflict(new_file, filename, base, other)
706
def summarize_name(self):
660
def summarize_name(self, changeset, reverse=False):
707
661
"""Produce a one-line summary of the filename. Indicates renames as
708
662
old => new, indicates creation as None => new, indicates deletion as
665
:param changeset: The changeset to get paths from
666
:type changeset: `Changeset`
667
:param reverse: If true, reverse the names in the output
713
671
orig_path = self.get_cset_path(False)
714
672
mod_path = self.get_cset_path(True)
715
if orig_path and orig_path.startswith('./'):
673
if orig_path is not None:
716
674
orig_path = orig_path[2:]
717
if mod_path and mod_path.startswith('./'):
675
if mod_path is not None:
718
676
mod_path = mod_path[2:]
719
677
if orig_path == mod_path:
722
return "%s => %s" % (orig_path, mod_path)
724
def get_new_path(self, id_map, changeset):
681
return "%s => %s" % (orig_path, mod_path)
683
return "%s => %s" % (mod_path, orig_path)
686
def get_new_path(self, id_map, changeset, reverse=False):
725
687
"""Determine the full pathname to rename to
727
689
:param id_map: The map of ids to filenames for the tree
728
690
:type id_map: Dictionary
729
691
:param changeset: The changeset to get data from
730
692
:type changeset: `Changeset`
693
: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
697
mutter("Finding new path for %s" % self.summarize_name(changeset))
701
from_dir = self.new_dir
703
from_name = self.new_name
705
parent = self.new_parent
706
to_dir = self.new_dir
708
to_name = self.new_name
709
from_name = self.name
740
711
if to_name is None:
743
714
if parent == NULL_ID or parent is None:
745
716
raise SourceRootHasName(self, to_name)
748
parent_entry = changeset.entries.get(parent)
749
if parent_entry is None:
719
if from_dir == to_dir:
750
720
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)
722
mutter("path, new_path: %r %r" % (self.path, self.new_path))
723
parent_entry = changeset.entries[parent]
724
dir = parent_entry.get_new_path(id_map, changeset, reverse)
754
725
if from_name == to_name:
755
726
name = os.path.basename(id_map[self.id])
758
729
assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
759
return pathjoin(dir, name)
730
return os.path.join(dir, name)
761
732
def is_boring(self):
762
733
"""Determines whether the entry does nothing
856
843
:type inventory: Dictionary
857
844
:param dir: The directory to apply changes to
846
:param reverse: Apply changes in reverse
859
848
:return: a mapping of id to temporary name
860
849
:rtype: Dictionary
863
852
for i in range(len(source_entries)):
864
853
entry = source_entries[i]
865
if entry.is_deletion():
866
path = pathjoin(dir, inventory[entry.id])
867
entry.apply(path, conflict_handler)
854
if entry.is_deletion(reverse):
855
path = os.path.join(dir, inventory[entry.id])
856
entry.apply(path, conflict_handler, reverse)
868
857
temp_name[entry.id] = None
870
elif entry.needs_rename():
871
if entry.is_creation():
873
to_name = pathjoin(temp_dir, str(i))
860
to_name = os.path.join(temp_dir, str(i))
874
861
src_path = inventory.get(entry.id)
875
862
if src_path is not None:
876
src_path = pathjoin(dir, src_path)
863
src_path = os.path.join(dir, src_path)
878
rename(src_path, to_name)
865
os.rename(src_path, to_name)
879
866
temp_name[entry.id] = to_name
880
867
except OSError, e:
881
868
if e.errno != errno.ENOENT:
883
if conflict_handler.missing_for_rename(src_path, to_name) \
870
if conflict_handler.missing_for_rename(src_path) == "skip":
890
876
def rename_to_new_create(changed_inventory, target_entries, inventory,
891
changeset, dir, conflict_handler):
877
changeset, dir, conflict_handler, reverse):
892
878
"""Rename entries with temp names to their final names, create new files.
894
880
:param changed_inventory: A mapping of id to temporary name
899
885
:type changeset: `Changeset`
900
886
:param dir: The directory to apply changes to
888
:param reverse: If true, apply changes in reverse
903
891
for entry in target_entries:
904
new_tree_path = entry.get_new_path(inventory, changeset)
892
new_tree_path = entry.get_new_path(inventory, changeset, reverse)
905
893
if new_tree_path is None:
907
new_path = pathjoin(dir, new_tree_path)
895
new_path = os.path.join(dir, new_tree_path)
908
896
old_path = changed_inventory.get(entry.id)
909
if bzrlib.osutils.lexists(new_path):
897
if os.path.exists(new_path):
910
898
if conflict_handler.target_exists(entry, new_path, old_path) == \
913
if entry.is_creation():
914
entry.apply(new_path, conflict_handler)
901
if entry.is_creation(reverse):
902
entry.apply(new_path, conflict_handler, reverse)
915
903
changed_inventory[entry.id] = new_tree_path
916
elif entry.needs_rename():
917
if entry.is_deletion():
919
905
if old_path is None:
922
mutter('rename %s to final name %s', old_path, new_path)
923
rename(old_path, new_path)
908
os.rename(old_path, new_path)
924
909
changed_inventory[entry.id] = new_tree_path
925
910
except OSError, e:
926
raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
927
% (old_path, new_path, entry, e))
911
raise Exception ("%s is missing" % new_path)
930
913
class TargetExists(Exception):
931
914
def __init__(self, entry, target):
1074
1047
def rename_conflict(self, id, this_name, base_name, other_name):
1075
1048
raise RenameConflict(id, this_name, base_name, other_name)
1077
def move_conflict(self, id, this_dir, base_dir, other_dir):
1050
def move_conflict(self, id, inventory):
1051
this_dir = inventory.this.get_dir(id)
1052
base_dir = inventory.base.get_dir(id)
1053
other_dir = inventory.other.get_dir(id)
1078
1054
raise MoveConflict(id, this_dir, base_dir, other_dir)
1080
def merge_conflict(self, new_file, this_path, base_lines, other_lines):
1056
def merge_conflict(self, new_file, this_path, base_path, other_path):
1081
1057
os.unlink(new_file)
1082
1058
raise MergeConflict(this_path)
1060
def permission_conflict(self, this_path, base_path, other_path):
1061
raise MergePermissionConflict(this_path, base_path, other_path)
1084
1063
def wrong_old_contents(self, filename, expected_contents):
1085
1064
raise WrongOldContents(filename)
1087
1066
def rem_contents_conflict(self, filename, this_contents, base_contents):
1088
1067
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)
1069
def wrong_old_perms(self, filename, old_perms, new_perms):
1070
raise WrongOldPermissions(filename, old_perms, new_perms)
1093
1072
def rmdir_non_empty(self, filename):
1094
1073
raise DeletingNonEmptyDirectory(filename)
1099
1078
def patch_target_missing(self, filename, contents):
1100
1079
raise PatchTargetMissing(filename)
1102
def missing_for_exec_flag(self, filename):
1103
raise MissingForExecFlag(filename)
1081
def missing_for_chmod(self, filename):
1082
raise MissingPermsFile(filename)
1105
1084
def missing_for_rm(self, filename, change):
1106
1085
raise MissingForRm(filename)
1108
def missing_for_rename(self, filename, to_path):
1109
raise MissingForRename(filename, to_path)
1087
def missing_for_rename(self, filename):
1088
raise MissingForRename(filename)
1111
def missing_for_merge(self, file_id, other_path):
1112
raise MissingForMerge(other_path)
1090
def missing_for_merge(self, file_id, inventory):
1091
raise MissingForMerge(inventory.other.get_path(file_id))
1114
1093
def new_contents_conflict(self, filename, other_contents):
1115
1094
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):
1099
def apply_changeset(changeset, inventory, dir, conflict_handler=None,
1129
1101
"""Apply a changeset to a directory.
1131
1103
:param changeset: The changes to perform
1156
1130
#apply changes that don't affect filenames
1157
1131
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)
1132
if not entry.is_creation_or_deletion():
1133
path = os.path.join(dir, inventory[entry.id])
1134
entry.apply(path, conflict_handler, reverse)
1166
1136
# Apply renames in stages, to minimize conflicts:
1167
1137
# Only files whose name or parent change are interesting, because their
1168
1138
# target name may exist in the source tree. If a directory's name changes,
1169
1139
# that doesn't make its children interesting.
1170
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1140
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1172
1143
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1173
temp_dir, conflict_handler)
1144
temp_dir, conflict_handler,
1175
1147
rename_to_new_create(changed_inventory, target_entries, inventory,
1176
changeset, dir, conflict_handler)
1148
changeset, dir, conflict_handler, reverse)
1177
1149
os.rmdir(temp_dir)
1178
1150
return changed_inventory
1181
def apply_changeset_tree(cset, tree):
1153
def apply_changeset_tree(cset, tree, reverse=False):
1182
1154
r_inventory = {}
1183
1155
for entry in tree.source_inventory().itervalues():
1184
1156
inventory[entry.id] = entry.path
1185
new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1157
new_inventory = apply_changeset(cset, r_inventory, tree.root,
1186
1159
new_entries, remove_entries = \
1187
get_inventory_change(inventory, new_inventory, cset)
1160
get_inventory_change(inventory, new_inventory, cset, reverse)
1188
1161
tree.update_source_inventory(new_entries, remove_entries)
1191
def get_inventory_change(inventory, new_inventory, cset):
1164
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1192
1165
new_entries = {}
1193
1166
remove_entries = []
1194
1167
for entry in cset.entries.itervalues():
1214
1187
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." \
1189
class CompositionFailure(Exception):
1190
def __init__(self, old_entry, new_entry, problem):
1191
msg = "Unable to conpose entries.\n %s" % problem
1192
Exception.__init__(self, msg)
1194
class IDMismatch(CompositionFailure):
1195
def __init__(self, old_entry, new_entry):
1196
problem = "Attempt to compose entries with different ids: %s and %s" %\
1197
(old_entry.id, new_entry.id)
1198
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1200
def compose_changesets(old_cset, new_cset):
1201
"""Combine two changesets into one. This works well for exact patching.
1202
Otherwise, not so well.
1204
:param old_cset: The first changeset that would be applied
1205
:type old_cset: `Changeset`
1206
:param new_cset: The second changeset that would be applied
1207
:type new_cset: `Changeset`
1208
:return: A changeset that combines the changes in both changesets
1211
composed = Changeset()
1212
for old_entry in old_cset.entries.itervalues():
1213
new_entry = new_cset.entries.get(old_entry.id)
1214
if new_entry is None:
1215
composed.add_entry(old_entry)
1217
composed_entry = compose_entries(old_entry, new_entry)
1218
if composed_entry.parent is not None or\
1219
composed_entry.new_parent is not None:
1220
composed.add_entry(composed_entry)
1221
for new_entry in new_cset.entries.itervalues():
1222
if not old_cset.entries.has_key(new_entry.id):
1223
composed.add_entry(new_entry)
1226
def compose_entries(old_entry, new_entry):
1227
"""Combine two entries into one.
1229
:param old_entry: The first entry that would be applied
1230
:type old_entry: ChangesetEntry
1231
:param old_entry: The second entry that would be applied
1232
:type old_entry: ChangesetEntry
1233
:return: A changeset entry combining both entries
1234
:rtype: `ChangesetEntry`
1236
if old_entry.id != new_entry.id:
1237
raise IDMismatch(old_entry, new_entry)
1238
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1240
if (old_entry.parent != old_entry.new_parent or
1241
new_entry.parent != new_entry.new_parent):
1242
output.new_parent = new_entry.new_parent
1244
if (old_entry.path != old_entry.new_path or
1245
new_entry.path != new_entry.new_path):
1246
output.new_path = new_entry.new_path
1248
output.contents_change = compose_contents(old_entry, new_entry)
1249
output.metadata_change = compose_metadata(old_entry, new_entry)
1252
def compose_contents(old_entry, new_entry):
1253
"""Combine the contents of two changeset entries. Entries are combined
1254
intelligently where possible, but the fallback behavior returns an
1257
:param old_entry: The first entry that would be applied
1258
:type old_entry: `ChangesetEntry`
1259
:param new_entry: The second entry that would be applied
1260
:type new_entry: `ChangesetEntry`
1261
:return: A combined contents change
1262
:rtype: anything supporting the apply(reverse=False) method
1264
old_contents = old_entry.contents_change
1265
new_contents = new_entry.contents_change
1266
if old_entry.contents_change is None:
1267
return new_entry.contents_change
1268
elif new_entry.contents_change is None:
1269
return old_entry.contents_change
1270
elif isinstance(old_contents, ReplaceContents) and \
1271
isinstance(new_contents, ReplaceContents):
1272
if old_contents.old_contents == new_contents.new_contents:
1275
return ReplaceContents(old_contents.old_contents,
1276
new_contents.new_contents)
1277
elif isinstance(old_contents, ApplySequence):
1278
output = ApplySequence(old_contents.changes)
1279
if isinstance(new_contents, ApplySequence):
1280
output.changes.extend(new_contents.changes)
1282
output.changes.append(new_contents)
1284
elif isinstance(new_contents, ApplySequence):
1285
output = ApplySequence((old_contents.changes,))
1286
output.extend(new_contents.changes)
1289
return ApplySequence((old_contents, new_contents))
1291
def compose_metadata(old_entry, new_entry):
1292
old_meta = old_entry.metadata_change
1293
new_meta = new_entry.metadata_change
1294
if old_meta is None:
1296
elif new_meta is None:
1298
elif isinstance(old_meta, ChangeUnixPermissions) and \
1299
isinstance(new_meta, ChangeUnixPermissions):
1300
return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1302
return ApplySequence(old_meta, new_meta)
1305
def changeset_is_null(changeset):
1306
for entry in changeset.entries.itervalues():
1307
if not entry.is_boring():
1311
class UnsuppportedFiletype(Exception):
1312
def __init__(self, full_path, stat_result):
1313
msg = "The file \"%s\" is not a supported filetype." % full_path
1221
1314
Exception.__init__(self, msg)
1222
1315
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)()
1316
self.stat_result = stat_result
1318
def generate_changeset(tree_a, tree_b, inventory_a=None, inventory_b=None):
1319
return ChangesetGenerator(tree_a, tree_b, inventory_a, inventory_b)()
1230
1321
class ChangesetGenerator(object):
1231
def __init__(self, tree_a, tree_b, interesting_ids=None):
1322
def __init__(self, tree_a, tree_b, inventory_a=None, inventory_b=None):
1232
1323
object.__init__(self)
1233
1324
self.tree_a = tree_a
1234
1325
self.tree_b = tree_b
1235
self._interesting_ids = interesting_ids
1326
if inventory_a is not None:
1327
self.inventory_a = inventory_a
1329
self.inventory_a = tree_a.inventory()
1330
if inventory_b is not None:
1331
self.inventory_b = inventory_b
1333
self.inventory_b = tree_b.inventory()
1334
self.r_inventory_a = self.reverse_inventory(self.inventory_a)
1335
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:
1337
def reverse_inventory(self, inventory):
1339
for entry in inventory.itervalues():
1340
if entry.id is None:
1342
r_inventory[entry.id] = entry
1244
1345
def __call__(self):
1245
1346
cset = Changeset()
1246
for file_id in self.iter_both_tree_ids():
1247
cs_entry = self.make_entry(file_id)
1347
for entry in self.inventory_a.itervalues():
1348
if entry.id is None:
1350
cs_entry = self.make_entry(entry.id)
1248
1351
if cs_entry is not None and not cs_entry.is_boring():
1249
1352
cset.add_entry(cs_entry)
1354
for entry in self.inventory_b.itervalues():
1355
if entry.id is None:
1357
if not self.r_inventory_a.has_key(entry.id):
1358
cs_entry = self.make_entry(entry.id)
1359
if cs_entry is not None and not cs_entry.is_boring():
1360
cset.add_entry(cs_entry)
1251
1361
for entry in list(cset.entries.itervalues()):
1252
1362
if entry.parent != entry.new_parent:
1253
1363
if not cset.entries.has_key(entry.parent) and\
1261
1371
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)
1374
def get_entry_parent(self, entry, inventory):
1377
if entry.path == "./.":
1379
dirname = os.path.dirname(entry.path)
1382
parent = inventory[dirname]
1385
def get_paths(self, entry, tree):
1388
full_path = tree.readonly_path(entry.id)
1389
if entry.path == ".":
1390
return ("", full_path)
1391
return (entry.path, full_path)
1393
def make_basic_entry(self, id, only_interesting):
1394
entry_a = self.r_inventory_a.get(id)
1395
entry_b = self.r_inventory_b.get(id)
1290
1396
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)
1397
return (None, None, None)
1398
parent = self.get_entry_parent(entry_a, self.inventory_a)
1399
(path, full_path_a) = self.get_paths(entry_a, self.tree_a)
1400
cs_entry = ChangesetEntry(id, parent, path)
1401
new_parent = self.get_entry_parent(entry_b, self.inventory_b)
1404
(new_path, full_path_b) = self.get_paths(entry_b, self.tree_b)
1299
1406
cs_entry.new_path = new_path
1300
1407
cs_entry.new_parent = new_parent
1408
return (cs_entry, full_path_a, full_path_b)
1303
1410
def is_interesting(self, entry_a, entry_b):
1304
if self._interesting_ids is None:
1306
1411
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
1412
if entry_a.interesting:
1414
if entry_b is not None:
1415
if entry_b.interesting:
1314
1419
def make_boring_entry(self, id):
1315
cs_entry = self.make_basic_entry(id, only_interesting=False)
1420
(cs_entry, full_path_a, full_path_b) = \
1421
self.make_basic_entry(id, only_interesting=False)
1316
1422
if cs_entry.is_creation_or_deletion():
1317
1423
return self.make_entry(id, only_interesting=False)
1319
1425
return cs_entry
1321
1428
def make_entry(self, id, only_interesting=True):
1322
cs_entry = self.make_basic_entry(id, only_interesting)
1429
(cs_entry, full_path_a, full_path_b) = \
1430
self.make_basic_entry(id, only_interesting)
1324
1432
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)
1435
stat_a = self.lstat(full_path_a)
1436
stat_b = self.lstat(full_path_b)
1438
cs_entry.new_parent = None
1439
cs_entry.new_path = None
1441
cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1442
cs_entry.contents_change = self.make_contents_change(full_path_a,
1336
1446
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)
1448
def make_mode_change(self, stat_a, stat_b):
1450
if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1451
mode_a = stat_a.st_mode & 0777
1453
if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1454
mode_b = stat_b.st_mode & 0777
1455
if mode_a == mode_b:
1457
return ChangeUnixPermissions(mode_a, mode_b)
1459
def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1460
if stat_a is None and stat_b is None:
1462
if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1463
stat.S_ISDIR(stat_b.st_mode):
1465
if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1466
stat.S_ISREG(stat_b.st_mode):
1467
if stat_a.st_ino == stat_b.st_ino and \
1468
stat_a.st_dev == stat_b.st_dev:
1470
if file(full_path_a, "rb").read() == \
1471
file(full_path_b, "rb").read():
1474
patch_contents = patch.diff(full_path_a,
1475
file(full_path_b, "rb").read())
1476
if patch_contents is None:
1478
return PatchApply(patch_contents)
1480
a_contents = self.get_contents(stat_a, full_path_a)
1481
b_contents = self.get_contents(stat_b, full_path_b)
1353
1482
if a_contents == b_contents:
1355
1484
return ReplaceContents(a_contents, b_contents)
1486
def get_contents(self, stat_result, full_path):
1487
if stat_result is None:
1489
elif stat.S_ISREG(stat_result.st_mode):
1490
return FileCreate(file(full_path, "rb").read())
1491
elif stat.S_ISDIR(stat_result.st_mode):
1493
elif stat.S_ISLNK(stat_result.st_mode):
1494
return SymlinkCreate(os.readlink(full_path))
1496
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))
1498
def lstat(self, full_path):
1500
if full_path is not None:
1502
stat_result = os.lstat(full_path)
1504
if e.errno != errno.ENOENT:
1373
1509
def full_path(entry, tree):
1374
return pathjoin(tree.basedir, entry.path)
1510
return os.path.join(tree.root, entry.path)
1377
1512
def new_delete_entry(entry, tree, inventory, delete):
1378
1513
if entry.path == "":