830
def apply(self, filename, conflict_handler, reverse=False):
762
def apply(self, filename, conflict_handler):
831
763
"""Applies the file content and/or metadata changes.
833
765
:param filename: the filename of the entry
834
766
:type filename: str
835
:param reverse: If true, apply the changes in reverse
838
if self.is_deletion(reverse) and self.metadata_change is not None:
839
self.metadata_change.apply(filename, conflict_handler, reverse)
768
if self.is_deletion() and self.metadata_change is not None:
769
self.metadata_change.apply(filename, conflict_handler)
840
770
if self.contents_change is not None:
841
self.contents_change.apply(filename, conflict_handler, reverse)
842
if not self.is_deletion(reverse) and self.metadata_change is not None:
843
self.metadata_change.apply(filename, conflict_handler, reverse)
771
self.contents_change.apply(filename, conflict_handler)
772
if not self.is_deletion() and self.metadata_change is not None:
773
self.metadata_change.apply(filename, conflict_handler)
845
775
class IDPresent(Exception):
846
776
def __init__(self, id):
860
790
raise IDPresent(entry.id)
861
791
self.entries[entry.id] = entry
863
def my_sort(sequence, key, reverse=False):
864
"""A sort function that supports supplying a key for comparison
866
:param sequence: The sequence to sort
867
:param key: A callable object that returns the values to be compared
868
:param reverse: If true, sort in reverse order
871
def cmp_by_key(entry_a, entry_b):
876
return cmp(key(entry_a), key(entry_b))
877
sequence.sort(cmp_by_key)
879
def get_rename_entries(changeset, inventory, reverse):
793
def get_rename_entries(changeset, inventory):
880
794
"""Return a list of entries that will be renamed. Entries are sorted from
881
795
longest to shortest source path and from shortest to longest target path.
902
my_sort(source_entries, longest_to_shortest, reverse=True)
814
source_entries.sort(None, longest_to_shortest, True)
904
816
target_entries = source_entries[:]
905
817
# These are done from shortest to longest path, to avoid creating a
906
818
# child before its parent has been created/renamed
907
819
def shortest_to_longest(entry):
908
path = entry.get_new_path(inventory, changeset, reverse)
820
path = entry.get_new_path(inventory, changeset)
913
my_sort(target_entries, shortest_to_longest)
825
target_entries.sort(None, shortest_to_longest)
914
826
return (source_entries, target_entries)
916
828
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir,
917
conflict_handler, reverse):
918
830
"""Delete and rename entries as appropriate. Entries are renamed to temp
919
831
names. A map of id -> temp name (or None, for deletions) is returned.
1224
1129
path = pathjoin(dir, inventory[entry.id])
1225
entry.apply(path, conflict_handler, reverse)
1130
entry.apply(path, conflict_handler)
1227
1132
# Apply renames in stages, to minimize conflicts:
1228
1133
# Only files whose name or parent change are interesting, because their
1229
1134
# target name may exist in the source tree. If a directory's name changes,
1230
1135
# that doesn't make its children interesting.
1231
(source_entries, target_entries) = get_rename_entries(changeset, inventory,
1136
(source_entries, target_entries) = get_rename_entries(changeset, inventory)
1234
1138
changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1235
temp_dir, conflict_handler,
1139
temp_dir, conflict_handler)
1238
1141
rename_to_new_create(changed_inventory, target_entries, inventory,
1239
changeset, dir, conflict_handler, reverse)
1142
changeset, dir, conflict_handler)
1240
1143
os.rmdir(temp_dir)
1241
1144
return changed_inventory
1244
def apply_changeset_tree(cset, tree, reverse=False):
1147
def apply_changeset_tree(cset, tree):
1245
1148
r_inventory = {}
1246
1149
for entry in tree.source_inventory().itervalues():
1247
1150
inventory[entry.id] = entry.path
1248
new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1151
new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1250
1152
new_entries, remove_entries = \
1251
get_inventory_change(inventory, new_inventory, cset, reverse)
1153
get_inventory_change(inventory, new_inventory, cset)
1252
1154
tree.update_source_inventory(new_entries, remove_entries)
1255
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1157
def get_inventory_change(inventory, new_inventory, cset):
1256
1158
new_entries = {}
1257
1159
remove_entries = []
1258
1160
for entry in cset.entries.itervalues():
1278
1180
print entry.summarize_name(cset)
1280
class CompositionFailure(Exception):
1281
def __init__(self, old_entry, new_entry, problem):
1282
msg = "Unable to conpose entries.\n %s" % problem
1283
Exception.__init__(self, msg)
1285
class IDMismatch(CompositionFailure):
1286
def __init__(self, old_entry, new_entry):
1287
problem = "Attempt to compose entries with different ids: %s and %s" %\
1288
(old_entry.id, new_entry.id)
1289
CompositionFailure.__init__(self, old_entry, new_entry, problem)
1291
def compose_changesets(old_cset, new_cset):
1292
"""Combine two changesets into one. This works well for exact patching.
1293
Otherwise, not so well.
1295
:param old_cset: The first changeset that would be applied
1296
:type old_cset: `Changeset`
1297
:param new_cset: The second changeset that would be applied
1298
:type new_cset: `Changeset`
1299
:return: A changeset that combines the changes in both changesets
1302
composed = Changeset()
1303
for old_entry in old_cset.entries.itervalues():
1304
new_entry = new_cset.entries.get(old_entry.id)
1305
if new_entry is None:
1306
composed.add_entry(old_entry)
1308
composed_entry = compose_entries(old_entry, new_entry)
1309
if composed_entry.parent is not None or\
1310
composed_entry.new_parent is not None:
1311
composed.add_entry(composed_entry)
1312
for new_entry in new_cset.entries.itervalues():
1313
if not old_cset.entries.has_key(new_entry.id):
1314
composed.add_entry(new_entry)
1317
def compose_entries(old_entry, new_entry):
1318
"""Combine two entries into one.
1320
:param old_entry: The first entry that would be applied
1321
:type old_entry: ChangesetEntry
1322
:param old_entry: The second entry that would be applied
1323
:type old_entry: ChangesetEntry
1324
:return: A changeset entry combining both entries
1325
:rtype: `ChangesetEntry`
1327
if old_entry.id != new_entry.id:
1328
raise IDMismatch(old_entry, new_entry)
1329
output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1331
if (old_entry.parent != old_entry.new_parent or
1332
new_entry.parent != new_entry.new_parent):
1333
output.new_parent = new_entry.new_parent
1335
if (old_entry.path != old_entry.new_path or
1336
new_entry.path != new_entry.new_path):
1337
output.new_path = new_entry.new_path
1339
output.contents_change = compose_contents(old_entry, new_entry)
1340
output.metadata_change = compose_metadata(old_entry, new_entry)
1343
def compose_contents(old_entry, new_entry):
1344
"""Combine the contents of two changeset entries. Entries are combined
1345
intelligently where possible, but the fallback behavior returns an
1348
:param old_entry: The first entry that would be applied
1349
:type old_entry: `ChangesetEntry`
1350
:param new_entry: The second entry that would be applied
1351
:type new_entry: `ChangesetEntry`
1352
:return: A combined contents change
1353
:rtype: anything supporting the apply(reverse=False) method
1355
old_contents = old_entry.contents_change
1356
new_contents = new_entry.contents_change
1357
if old_entry.contents_change is None:
1358
return new_entry.contents_change
1359
elif new_entry.contents_change is None:
1360
return old_entry.contents_change
1361
elif isinstance(old_contents, ReplaceContents) and \
1362
isinstance(new_contents, ReplaceContents):
1363
if old_contents.old_contents == new_contents.new_contents:
1366
return ReplaceContents(old_contents.old_contents,
1367
new_contents.new_contents)
1368
elif isinstance(old_contents, ApplySequence):
1369
output = ApplySequence(old_contents.changes)
1370
if isinstance(new_contents, ApplySequence):
1371
output.changes.extend(new_contents.changes)
1373
output.changes.append(new_contents)
1375
elif isinstance(new_contents, ApplySequence):
1376
output = ApplySequence((old_contents.changes,))
1377
output.extend(new_contents.changes)
1380
return ApplySequence((old_contents, new_contents))
1382
def compose_metadata(old_entry, new_entry):
1383
old_meta = old_entry.metadata_change
1384
new_meta = new_entry.metadata_change
1385
if old_meta is None:
1387
elif new_meta is None:
1389
elif (isinstance(old_meta, ChangeExecFlag) and
1390
isinstance(new_meta, ChangeExecFlag)):
1391
return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1393
return ApplySequence(old_meta, new_meta)
1396
def changeset_is_null(changeset):
1397
for entry in changeset.entries.itervalues():
1398
if not entry.is_boring():
1402
1182
class UnsupportedFiletype(Exception):
1403
1183
def __init__(self, kind, full_path):
1404
1184
msg = "The file \"%s\" is a %s, which is not a supported filetype." \