1201
856
(from_executable, to_executable)))
1202
857
return iter(sorted(results, key=lambda x:x[1]))
859
def get_preview_tree(self):
860
"""Return a tree representing the result of the transform.
862
The tree is a snapshot, and altering the TreeTransform will invalidate
865
return _PreviewTree(self)
867
def commit(self, branch, message, merge_parents=None, strict=False):
868
"""Commit the result of this TreeTransform to a branch.
870
:param branch: The branch to commit to.
871
:param message: The message to attach to the commit.
872
:param merge_parents: Additional parents specified by pending merges.
873
:return: The revision_id of the revision committed.
875
self._check_malformed()
877
unversioned = set(self._new_contents).difference(set(self._new_id))
878
for trans_id in unversioned:
879
if self.final_file_id(trans_id) is None:
880
raise errors.StrictCommitFailed()
882
revno, last_rev_id = branch.last_revision_info()
883
if last_rev_id == _mod_revision.NULL_REVISION:
884
if merge_parents is not None:
885
raise ValueError('Cannot supply merge parents for first'
889
parent_ids = [last_rev_id]
890
if merge_parents is not None:
891
parent_ids.extend(merge_parents)
892
if self._tree.get_revision_id() != last_rev_id:
893
raise ValueError('TreeTransform not based on branch basis: %s' %
894
self._tree.get_revision_id())
895
builder = branch.get_commit_builder(parent_ids)
896
preview = self.get_preview_tree()
897
list(builder.record_iter_changes(preview, last_rev_id,
898
self.iter_changes()))
899
builder.finish_inventory()
900
revision_id = builder.commit(message)
901
branch.set_last_revision_info(revno + 1, revision_id)
904
def _text_parent(self, trans_id):
905
file_id = self.tree_file_id(trans_id)
907
if file_id is None or self._tree.kind(file_id) != 'file':
909
except errors.NoSuchFile:
913
def _get_parents_texts(self, trans_id):
914
"""Get texts for compression parents of this file."""
915
file_id = self._text_parent(trans_id)
918
return (self._tree.get_file_text(file_id),)
920
def _get_parents_lines(self, trans_id):
921
"""Get lines for compression parents of this file."""
922
file_id = self._text_parent(trans_id)
925
return (self._tree.get_file_lines(file_id),)
927
def serialize(self, serializer):
928
"""Serialize this TreeTransform.
930
:param serializer: A Serialiser like pack.ContainerSerializer.
932
new_name = dict((k, v.encode('utf-8')) for k, v in
933
self._new_name.items())
934
new_executability = dict((k, int(v)) for k, v in
935
self._new_executability.items())
936
tree_path_ids = dict((k.encode('utf-8'), v)
937
for k, v in self._tree_path_ids.items())
939
'_id_number': self._id_number,
940
'_new_name': new_name,
941
'_new_parent': self._new_parent,
942
'_new_executability': new_executability,
943
'_new_id': self._new_id,
944
'_tree_path_ids': tree_path_ids,
945
'_removed_id': list(self._removed_id),
946
'_removed_contents': list(self._removed_contents),
947
'_non_present_ids': self._non_present_ids,
949
yield serializer.bytes_record(bencode.bencode(attribs),
951
for trans_id, kind in self._new_contents.items():
953
lines = osutils.chunks_to_lines(
954
self._read_file_chunks(trans_id))
955
parents = self._get_parents_lines(trans_id)
956
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
957
content = ''.join(mpdiff.to_patch())
958
if kind == 'directory':
960
if kind == 'symlink':
961
content = self._read_symlink_target(trans_id)
962
yield serializer.bytes_record(content, ((trans_id, kind),))
964
def deserialize(self, records):
965
"""Deserialize a stored TreeTransform.
967
:param records: An iterable of (names, content) tuples, as per
968
pack.ContainerPushParser.
970
names, content = records.next()
971
attribs = bencode.bdecode(content)
972
self._id_number = attribs['_id_number']
973
self._new_name = dict((k, v.decode('utf-8'))
974
for k, v in attribs['_new_name'].items())
975
self._new_parent = attribs['_new_parent']
976
self._new_executability = dict((k, bool(v)) for k, v in
977
attribs['_new_executability'].items())
978
self._new_id = attribs['_new_id']
979
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
980
self._tree_path_ids = {}
981
self._tree_id_paths = {}
982
for bytepath, trans_id in attribs['_tree_path_ids'].items():
983
path = bytepath.decode('utf-8')
984
self._tree_path_ids[path] = trans_id
985
self._tree_id_paths[trans_id] = path
986
self._removed_id = set(attribs['_removed_id'])
987
self._removed_contents = set(attribs['_removed_contents'])
988
self._non_present_ids = attribs['_non_present_ids']
989
for ((trans_id, kind),), content in records:
991
mpdiff = multiparent.MultiParent.from_patch(content)
992
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
993
self.create_file(lines, trans_id)
994
if kind == 'directory':
995
self.create_directory(trans_id)
996
if kind == 'symlink':
997
self.create_symlink(content.decode('utf-8'), trans_id)
1000
class DiskTreeTransform(TreeTransformBase):
1001
"""Tree transform storing its contents on disk."""
1003
def __init__(self, tree, limbodir, pb=DummyProgress(),
1004
case_sensitive=True):
1006
:param tree: The tree that will be transformed, but not necessarily
1008
:param limbodir: A directory where new files can be stored until
1009
they are installed in their proper places
1010
:param pb: A ProgressBar indicating how much progress is being made
1011
:param case_sensitive: If True, the target of the transform is
1012
case sensitive, not just case preserving.
1014
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1015
self._limbodir = limbodir
1016
self._deletiondir = None
1017
# A mapping of transform ids to their limbo filename
1018
self._limbo_files = {}
1019
# A mapping of transform ids to a set of the transform ids of children
1020
# that their limbo directory has
1021
self._limbo_children = {}
1022
# Map transform ids to maps of child filename to child transform id
1023
self._limbo_children_names = {}
1024
# List of transform ids that need to be renamed from limbo into place
1025
self._needs_rename = set()
1028
"""Release the working tree lock, if held, clean up limbo dir.
1030
This is required if apply has not been invoked, but can be invoked
1033
if self._tree is None:
1036
entries = [(self._limbo_name(t), t, k) for t, k in
1037
self._new_contents.iteritems()]
1038
entries.sort(reverse=True)
1039
for path, trans_id, kind in entries:
1042
delete_any(self._limbodir)
1044
# We don't especially care *why* the dir is immortal.
1045
raise ImmortalLimbo(self._limbodir)
1047
if self._deletiondir is not None:
1048
delete_any(self._deletiondir)
1050
raise errors.ImmortalPendingDeletion(self._deletiondir)
1052
TreeTransformBase.finalize(self)
1054
def _limbo_name(self, trans_id):
1055
"""Generate the limbo name of a file"""
1056
limbo_name = self._limbo_files.get(trans_id)
1057
if limbo_name is None:
1058
limbo_name = self._generate_limbo_path(trans_id)
1059
self._limbo_files[trans_id] = limbo_name
1062
def _generate_limbo_path(self, trans_id):
1063
"""Generate a limbo path using the trans_id as the relative path.
1065
This is suitable as a fallback, and when the transform should not be
1066
sensitive to the path encoding of the limbo directory.
1068
self._needs_rename.add(trans_id)
1069
return pathjoin(self._limbodir, trans_id)
1071
def adjust_path(self, name, parent, trans_id):
1072
previous_parent = self._new_parent.get(trans_id)
1073
previous_name = self._new_name.get(trans_id)
1074
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1075
if (trans_id in self._limbo_files and
1076
trans_id not in self._needs_rename):
1077
self._rename_in_limbo([trans_id])
1078
self._limbo_children[previous_parent].remove(trans_id)
1079
del self._limbo_children_names[previous_parent][previous_name]
1081
def _rename_in_limbo(self, trans_ids):
1082
"""Fix limbo names so that the right final path is produced.
1084
This means we outsmarted ourselves-- we tried to avoid renaming
1085
these files later by creating them with their final names in their
1086
final parents. But now the previous name or parent is no longer
1087
suitable, so we have to rename them.
1089
Even for trans_ids that have no new contents, we must remove their
1090
entries from _limbo_files, because they are now stale.
1092
for trans_id in trans_ids:
1093
old_path = self._limbo_files.pop(trans_id)
1094
if trans_id not in self._new_contents:
1096
new_path = self._limbo_name(trans_id)
1097
os.rename(old_path, new_path)
1099
def create_file(self, contents, trans_id, mode_id=None):
1100
"""Schedule creation of a new file.
1104
Contents is an iterator of strings, all of which will be written
1105
to the target destination.
1107
New file takes the permissions of any existing file with that id,
1108
unless mode_id is specified.
1110
name = self._limbo_name(trans_id)
1111
f = open(name, 'wb')
1114
unique_add(self._new_contents, trans_id, 'file')
1116
# Clean up the file, it never got registered so
1117
# TreeTransform.finalize() won't clean it up.
1122
f.writelines(contents)
1125
self._set_mode(trans_id, mode_id, S_ISREG)
1127
def _read_file_chunks(self, trans_id):
1128
cur_file = open(self._limbo_name(trans_id), 'rb')
1130
return cur_file.readlines()
1134
def _read_symlink_target(self, trans_id):
1135
return os.readlink(self._limbo_name(trans_id))
1137
def create_hardlink(self, path, trans_id):
1138
"""Schedule creation of a hard link"""
1139
name = self._limbo_name(trans_id)
1143
if e.errno != errno.EPERM:
1145
raise errors.HardLinkNotSupported(path)
1147
unique_add(self._new_contents, trans_id, 'file')
1149
# Clean up the file, it never got registered so
1150
# TreeTransform.finalize() won't clean it up.
1154
def create_directory(self, trans_id):
1155
"""Schedule creation of a new directory.
1157
See also new_directory.
1159
os.mkdir(self._limbo_name(trans_id))
1160
unique_add(self._new_contents, trans_id, 'directory')
1162
def create_symlink(self, target, trans_id):
1163
"""Schedule creation of a new symbolic link.
1165
target is a bytestring.
1166
See also new_symlink.
1169
os.symlink(target, self._limbo_name(trans_id))
1170
unique_add(self._new_contents, trans_id, 'symlink')
1173
path = FinalPaths(self).get_path(trans_id)
1176
raise UnableCreateSymlink(path=path)
1178
def cancel_creation(self, trans_id):
1179
"""Cancel the creation of new file contents."""
1180
del self._new_contents[trans_id]
1181
children = self._limbo_children.get(trans_id)
1182
# if this is a limbo directory with children, move them before removing
1184
if children is not None:
1185
self._rename_in_limbo(children)
1186
del self._limbo_children[trans_id]
1187
del self._limbo_children_names[trans_id]
1188
delete_any(self._limbo_name(trans_id))
1191
class TreeTransform(DiskTreeTransform):
1192
"""Represent a tree transformation.
1194
This object is designed to support incremental generation of the transform,
1197
However, it gives optimum performance when parent directories are created
1198
before their contents. The transform is then able to put child files
1199
directly in their parent directory, avoiding later renames.
1201
It is easy to produce malformed transforms, but they are generally
1202
harmless. Attempting to apply a malformed transform will cause an
1203
exception to be raised before any modifications are made to the tree.
1205
Many kinds of malformed transforms can be corrected with the
1206
resolve_conflicts function. The remaining ones indicate programming error,
1207
such as trying to create a file with no path.
1209
Two sets of file creation methods are supplied. Convenience methods are:
1214
These are composed of the low-level methods:
1216
* create_file or create_directory or create_symlink
1220
Transform/Transaction ids
1221
-------------------------
1222
trans_ids are temporary ids assigned to all files involved in a transform.
1223
It's possible, even common, that not all files in the Tree have trans_ids.
1225
trans_ids are used because filenames and file_ids are not good enough
1226
identifiers; filenames change, and not all files have file_ids. File-ids
1227
are also associated with trans-ids, so that moving a file moves its
1230
trans_ids are only valid for the TreeTransform that generated them.
1234
Limbo is a temporary directory use to hold new versions of files.
1235
Files are added to limbo by create_file, create_directory, create_symlink,
1236
and their convenience variants (new_*). Files may be removed from limbo
1237
using cancel_creation. Files are renamed from limbo into their final
1238
location as part of TreeTransform.apply
1240
Limbo must be cleaned up, by either calling TreeTransform.apply or
1241
calling TreeTransform.finalize.
1243
Files are placed into limbo inside their parent directories, where
1244
possible. This reduces subsequent renames, and makes operations involving
1245
lots of files faster. This optimization is only possible if the parent
1246
directory is created *before* creating any of its children, so avoid
1247
creating children before parents, where possible.
1251
This temporary directory is used by _FileMover for storing files that are
1252
about to be deleted. In case of rollback, the files will be restored.
1253
FileMover does not delete files until it is sure that a rollback will not
1256
def __init__(self, tree, pb=DummyProgress()):
1257
"""Note: a tree_write lock is taken on the tree.
1259
Use TreeTransform.finalize() to release the lock (can be omitted if
1260
TreeTransform.apply() called).
1262
tree.lock_tree_write()
1265
limbodir = urlutils.local_path_from_url(
1266
tree._transport.abspath('limbo'))
1270
if e.errno == errno.EEXIST:
1271
raise ExistingLimbo(limbodir)
1272
deletiondir = urlutils.local_path_from_url(
1273
tree._transport.abspath('pending-deletion'))
1275
os.mkdir(deletiondir)
1277
if e.errno == errno.EEXIST:
1278
raise errors.ExistingPendingDeletion(deletiondir)
1283
# Cache of realpath results, to speed up canonical_path
1284
self._realpaths = {}
1285
# Cache of relpath results, to speed up canonical_path
1287
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1288
tree.case_sensitive)
1289
self._deletiondir = deletiondir
1291
def canonical_path(self, path):
1292
"""Get the canonical tree-relative path"""
1293
# don't follow final symlinks
1294
abs = self._tree.abspath(path)
1295
if abs in self._relpaths:
1296
return self._relpaths[abs]
1297
dirname, basename = os.path.split(abs)
1298
if dirname not in self._realpaths:
1299
self._realpaths[dirname] = os.path.realpath(dirname)
1300
dirname = self._realpaths[dirname]
1301
abs = pathjoin(dirname, basename)
1302
if dirname in self._relpaths:
1303
relpath = pathjoin(self._relpaths[dirname], basename)
1304
relpath = relpath.rstrip('/\\')
1306
relpath = self._tree.relpath(abs)
1307
self._relpaths[abs] = relpath
1310
def tree_kind(self, trans_id):
1311
"""Determine the file kind in the working tree.
1313
Raises NoSuchFile if the file does not exist
1315
path = self._tree_id_paths.get(trans_id)
1317
raise NoSuchFile(None)
1319
return file_kind(self._tree.abspath(path))
1321
if e.errno != errno.ENOENT:
1324
raise NoSuchFile(path)
1326
def _set_mode(self, trans_id, mode_id, typefunc):
1327
"""Set the mode of new file contents.
1328
The mode_id is the existing file to get the mode from (often the same
1329
as trans_id). The operation is only performed if there's a mode match
1330
according to typefunc.
1335
old_path = self._tree_id_paths[mode_id]
1339
mode = os.stat(self._tree.abspath(old_path)).st_mode
1341
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1342
# Either old_path doesn't exist, or the parent of the
1343
# target is not a directory (but will be one eventually)
1344
# Either way, we know it doesn't exist *right now*
1345
# See also bug #248448
1350
os.chmod(self._limbo_name(trans_id), mode)
1352
def iter_tree_children(self, parent_id):
1353
"""Iterate through the entry's tree children, if any"""
1355
path = self._tree_id_paths[parent_id]
1359
children = os.listdir(self._tree.abspath(path))
1361
if not (osutils._is_error_enotdir(e)
1362
or e.errno in (errno.ENOENT, errno.ESRCH)):
1366
for child in children:
1367
childpath = joinpath(path, child)
1368
if self._tree.is_control_filename(childpath):
1370
yield self.trans_id_tree_path(childpath)
1372
def _generate_limbo_path(self, trans_id):
1373
"""Generate a limbo path using the final path if possible.
1375
This optimizes the performance of applying the tree transform by
1376
avoiding renames. These renames can be avoided only when the parent
1377
directory is already scheduled for creation.
1379
If the final path cannot be used, falls back to using the trans_id as
1382
parent = self._new_parent.get(trans_id)
1383
# if the parent directory is already in limbo (e.g. when building a
1384
# tree), choose a limbo name inside the parent, to reduce further
1386
use_direct_path = False
1387
if self._new_contents.get(parent) == 'directory':
1388
filename = self._new_name.get(trans_id)
1389
if filename is not None:
1390
if parent not in self._limbo_children:
1391
self._limbo_children[parent] = set()
1392
self._limbo_children_names[parent] = {}
1393
use_direct_path = True
1394
# the direct path can only be used if no other file has
1395
# already taken this pathname, i.e. if the name is unused, or
1396
# if it is already associated with this trans_id.
1397
elif self._case_sensitive_target:
1398
if (self._limbo_children_names[parent].get(filename)
1399
in (trans_id, None)):
1400
use_direct_path = True
1402
for l_filename, l_trans_id in\
1403
self._limbo_children_names[parent].iteritems():
1404
if l_trans_id == trans_id:
1406
if l_filename.lower() == filename.lower():
1409
use_direct_path = True
1411
if not use_direct_path:
1412
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1414
limbo_name = pathjoin(self._limbo_files[parent], filename)
1415
self._limbo_children[parent].add(trans_id)
1416
self._limbo_children_names[parent][filename] = trans_id
1420
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1421
"""Apply all changes to the inventory and filesystem.
1423
If filesystem or inventory conflicts are present, MalformedTransform
1426
If apply succeeds, finalize is not necessary.
1428
:param no_conflicts: if True, the caller guarantees there are no
1429
conflicts, so no check is made.
1430
:param precomputed_delta: An inventory delta to use instead of
1432
:param _mover: Supply an alternate FileMover, for testing
1434
if not no_conflicts:
1435
self._check_malformed()
1436
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1438
if precomputed_delta is None:
1439
child_pb.update('Apply phase', 0, 2)
1440
inventory_delta = self._generate_inventory_delta()
1443
inventory_delta = precomputed_delta
1446
mover = _FileMover()
1450
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1451
self._apply_removals(mover)
1452
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1453
modified_paths = self._apply_insertions(mover)
1458
mover.apply_deletions()
1461
self._tree.apply_inventory_delta(inventory_delta)
1464
return _TransformResults(modified_paths, self.rename_count)
1466
def _generate_inventory_delta(self):
1467
"""Generate an inventory delta for the current transform."""
1468
inventory_delta = []
1469
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1470
new_paths = self._inventory_altered()
1471
total_entries = len(new_paths) + len(self._removed_id)
1473
for num, trans_id in enumerate(self._removed_id):
1475
child_pb.update('removing file', num, total_entries)
1476
if trans_id == self._new_root:
1477
file_id = self._tree.get_root_id()
1479
file_id = self.tree_file_id(trans_id)
1480
# File-id isn't really being deleted, just moved
1481
if file_id in self._r_new_id:
1483
path = self._tree_id_paths[trans_id]
1484
inventory_delta.append((path, None, file_id, None))
1485
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1487
entries = self._tree.iter_entries_by_dir(
1488
new_path_file_ids.values())
1489
old_paths = dict((e.file_id, p) for p, e in entries)
1491
for num, (path, trans_id) in enumerate(new_paths):
1493
child_pb.update('adding file',
1494
num + len(self._removed_id), total_entries)
1495
file_id = new_path_file_ids[trans_id]
1500
kind = self.final_kind(trans_id)
1502
kind = self._tree.stored_kind(file_id)
1503
parent_trans_id = self.final_parent(trans_id)
1504
parent_file_id = new_path_file_ids.get(parent_trans_id)
1505
if parent_file_id is None:
1506
parent_file_id = self.final_file_id(parent_trans_id)
1507
if trans_id in self._new_reference_revision:
1508
new_entry = inventory.TreeReference(
1510
self._new_name[trans_id],
1511
self.final_file_id(self._new_parent[trans_id]),
1512
None, self._new_reference_revision[trans_id])
1514
new_entry = inventory.make_entry(kind,
1515
self.final_name(trans_id),
1516
parent_file_id, file_id)
1517
old_path = old_paths.get(new_entry.file_id)
1518
new_executability = self._new_executability.get(trans_id)
1519
if new_executability is not None:
1520
new_entry.executable = new_executability
1521
inventory_delta.append(
1522
(old_path, path, new_entry.file_id, new_entry))
1525
return inventory_delta
1527
def _apply_removals(self, mover):
1528
"""Perform tree operations that remove directory/inventory names.
1530
That is, delete files that are to be deleted, and put any files that
1531
need renaming into limbo. This must be done in strict child-to-parent
1534
If inventory_delta is None, no inventory delta generation is performed.
1536
tree_paths = list(self._tree_path_ids.iteritems())
1537
tree_paths.sort(reverse=True)
1538
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1540
for num, data in enumerate(tree_paths):
1541
path, trans_id = data
1542
child_pb.update('removing file', num, len(tree_paths))
1543
full_path = self._tree.abspath(path)
1544
if trans_id in self._removed_contents:
1545
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1547
elif trans_id in self._new_name or trans_id in \
1550
mover.rename(full_path, self._limbo_name(trans_id))
1552
if e.errno != errno.ENOENT:
1555
self.rename_count += 1
1559
def _apply_insertions(self, mover):
1560
"""Perform tree operations that insert directory/inventory names.
1562
That is, create any files that need to be created, and restore from
1563
limbo any files that needed renaming. This must be done in strict
1564
parent-to-child order.
1566
If inventory_delta is None, no inventory delta is calculated, and
1567
no list of modified paths is returned.
1569
new_paths = self.new_paths(filesystem_only=True)
1571
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1573
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1575
for num, (path, trans_id) in enumerate(new_paths):
1577
child_pb.update('adding file', num, len(new_paths))
1578
full_path = self._tree.abspath(path)
1579
if trans_id in self._needs_rename:
1581
mover.rename(self._limbo_name(trans_id), full_path)
1583
# We may be renaming a dangling inventory id
1584
if e.errno != errno.ENOENT:
1587
self.rename_count += 1
1588
if (trans_id in self._new_contents or
1589
self.path_changed(trans_id)):
1590
if trans_id in self._new_contents:
1591
modified_paths.append(full_path)
1592
if trans_id in self._new_executability:
1593
self._set_executability(path, trans_id)
1596
self._new_contents.clear()
1597
return modified_paths
1600
class TransformPreview(DiskTreeTransform):
1601
"""A TreeTransform for generating preview trees.
1603
Unlike TreeTransform, this version works when the input tree is a
1604
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1605
unversioned files in the input tree.
1608
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1610
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1611
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1613
def canonical_path(self, path):
1616
def tree_kind(self, trans_id):
1617
path = self._tree_id_paths.get(trans_id)
1619
raise NoSuchFile(None)
1620
file_id = self._tree.path2id(path)
1621
return self._tree.kind(file_id)
1623
def _set_mode(self, trans_id, mode_id, typefunc):
1624
"""Set the mode of new file contents.
1625
The mode_id is the existing file to get the mode from (often the same
1626
as trans_id). The operation is only performed if there's a mode match
1627
according to typefunc.
1629
# is it ok to ignore this? probably
1632
def iter_tree_children(self, parent_id):
1633
"""Iterate through the entry's tree children, if any"""
1635
path = self._tree_id_paths[parent_id]
1638
file_id = self.tree_file_id(parent_id)
1641
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1642
children = getattr(entry, 'children', {})
1643
for child in children:
1644
childpath = joinpath(path, child)
1645
yield self.trans_id_tree_path(childpath)
1648
class _PreviewTree(tree.Tree):
1649
"""Partial implementation of Tree to support show_diff_trees"""
1651
def __init__(self, transform):
1652
self._transform = transform
1653
self._final_paths = FinalPaths(transform)
1654
self.__by_parent = None
1655
self._parent_ids = []
1656
self._all_children_cache = {}
1657
self._path2trans_id_cache = {}
1658
self._final_name_cache = {}
1659
self._iter_changes_cache = dict((c[0], c) for c in
1660
self._transform.iter_changes())
1662
def _content_change(self, file_id):
1663
"""Return True if the content of this file changed"""
1664
changes = self._iter_changes_cache.get(file_id)
1665
# changes[2] is true if the file content changed. See
1666
# InterTree.iter_changes.
1667
return (changes is not None and changes[2])
1669
def _get_repository(self):
1670
repo = getattr(self._transform._tree, '_repository', None)
1672
repo = self._transform._tree.branch.repository
1675
def _iter_parent_trees(self):
1676
for revision_id in self.get_parent_ids():
1678
yield self.revision_tree(revision_id)
1679
except errors.NoSuchRevisionInTree:
1680
yield self._get_repository().revision_tree(revision_id)
1682
def _get_file_revision(self, file_id, vf, tree_revision):
1683
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1684
self._iter_parent_trees()]
1685
vf.add_lines((file_id, tree_revision), parent_keys,
1686
self.get_file(file_id).readlines())
1687
repo = self._get_repository()
1688
base_vf = repo.texts
1689
if base_vf not in vf.fallback_versionedfiles:
1690
vf.fallback_versionedfiles.append(base_vf)
1691
return tree_revision
1693
def _stat_limbo_file(self, file_id):
1694
trans_id = self._transform.trans_id_file_id(file_id)
1695
name = self._transform._limbo_name(trans_id)
1696
return os.lstat(name)
1699
def _by_parent(self):
1700
if self.__by_parent is None:
1701
self.__by_parent = self._transform.by_parent()
1702
return self.__by_parent
1704
def _comparison_data(self, entry, path):
1705
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1706
if kind == 'missing':
1710
file_id = self._transform.final_file_id(self._path2trans_id(path))
1711
executable = self.is_executable(file_id, path)
1712
return kind, executable, None
1714
def lock_read(self):
1715
# Perhaps in theory, this should lock the TreeTransform?
1722
def inventory(self):
1723
"""This Tree does not use inventory as its backing data."""
1724
raise NotImplementedError(_PreviewTree.inventory)
1726
def get_root_id(self):
1727
return self._transform.final_file_id(self._transform.root)
1729
def all_file_ids(self):
1730
tree_ids = set(self._transform._tree.all_file_ids())
1731
tree_ids.difference_update(self._transform.tree_file_id(t)
1732
for t in self._transform._removed_id)
1733
tree_ids.update(self._transform._new_id.values())
1737
return iter(self.all_file_ids())
1739
def _has_id(self, file_id, fallback_check):
1740
if file_id in self._transform._r_new_id:
1742
elif file_id in set([self._transform.tree_file_id(trans_id) for
1743
trans_id in self._transform._removed_id]):
1746
return fallback_check(file_id)
1748
def has_id(self, file_id):
1749
return self._has_id(file_id, self._transform._tree.has_id)
1751
def has_or_had_id(self, file_id):
1752
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1754
def _path2trans_id(self, path):
1755
# We must not use None here, because that is a valid value to store.
1756
trans_id = self._path2trans_id_cache.get(path, object)
1757
if trans_id is not object:
1759
segments = splitpath(path)
1760
cur_parent = self._transform.root
1761
for cur_segment in segments:
1762
for child in self._all_children(cur_parent):
1763
final_name = self._final_name_cache.get(child)
1764
if final_name is None:
1765
final_name = self._transform.final_name(child)
1766
self._final_name_cache[child] = final_name
1767
if final_name == cur_segment:
1771
self._path2trans_id_cache[path] = None
1773
self._path2trans_id_cache[path] = cur_parent
1776
def path2id(self, path):
1777
return self._transform.final_file_id(self._path2trans_id(path))
1779
def id2path(self, file_id):
1780
trans_id = self._transform.trans_id_file_id(file_id)
1782
return self._final_paths._determine_path(trans_id)
1784
raise errors.NoSuchId(self, file_id)
1786
def _all_children(self, trans_id):
1787
children = self._all_children_cache.get(trans_id)
1788
if children is not None:
1790
children = set(self._transform.iter_tree_children(trans_id))
1791
# children in the _new_parent set are provided by _by_parent.
1792
children.difference_update(self._transform._new_parent.keys())
1793
children.update(self._by_parent.get(trans_id, []))
1794
self._all_children_cache[trans_id] = children
1797
def iter_children(self, file_id):
1798
trans_id = self._transform.trans_id_file_id(file_id)
1799
for child_trans_id in self._all_children(trans_id):
1800
yield self._transform.final_file_id(child_trans_id)
1803
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1804
in self._transform._tree.extras())
1805
possible_extras.update(self._transform._new_contents)
1806
possible_extras.update(self._transform._removed_id)
1807
for trans_id in possible_extras:
1808
if self._transform.final_file_id(trans_id) is None:
1809
yield self._final_paths._determine_path(trans_id)
1811
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1812
yield_parents=False):
1813
for trans_id, parent_file_id in ordered_entries:
1814
file_id = self._transform.final_file_id(trans_id)
1817
if (specific_file_ids is not None
1818
and file_id not in specific_file_ids):
1821
kind = self._transform.final_kind(trans_id)
1823
kind = self._transform._tree.stored_kind(file_id)
1824
new_entry = inventory.make_entry(
1826
self._transform.final_name(trans_id),
1827
parent_file_id, file_id)
1828
yield new_entry, trans_id
1830
def _list_files_by_dir(self):
1831
todo = [ROOT_PARENT]
1833
while len(todo) > 0:
1835
parent_file_id = self._transform.final_file_id(parent)
1836
children = list(self._all_children(parent))
1837
paths = dict(zip(children, self._final_paths.get_paths(children)))
1838
children.sort(key=paths.get)
1839
todo.extend(reversed(children))
1840
for trans_id in children:
1841
ordered_ids.append((trans_id, parent_file_id))
1844
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1845
# This may not be a maximally efficient implementation, but it is
1846
# reasonably straightforward. An implementation that grafts the
1847
# TreeTransform changes onto the tree's iter_entries_by_dir results
1848
# might be more efficient, but requires tricky inferences about stack
1850
ordered_ids = self._list_files_by_dir()
1851
for entry, trans_id in self._make_inv_entries(ordered_ids,
1852
specific_file_ids, yield_parents=yield_parents):
1853
yield unicode(self._final_paths.get_path(trans_id)), entry
1855
def _iter_entries_for_dir(self, dir_path):
1856
"""Return path, entry for items in a directory without recursing down."""
1857
dir_file_id = self.path2id(dir_path)
1859
for file_id in self.iter_children(dir_file_id):
1860
trans_id = self._transform.trans_id_file_id(file_id)
1861
ordered_ids.append((trans_id, file_id))
1862
for entry, trans_id in self._make_inv_entries(ordered_ids):
1863
yield unicode(self._final_paths.get_path(trans_id)), entry
1865
def list_files(self, include_root=False, from_dir=None, recursive=True):
1866
"""See WorkingTree.list_files."""
1867
# XXX This should behave like WorkingTree.list_files, but is really
1868
# more like RevisionTree.list_files.
1872
prefix = from_dir + '/'
1873
entries = self.iter_entries_by_dir()
1874
for path, entry in entries:
1875
if entry.name == '' and not include_root:
1878
if not path.startswith(prefix):
1880
path = path[len(prefix):]
1881
yield path, 'V', entry.kind, entry.file_id, entry
1883
if from_dir is None and include_root is True:
1884
root_entry = inventory.make_entry('directory', '',
1885
ROOT_PARENT, self.get_root_id())
1886
yield '', 'V', 'directory', root_entry.file_id, root_entry
1887
entries = self._iter_entries_for_dir(from_dir or '')
1888
for path, entry in entries:
1889
yield path, 'V', entry.kind, entry.file_id, entry
1891
def kind(self, file_id):
1892
trans_id = self._transform.trans_id_file_id(file_id)
1893
return self._transform.final_kind(trans_id)
1895
def stored_kind(self, file_id):
1896
trans_id = self._transform.trans_id_file_id(file_id)
1898
return self._transform._new_contents[trans_id]
1900
return self._transform._tree.stored_kind(file_id)
1902
def get_file_mtime(self, file_id, path=None):
1903
"""See Tree.get_file_mtime"""
1904
if not self._content_change(file_id):
1905
return self._transform._tree.get_file_mtime(file_id, path)
1906
return self._stat_limbo_file(file_id).st_mtime
1908
def _file_size(self, entry, stat_value):
1909
return self.get_file_size(entry.file_id)
1911
def get_file_size(self, file_id):
1912
"""See Tree.get_file_size"""
1913
if self.kind(file_id) == 'file':
1914
return self._transform._tree.get_file_size(file_id)
1918
def get_file_sha1(self, file_id, path=None, stat_value=None):
1919
trans_id = self._transform.trans_id_file_id(file_id)
1920
kind = self._transform._new_contents.get(trans_id)
1922
return self._transform._tree.get_file_sha1(file_id)
1924
fileobj = self.get_file(file_id)
1926
return sha_file(fileobj)
1930
def is_executable(self, file_id, path=None):
1933
trans_id = self._transform.trans_id_file_id(file_id)
1935
return self._transform._new_executability[trans_id]
1938
return self._transform._tree.is_executable(file_id, path)
1940
if e.errno == errno.ENOENT:
1943
except errors.NoSuchId:
1946
def path_content_summary(self, path):
1947
trans_id = self._path2trans_id(path)
1948
tt = self._transform
1949
tree_path = tt._tree_id_paths.get(trans_id)
1950
kind = tt._new_contents.get(trans_id)
1952
if tree_path is None or trans_id in tt._removed_contents:
1953
return 'missing', None, None, None
1954
summary = tt._tree.path_content_summary(tree_path)
1955
kind, size, executable, link_or_sha1 = summary
1958
limbo_name = tt._limbo_name(trans_id)
1959
if trans_id in tt._new_reference_revision:
1960
kind = 'tree-reference'
1962
statval = os.lstat(limbo_name)
1963
size = statval.st_size
1964
if not supports_executable():
1967
executable = statval.st_mode & S_IEXEC
1971
if kind == 'symlink':
1972
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
1973
if supports_executable():
1974
executable = tt._new_executability.get(trans_id, executable)
1975
return kind, size, executable, link_or_sha1
1977
def iter_changes(self, from_tree, include_unchanged=False,
1978
specific_files=None, pb=None, extra_trees=None,
1979
require_versioned=True, want_unversioned=False):
1980
"""See InterTree.iter_changes.
1982
This has a fast path that is only used when the from_tree matches
1983
the transform tree, and no fancy options are supplied.
1985
if (from_tree is not self._transform._tree or include_unchanged or
1986
specific_files or want_unversioned):
1987
return tree.InterTree(from_tree, self).iter_changes(
1988
include_unchanged=include_unchanged,
1989
specific_files=specific_files,
1991
extra_trees=extra_trees,
1992
require_versioned=require_versioned,
1993
want_unversioned=want_unversioned)
1994
if want_unversioned:
1995
raise ValueError('want_unversioned is not supported')
1996
return self._transform.iter_changes()
1998
def get_file(self, file_id, path=None):
1999
"""See Tree.get_file"""
2000
if not self._content_change(file_id):
2001
return self._transform._tree.get_file(file_id, path)
2002
trans_id = self._transform.trans_id_file_id(file_id)
2003
name = self._transform._limbo_name(trans_id)
2004
return open(name, 'rb')
2006
def get_file_with_stat(self, file_id, path=None):
2007
return self.get_file(file_id, path), None
2009
def annotate_iter(self, file_id,
2010
default_revision=_mod_revision.CURRENT_REVISION):
2011
changes = self._iter_changes_cache.get(file_id)
2015
changed_content, versioned, kind = (changes[2], changes[3],
2019
get_old = (kind[0] == 'file' and versioned[0])
2021
old_annotation = self._transform._tree.annotate_iter(file_id,
2022
default_revision=default_revision)
2026
return old_annotation
2027
if not changed_content:
2028
return old_annotation
2029
# TODO: This is doing something similar to what WT.annotate_iter is
2030
# doing, however it fails slightly because it doesn't know what
2031
# the *other* revision_id is, so it doesn't know how to give the
2032
# other as the origin for some lines, they all get
2033
# 'default_revision'
2034
# It would be nice to be able to use the new Annotator based
2035
# approach, as well.
2036
return annotate.reannotate([old_annotation],
2037
self.get_file(file_id).readlines(),
2040
def get_symlink_target(self, file_id):
2041
"""See Tree.get_symlink_target"""
2042
if not self._content_change(file_id):
2043
return self._transform._tree.get_symlink_target(file_id)
2044
trans_id = self._transform.trans_id_file_id(file_id)
2045
name = self._transform._limbo_name(trans_id)
2046
return osutils.readlink(name)
2048
def walkdirs(self, prefix=''):
2049
pending = [self._transform.root]
2050
while len(pending) > 0:
2051
parent_id = pending.pop()
2054
prefix = prefix.rstrip('/')
2055
parent_path = self._final_paths.get_path(parent_id)
2056
parent_file_id = self._transform.final_file_id(parent_id)
2057
for child_id in self._all_children(parent_id):
2058
path_from_root = self._final_paths.get_path(child_id)
2059
basename = self._transform.final_name(child_id)
2060
file_id = self._transform.final_file_id(child_id)
2062
kind = self._transform.final_kind(child_id)
2063
versioned_kind = kind
2066
versioned_kind = self._transform._tree.stored_kind(file_id)
2067
if versioned_kind == 'directory':
2068
subdirs.append(child_id)
2069
children.append((path_from_root, basename, kind, None,
2070
file_id, versioned_kind))
2072
if parent_path.startswith(prefix):
2073
yield (parent_path, parent_file_id), children
2074
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2077
def get_parent_ids(self):
2078
return self._parent_ids
2080
def set_parent_ids(self, parent_ids):
2081
self._parent_ids = parent_ids
2083
def get_revision_tree(self, revision_id):
2084
return self._transform._tree.get_revision_tree(revision_id)
1205
2087
def joinpath(parent, child):
1206
2088
"""Join tree-relative paths, handling the tree root specially"""