61
63
self.rename_count = rename_count
64
class TreeTransform(object):
65
"""Represent a tree transformation.
67
This object is designed to support incremental generation of the transform,
70
However, it gives optimum performance when parent directories are created
71
before their contents. The transform is then able to put child files
72
directly in their parent directory, avoiding later renames.
74
It is easy to produce malformed transforms, but they are generally
75
harmless. Attempting to apply a malformed transform will cause an
76
exception to be raised before any modifications are made to the tree.
78
Many kinds of malformed transforms can be corrected with the
79
resolve_conflicts function. The remaining ones indicate programming error,
80
such as trying to create a file with no path.
82
Two sets of file creation methods are supplied. Convenience methods are:
87
These are composed of the low-level methods:
89
* create_file or create_directory or create_symlink
93
def __init__(self, tree, pb=DummyProgress()):
94
"""Note: a tree_write lock is taken on the tree.
96
Use TreeTransform.finalize() to release the lock (can be omitted if
97
TreeTransform.apply() called).
66
class TreeTransformBase(object):
67
"""The base class for TreeTransform and TreeTransformBase"""
69
def __init__(self, tree, limbodir, pb=DummyProgress(),
73
:param tree: The tree that will be transformed, but not necessarily
75
:param limbodir: A directory where new files can be stored until
76
they are installed in their proper places
77
:param pb: A ProgressBar indicating how much progress is being made
78
:param case_sensitive: If True, the target of the transform is
79
case sensitive, not just case preserving.
99
81
object.__init__(self)
101
self._tree.lock_tree_write()
103
control_files = self._tree._control_files
104
self._limbodir = urlutils.local_path_from_url(
105
control_files.controlfilename('limbo'))
107
os.mkdir(self._limbodir)
109
if e.errno == errno.EEXIST:
110
raise ExistingLimbo(self._limbodir)
111
self._deletiondir = urlutils.local_path_from_url(
112
control_files.controlfilename('pending-deletion'))
114
os.mkdir(self._deletiondir)
116
if e.errno == errno.EEXIST:
117
raise errors.ExistingPendingDeletion(self._deletiondir)
83
self._limbodir = limbodir
84
self._deletiondir = None
123
85
self._id_number = 0
86
# mapping of trans_id -> new basename
124
87
self._new_name = {}
88
# mapping of trans_id -> new parent trans_id
125
89
self._new_parent = {}
90
# mapping of trans_id with new contents -> new file_kind
126
91
self._new_contents = {}
127
92
# A mapping of transform ids to their limbo filename
128
93
self._limbo_files = {}
876
838
self._limbo_files[trans_id] = limbo_name
877
839
return limbo_name
879
def _apply_removals(self, inv, inventory_delta, mover):
880
"""Perform tree operations that remove directory/inventory names.
882
That is, delete files that are to be deleted, and put any files that
883
need renaming into limbo. This must be done in strict child-to-parent
886
tree_paths = list(self._tree_path_ids.iteritems())
887
tree_paths.sort(reverse=True)
888
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
890
for num, data in enumerate(tree_paths):
891
path, trans_id = data
892
child_pb.update('removing file', num, len(tree_paths))
893
full_path = self._tree.abspath(path)
894
if trans_id in self._removed_contents:
895
mover.pre_delete(full_path, os.path.join(self._deletiondir,
897
elif trans_id in self._new_name or trans_id in \
900
mover.rename(full_path, self._limbo_name(trans_id))
902
if e.errno != errno.ENOENT:
905
self.rename_count += 1
906
if trans_id in self._removed_id:
907
if trans_id == self._new_root:
908
file_id = self._tree.get_root_id()
910
file_id = self.tree_file_id(trans_id)
911
assert file_id is not None
912
inventory_delta.append((path, None, file_id, None))
916
def _apply_insertions(self, inv, inventory_delta, mover):
917
"""Perform tree operations that insert directory/inventory names.
919
That is, create any files that need to be created, and restore from
920
limbo any files that needed renaming. This must be done in strict
921
parent-to-child order.
923
new_paths = self.new_paths()
925
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
927
for num, (path, trans_id) in enumerate(new_paths):
929
child_pb.update('adding file', num, len(new_paths))
931
kind = self._new_contents[trans_id]
933
kind = contents = None
934
if trans_id in self._new_contents or \
935
self.path_changed(trans_id):
936
full_path = self._tree.abspath(path)
937
if trans_id in self._needs_rename:
939
mover.rename(self._limbo_name(trans_id), full_path)
941
# We may be renaming a dangling inventory id
942
if e.errno != errno.ENOENT:
945
self.rename_count += 1
946
if trans_id in self._new_contents:
947
modified_paths.append(full_path)
948
del self._new_contents[trans_id]
950
if trans_id in self._new_id:
952
kind = file_kind(self._tree.abspath(path))
953
if trans_id in self._new_reference_revision:
954
new_entry = inventory.TreeReference(
955
self._new_id[trans_id],
956
self._new_name[trans_id],
957
self.final_file_id(self._new_parent[trans_id]),
958
None, self._new_reference_revision[trans_id])
960
new_entry = inventory.make_entry(kind,
961
self.final_name(trans_id),
962
self.final_file_id(self.final_parent(trans_id)),
963
self._new_id[trans_id])
965
if trans_id in self._new_name or trans_id in\
967
trans_id in self._new_executability:
968
file_id = self.final_file_id(trans_id)
969
if file_id is not None:
971
new_entry = entry.copy()
973
if trans_id in self._new_name or trans_id in\
975
if new_entry is not None:
976
new_entry.name = self.final_name(trans_id)
977
parent = self.final_parent(trans_id)
978
parent_id = self.final_file_id(parent)
979
new_entry.parent_id = parent_id
981
if trans_id in self._new_executability:
982
self._set_executability(path, new_entry, trans_id)
983
if new_entry is not None:
984
if new_entry.file_id in inv:
985
old_path = inv.id2path(new_entry.file_id)
988
inventory_delta.append((old_path, path,
993
return modified_paths
995
841
def _set_executability(self, path, entry, trans_id):
996
842
"""Set the executability of versioned files """
997
843
new_executability = self._new_executability[trans_id]
1201
1047
(from_executable, to_executable)))
1202
1048
return iter(sorted(results, key=lambda x:x[1]))
1050
def get_preview_tree(self):
1051
"""Return a tree representing the result of the transform.
1053
This tree only supports the subset of Tree functionality required
1054
by show_diff_trees. It must only be compared to tt._tree.
1056
return _PreviewTree(self)
1059
class TreeTransform(TreeTransformBase):
1060
"""Represent a tree transformation.
1062
This object is designed to support incremental generation of the transform,
1065
However, it gives optimum performance when parent directories are created
1066
before their contents. The transform is then able to put child files
1067
directly in their parent directory, avoiding later renames.
1069
It is easy to produce malformed transforms, but they are generally
1070
harmless. Attempting to apply a malformed transform will cause an
1071
exception to be raised before any modifications are made to the tree.
1073
Many kinds of malformed transforms can be corrected with the
1074
resolve_conflicts function. The remaining ones indicate programming error,
1075
such as trying to create a file with no path.
1077
Two sets of file creation methods are supplied. Convenience methods are:
1082
These are composed of the low-level methods:
1084
* create_file or create_directory or create_symlink
1088
Transform/Transaction ids
1089
-------------------------
1090
trans_ids are temporary ids assigned to all files involved in a transform.
1091
It's possible, even common, that not all files in the Tree have trans_ids.
1093
trans_ids are used because filenames and file_ids are not good enough
1094
identifiers; filenames change, and not all files have file_ids. File-ids
1095
are also associated with trans-ids, so that moving a file moves its
1098
trans_ids are only valid for the TreeTransform that generated them.
1102
Limbo is a temporary directory use to hold new versions of files.
1103
Files are added to limbo by create_file, create_directory, create_symlink,
1104
and their convenience variants (new_*). Files may be removed from limbo
1105
using cancel_creation. Files are renamed from limbo into their final
1106
location as part of TreeTransform.apply
1108
Limbo must be cleaned up, by either calling TreeTransform.apply or
1109
calling TreeTransform.finalize.
1111
Files are placed into limbo inside their parent directories, where
1112
possible. This reduces subsequent renames, and makes operations involving
1113
lots of files faster. This optimization is only possible if the parent
1114
directory is created *before* creating any of its children, so avoid
1115
creating children before parents, where possible.
1119
This temporary directory is used by _FileMover for storing files that are
1120
about to be deleted. In case of rollback, the files will be restored.
1121
FileMover does not delete files until it is sure that a rollback will not
1124
def __init__(self, tree, pb=DummyProgress()):
1125
"""Note: a tree_write lock is taken on the tree.
1127
Use TreeTransform.finalize() to release the lock (can be omitted if
1128
TreeTransform.apply() called).
1130
tree.lock_tree_write()
1133
control_files = tree._control_files
1134
limbodir = urlutils.local_path_from_url(
1135
control_files.controlfilename('limbo'))
1139
if e.errno == errno.EEXIST:
1140
raise ExistingLimbo(limbodir)
1141
deletiondir = urlutils.local_path_from_url(
1142
control_files.controlfilename('pending-deletion'))
1144
os.mkdir(deletiondir)
1146
if e.errno == errno.EEXIST:
1147
raise errors.ExistingPendingDeletion(deletiondir)
1152
TreeTransformBase.__init__(self, tree, limbodir, pb,
1153
tree.case_sensitive)
1154
self._deletiondir = deletiondir
1156
def apply(self, no_conflicts=False, _mover=None):
1157
"""Apply all changes to the inventory and filesystem.
1159
If filesystem or inventory conflicts are present, MalformedTransform
1162
If apply succeeds, finalize is not necessary.
1164
:param no_conflicts: if True, the caller guarantees there are no
1165
conflicts, so no check is made.
1166
:param _mover: Supply an alternate FileMover, for testing
1168
if not no_conflicts:
1169
conflicts = self.find_conflicts()
1170
if len(conflicts) != 0:
1171
raise MalformedTransform(conflicts=conflicts)
1172
inv = self._tree.inventory
1173
inventory_delta = []
1174
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1177
mover = _FileMover()
1181
child_pb.update('Apply phase', 0, 2)
1182
self._apply_removals(inv, inventory_delta, mover)
1183
child_pb.update('Apply phase', 1, 2)
1184
modified_paths = self._apply_insertions(inv, inventory_delta,
1190
mover.apply_deletions()
1193
self._tree.apply_inventory_delta(inventory_delta)
1196
return _TransformResults(modified_paths, self.rename_count)
1198
def _apply_removals(self, inv, inventory_delta, mover):
1199
"""Perform tree operations that remove directory/inventory names.
1201
That is, delete files that are to be deleted, and put any files that
1202
need renaming into limbo. This must be done in strict child-to-parent
1205
tree_paths = list(self._tree_path_ids.iteritems())
1206
tree_paths.sort(reverse=True)
1207
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1209
for num, data in enumerate(tree_paths):
1210
path, trans_id = data
1211
child_pb.update('removing file', num, len(tree_paths))
1212
full_path = self._tree.abspath(path)
1213
if trans_id in self._removed_contents:
1214
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1216
elif trans_id in self._new_name or trans_id in \
1219
mover.rename(full_path, self._limbo_name(trans_id))
1221
if e.errno != errno.ENOENT:
1224
self.rename_count += 1
1225
if trans_id in self._removed_id:
1226
if trans_id == self._new_root:
1227
file_id = self._tree.get_root_id()
1229
file_id = self.tree_file_id(trans_id)
1230
if file_id is not None:
1231
inventory_delta.append((path, None, file_id, None))
1235
def _apply_insertions(self, inv, inventory_delta, mover):
1236
"""Perform tree operations that insert directory/inventory names.
1238
That is, create any files that need to be created, and restore from
1239
limbo any files that needed renaming. This must be done in strict
1240
parent-to-child order.
1242
new_paths = self.new_paths()
1244
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1247
for num, (path, trans_id) in enumerate(new_paths):
1249
child_pb.update('adding file', num, len(new_paths))
1251
kind = self._new_contents[trans_id]
1253
kind = contents = None
1254
if trans_id in self._new_contents or \
1255
self.path_changed(trans_id):
1256
full_path = self._tree.abspath(path)
1257
if trans_id in self._needs_rename:
1259
mover.rename(self._limbo_name(trans_id), full_path)
1261
# We may be renaming a dangling inventory id
1262
if e.errno != errno.ENOENT:
1265
self.rename_count += 1
1266
if trans_id in self._new_contents:
1267
modified_paths.append(full_path)
1268
completed_new.append(trans_id)
1270
if trans_id in self._new_id:
1272
kind = file_kind(self._tree.abspath(path))
1273
if trans_id in self._new_reference_revision:
1274
new_entry = inventory.TreeReference(
1275
self._new_id[trans_id],
1276
self._new_name[trans_id],
1277
self.final_file_id(self._new_parent[trans_id]),
1278
None, self._new_reference_revision[trans_id])
1280
new_entry = inventory.make_entry(kind,
1281
self.final_name(trans_id),
1282
self.final_file_id(self.final_parent(trans_id)),
1283
self._new_id[trans_id])
1285
if trans_id in self._new_name or trans_id in\
1286
self._new_parent or\
1287
trans_id in self._new_executability:
1288
file_id = self.final_file_id(trans_id)
1289
if file_id is not None:
1290
entry = inv[file_id]
1291
new_entry = entry.copy()
1293
if trans_id in self._new_name or trans_id in\
1295
if new_entry is not None:
1296
new_entry.name = self.final_name(trans_id)
1297
parent = self.final_parent(trans_id)
1298
parent_id = self.final_file_id(parent)
1299
new_entry.parent_id = parent_id
1301
if trans_id in self._new_executability:
1302
self._set_executability(path, new_entry, trans_id)
1303
if new_entry is not None:
1304
if new_entry.file_id in inv:
1305
old_path = inv.id2path(new_entry.file_id)
1308
inventory_delta.append((old_path, path,
1313
for trans_id in completed_new:
1314
del self._new_contents[trans_id]
1315
return modified_paths
1318
class TransformPreview(TreeTransformBase):
1319
"""A TreeTransform for generating preview trees.
1321
Unlike TreeTransform, this version works when the input tree is a
1322
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1323
unversioned files in the input tree.
1326
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1328
limbodir = tempfile.mkdtemp(prefix='bzr-limbo-')
1329
TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1331
def canonical_path(self, path):
1334
def tree_kind(self, trans_id):
1335
path = self._tree_id_paths.get(trans_id)
1337
raise NoSuchFile(None)
1338
file_id = self._tree.path2id(path)
1339
return self._tree.kind(file_id)
1341
def _set_mode(self, trans_id, mode_id, typefunc):
1342
"""Set the mode of new file contents.
1343
The mode_id is the existing file to get the mode from (often the same
1344
as trans_id). The operation is only performed if there's a mode match
1345
according to typefunc.
1347
# is it ok to ignore this? probably
1350
def iter_tree_children(self, parent_id):
1351
"""Iterate through the entry's tree children, if any"""
1353
path = self._tree_id_paths[parent_id]
1356
file_id = self.tree_file_id(parent_id)
1357
for child in self._tree.inventory[file_id].children.iterkeys():
1358
childpath = joinpath(path, child)
1359
yield self.trans_id_tree_path(childpath)
1362
class _PreviewTree(object):
1363
"""Partial implementation of Tree to support show_diff_trees"""
1365
def __init__(self, transform):
1366
self._transform = transform
1368
def lock_read(self):
1369
# Perhaps in theory, this should lock the TreeTransform?
1375
def _iter_changes(self, from_tree, include_unchanged=False,
1376
specific_files=None, pb=None, extra_trees=None,
1377
require_versioned=True, want_unversioned=False):
1378
"""See InterTree._iter_changes.
1380
This implementation does not support include_unchanged, specific_files,
1381
or want_unversioned. extra_trees, require_versioned, and pb are
1384
if from_tree is not self._transform._tree:
1385
raise ValueError('from_tree must be transform source tree.')
1386
if include_unchanged:
1387
raise ValueError('include_unchanged is not supported')
1388
if specific_files is not None:
1389
raise ValueError('specific_files is not supported')
1390
if want_unversioned:
1391
raise ValueError('want_unversioned is not supported')
1392
return self._transform._iter_changes()
1394
def kind(self, file_id):
1395
trans_id = self._transform.trans_id_file_id(file_id)
1396
return self._transform.final_kind(trans_id)
1398
def get_file_mtime(self, file_id, path=None):
1399
"""See Tree.get_file_mtime"""
1400
trans_id = self._transform.trans_id_file_id(file_id)
1401
name = self._transform._limbo_name(trans_id)
1402
return os.stat(name).st_mtime
1404
def get_file(self, file_id):
1405
"""See Tree.get_file"""
1406
trans_id = self._transform.trans_id_file_id(file_id)
1407
name = self._transform._limbo_name(trans_id)
1408
return open(name, 'rb')
1410
def paths2ids(self, specific_files, trees=None, require_versioned=False):
1411
"""See Tree.paths2ids"""
1205
1415
def joinpath(parent, child):
1206
1416
"""Join tree-relative paths, handling the tree root specially"""