1166
917
(from_executable, to_executable)))
1167
918
return iter(sorted(results, key=lambda x:x[1]))
920
def get_preview_tree(self):
921
"""Return a tree representing the result of the transform.
923
The tree is a snapshot, and altering the TreeTransform will invalidate
926
return _PreviewTree(self)
928
def commit(self, branch, message, merge_parents=None, strict=False):
929
"""Commit the result of this TreeTransform to a branch.
931
:param branch: The branch to commit to.
932
:param message: The message to attach to the commit.
933
:param merge_parents: Additional parents specified by pending merges.
934
:return: The revision_id of the revision committed.
936
self._check_malformed()
938
unversioned = set(self._new_contents).difference(set(self._new_id))
939
for trans_id in unversioned:
940
if self.final_file_id(trans_id) is None:
941
raise errors.StrictCommitFailed()
943
revno, last_rev_id = branch.last_revision_info()
944
if last_rev_id == _mod_revision.NULL_REVISION:
945
if merge_parents is not None:
946
raise ValueError('Cannot supply merge parents for first'
950
parent_ids = [last_rev_id]
951
if merge_parents is not None:
952
parent_ids.extend(merge_parents)
953
if self._tree.get_revision_id() != last_rev_id:
954
raise ValueError('TreeTransform not based on branch basis: %s' %
955
self._tree.get_revision_id())
956
builder = branch.get_commit_builder(parent_ids)
957
preview = self.get_preview_tree()
958
list(builder.record_iter_changes(preview, last_rev_id,
959
self.iter_changes()))
960
builder.finish_inventory()
961
revision_id = builder.commit(message)
962
branch.set_last_revision_info(revno + 1, revision_id)
965
def _text_parent(self, trans_id):
966
file_id = self.tree_file_id(trans_id)
968
if file_id is None or self._tree.kind(file_id) != 'file':
970
except errors.NoSuchFile:
974
def _get_parents_texts(self, trans_id):
975
"""Get texts for compression parents of this file."""
976
file_id = self._text_parent(trans_id)
979
return (self._tree.get_file_text(file_id),)
981
def _get_parents_lines(self, trans_id):
982
"""Get lines for compression parents of this file."""
983
file_id = self._text_parent(trans_id)
986
return (self._tree.get_file_lines(file_id),)
988
def serialize(self, serializer):
989
"""Serialize this TreeTransform.
991
:param serializer: A Serialiser like pack.ContainerSerializer.
993
new_name = dict((k, v.encode('utf-8')) for k, v in
994
self._new_name.items())
995
new_executability = dict((k, int(v)) for k, v in
996
self._new_executability.items())
997
tree_path_ids = dict((k.encode('utf-8'), v)
998
for k, v in self._tree_path_ids.items())
1000
'_id_number': self._id_number,
1001
'_new_name': new_name,
1002
'_new_parent': self._new_parent,
1003
'_new_executability': new_executability,
1004
'_new_id': self._new_id,
1005
'_tree_path_ids': tree_path_ids,
1006
'_removed_id': list(self._removed_id),
1007
'_removed_contents': list(self._removed_contents),
1008
'_non_present_ids': self._non_present_ids,
1010
yield serializer.bytes_record(bencode.bencode(attribs),
1012
for trans_id, kind in self._new_contents.items():
1014
lines = osutils.chunks_to_lines(
1015
self._read_file_chunks(trans_id))
1016
parents = self._get_parents_lines(trans_id)
1017
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1018
content = ''.join(mpdiff.to_patch())
1019
if kind == 'directory':
1021
if kind == 'symlink':
1022
content = self._read_symlink_target(trans_id)
1023
yield serializer.bytes_record(content, ((trans_id, kind),))
1025
def deserialize(self, records):
1026
"""Deserialize a stored TreeTransform.
1028
:param records: An iterable of (names, content) tuples, as per
1029
pack.ContainerPushParser.
1031
names, content = records.next()
1032
attribs = bencode.bdecode(content)
1033
self._id_number = attribs['_id_number']
1034
self._new_name = dict((k, v.decode('utf-8'))
1035
for k, v in attribs['_new_name'].items())
1036
self._new_parent = attribs['_new_parent']
1037
self._new_executability = dict((k, bool(v)) for k, v in
1038
attribs['_new_executability'].items())
1039
self._new_id = attribs['_new_id']
1040
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1041
self._tree_path_ids = {}
1042
self._tree_id_paths = {}
1043
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1044
path = bytepath.decode('utf-8')
1045
self._tree_path_ids[path] = trans_id
1046
self._tree_id_paths[trans_id] = path
1047
self._removed_id = set(attribs['_removed_id'])
1048
self._removed_contents = set(attribs['_removed_contents'])
1049
self._non_present_ids = attribs['_non_present_ids']
1050
for ((trans_id, kind),), content in records:
1052
mpdiff = multiparent.MultiParent.from_patch(content)
1053
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1054
self.create_file(lines, trans_id)
1055
if kind == 'directory':
1056
self.create_directory(trans_id)
1057
if kind == 'symlink':
1058
self.create_symlink(content.decode('utf-8'), trans_id)
1061
class DiskTreeTransform(TreeTransformBase):
1062
"""Tree transform storing its contents on disk."""
1064
def __init__(self, tree, limbodir, pb=DummyProgress(),
1065
case_sensitive=True):
1067
:param tree: The tree that will be transformed, but not necessarily
1069
:param limbodir: A directory where new files can be stored until
1070
they are installed in their proper places
1071
:param pb: A ProgressBar indicating how much progress is being made
1072
:param case_sensitive: If True, the target of the transform is
1073
case sensitive, not just case preserving.
1075
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1076
self._limbodir = limbodir
1077
self._deletiondir = None
1078
# A mapping of transform ids to their limbo filename
1079
self._limbo_files = {}
1080
# A mapping of transform ids to a set of the transform ids of children
1081
# that their limbo directory has
1082
self._limbo_children = {}
1083
# Map transform ids to maps of child filename to child transform id
1084
self._limbo_children_names = {}
1085
# List of transform ids that need to be renamed from limbo into place
1086
self._needs_rename = set()
1089
"""Release the working tree lock, if held, clean up limbo dir.
1091
This is required if apply has not been invoked, but can be invoked
1094
if self._tree is None:
1097
entries = [(self._limbo_name(t), t, k) for t, k in
1098
self._new_contents.iteritems()]
1099
entries.sort(reverse=True)
1100
for path, trans_id, kind in entries:
1103
delete_any(self._limbodir)
1105
# We don't especially care *why* the dir is immortal.
1106
raise ImmortalLimbo(self._limbodir)
1108
if self._deletiondir is not None:
1109
delete_any(self._deletiondir)
1111
raise errors.ImmortalPendingDeletion(self._deletiondir)
1113
TreeTransformBase.finalize(self)
1115
def _limbo_name(self, trans_id):
1116
"""Generate the limbo name of a file"""
1117
limbo_name = self._limbo_files.get(trans_id)
1118
if limbo_name is None:
1119
limbo_name = self._generate_limbo_path(trans_id)
1120
self._limbo_files[trans_id] = limbo_name
1123
def _generate_limbo_path(self, trans_id):
1124
"""Generate a limbo path using the trans_id as the relative path.
1126
This is suitable as a fallback, and when the transform should not be
1127
sensitive to the path encoding of the limbo directory.
1129
self._needs_rename.add(trans_id)
1130
return pathjoin(self._limbodir, trans_id)
1132
def adjust_path(self, name, parent, trans_id):
1133
previous_parent = self._new_parent.get(trans_id)
1134
previous_name = self._new_name.get(trans_id)
1135
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1136
if (trans_id in self._limbo_files and
1137
trans_id not in self._needs_rename):
1138
self._rename_in_limbo([trans_id])
1139
if previous_parent != parent:
1140
self._limbo_children[previous_parent].remove(trans_id)
1141
if previous_parent != parent or previous_name != name:
1142
del self._limbo_children_names[previous_parent][previous_name]
1144
def _rename_in_limbo(self, trans_ids):
1145
"""Fix limbo names so that the right final path is produced.
1147
This means we outsmarted ourselves-- we tried to avoid renaming
1148
these files later by creating them with their final names in their
1149
final parents. But now the previous name or parent is no longer
1150
suitable, so we have to rename them.
1152
Even for trans_ids that have no new contents, we must remove their
1153
entries from _limbo_files, because they are now stale.
1155
for trans_id in trans_ids:
1156
old_path = self._limbo_files.pop(trans_id)
1157
if trans_id not in self._new_contents:
1159
new_path = self._limbo_name(trans_id)
1160
os.rename(old_path, new_path)
1162
def create_file(self, contents, trans_id, mode_id=None):
1163
"""Schedule creation of a new file.
1167
Contents is an iterator of strings, all of which will be written
1168
to the target destination.
1170
New file takes the permissions of any existing file with that id,
1171
unless mode_id is specified.
1173
name = self._limbo_name(trans_id)
1174
f = open(name, 'wb')
1177
unique_add(self._new_contents, trans_id, 'file')
1179
# Clean up the file, it never got registered so
1180
# TreeTransform.finalize() won't clean it up.
1185
f.writelines(contents)
1188
self._set_mode(trans_id, mode_id, S_ISREG)
1190
def _read_file_chunks(self, trans_id):
1191
cur_file = open(self._limbo_name(trans_id), 'rb')
1193
return cur_file.readlines()
1197
def _read_symlink_target(self, trans_id):
1198
return os.readlink(self._limbo_name(trans_id))
1200
def create_hardlink(self, path, trans_id):
1201
"""Schedule creation of a hard link"""
1202
name = self._limbo_name(trans_id)
1206
if e.errno != errno.EPERM:
1208
raise errors.HardLinkNotSupported(path)
1210
unique_add(self._new_contents, trans_id, 'file')
1212
# Clean up the file, it never got registered so
1213
# TreeTransform.finalize() won't clean it up.
1217
def create_directory(self, trans_id):
1218
"""Schedule creation of a new directory.
1220
See also new_directory.
1222
os.mkdir(self._limbo_name(trans_id))
1223
unique_add(self._new_contents, trans_id, 'directory')
1225
def create_symlink(self, target, trans_id):
1226
"""Schedule creation of a new symbolic link.
1228
target is a bytestring.
1229
See also new_symlink.
1232
os.symlink(target, self._limbo_name(trans_id))
1233
unique_add(self._new_contents, trans_id, 'symlink')
1236
path = FinalPaths(self).get_path(trans_id)
1239
raise UnableCreateSymlink(path=path)
1241
def cancel_creation(self, trans_id):
1242
"""Cancel the creation of new file contents."""
1243
del self._new_contents[trans_id]
1244
children = self._limbo_children.get(trans_id)
1245
# if this is a limbo directory with children, move them before removing
1247
if children is not None:
1248
self._rename_in_limbo(children)
1249
del self._limbo_children[trans_id]
1250
del self._limbo_children_names[trans_id]
1251
delete_any(self._limbo_name(trans_id))
1254
class TreeTransform(DiskTreeTransform):
1255
"""Represent a tree transformation.
1257
This object is designed to support incremental generation of the transform,
1260
However, it gives optimum performance when parent directories are created
1261
before their contents. The transform is then able to put child files
1262
directly in their parent directory, avoiding later renames.
1264
It is easy to produce malformed transforms, but they are generally
1265
harmless. Attempting to apply a malformed transform will cause an
1266
exception to be raised before any modifications are made to the tree.
1268
Many kinds of malformed transforms can be corrected with the
1269
resolve_conflicts function. The remaining ones indicate programming error,
1270
such as trying to create a file with no path.
1272
Two sets of file creation methods are supplied. Convenience methods are:
1277
These are composed of the low-level methods:
1279
* create_file or create_directory or create_symlink
1283
Transform/Transaction ids
1284
-------------------------
1285
trans_ids are temporary ids assigned to all files involved in a transform.
1286
It's possible, even common, that not all files in the Tree have trans_ids.
1288
trans_ids are used because filenames and file_ids are not good enough
1289
identifiers; filenames change, and not all files have file_ids. File-ids
1290
are also associated with trans-ids, so that moving a file moves its
1293
trans_ids are only valid for the TreeTransform that generated them.
1297
Limbo is a temporary directory use to hold new versions of files.
1298
Files are added to limbo by create_file, create_directory, create_symlink,
1299
and their convenience variants (new_*). Files may be removed from limbo
1300
using cancel_creation. Files are renamed from limbo into their final
1301
location as part of TreeTransform.apply
1303
Limbo must be cleaned up, by either calling TreeTransform.apply or
1304
calling TreeTransform.finalize.
1306
Files are placed into limbo inside their parent directories, where
1307
possible. This reduces subsequent renames, and makes operations involving
1308
lots of files faster. This optimization is only possible if the parent
1309
directory is created *before* creating any of its children, so avoid
1310
creating children before parents, where possible.
1314
This temporary directory is used by _FileMover for storing files that are
1315
about to be deleted. In case of rollback, the files will be restored.
1316
FileMover does not delete files until it is sure that a rollback will not
1319
def __init__(self, tree, pb=DummyProgress()):
1320
"""Note: a tree_write lock is taken on the tree.
1322
Use TreeTransform.finalize() to release the lock (can be omitted if
1323
TreeTransform.apply() called).
1325
tree.lock_tree_write()
1328
limbodir = urlutils.local_path_from_url(
1329
tree._transport.abspath('limbo'))
1333
if e.errno == errno.EEXIST:
1334
raise ExistingLimbo(limbodir)
1335
deletiondir = urlutils.local_path_from_url(
1336
tree._transport.abspath('pending-deletion'))
1338
os.mkdir(deletiondir)
1340
if e.errno == errno.EEXIST:
1341
raise errors.ExistingPendingDeletion(deletiondir)
1346
# Cache of realpath results, to speed up canonical_path
1347
self._realpaths = {}
1348
# Cache of relpath results, to speed up canonical_path
1350
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1351
tree.case_sensitive)
1352
self._deletiondir = deletiondir
1354
def canonical_path(self, path):
1355
"""Get the canonical tree-relative path"""
1356
# don't follow final symlinks
1357
abs = self._tree.abspath(path)
1358
if abs in self._relpaths:
1359
return self._relpaths[abs]
1360
dirname, basename = os.path.split(abs)
1361
if dirname not in self._realpaths:
1362
self._realpaths[dirname] = os.path.realpath(dirname)
1363
dirname = self._realpaths[dirname]
1364
abs = pathjoin(dirname, basename)
1365
if dirname in self._relpaths:
1366
relpath = pathjoin(self._relpaths[dirname], basename)
1367
relpath = relpath.rstrip('/\\')
1369
relpath = self._tree.relpath(abs)
1370
self._relpaths[abs] = relpath
1373
def tree_kind(self, trans_id):
1374
"""Determine the file kind in the working tree.
1376
Raises NoSuchFile if the file does not exist
1378
path = self._tree_id_paths.get(trans_id)
1380
raise NoSuchFile(None)
1382
return file_kind(self._tree.abspath(path))
1384
if e.errno != errno.ENOENT:
1387
raise NoSuchFile(path)
1389
def _set_mode(self, trans_id, mode_id, typefunc):
1390
"""Set the mode of new file contents.
1391
The mode_id is the existing file to get the mode from (often the same
1392
as trans_id). The operation is only performed if there's a mode match
1393
according to typefunc.
1398
old_path = self._tree_id_paths[mode_id]
1402
mode = os.stat(self._tree.abspath(old_path)).st_mode
1404
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1405
# Either old_path doesn't exist, or the parent of the
1406
# target is not a directory (but will be one eventually)
1407
# Either way, we know it doesn't exist *right now*
1408
# See also bug #248448
1413
os.chmod(self._limbo_name(trans_id), mode)
1415
def iter_tree_children(self, parent_id):
1416
"""Iterate through the entry's tree children, if any"""
1418
path = self._tree_id_paths[parent_id]
1422
children = os.listdir(self._tree.abspath(path))
1424
if not (osutils._is_error_enotdir(e)
1425
or e.errno in (errno.ENOENT, errno.ESRCH)):
1429
for child in children:
1430
childpath = joinpath(path, child)
1431
if self._tree.is_control_filename(childpath):
1433
yield self.trans_id_tree_path(childpath)
1435
def _generate_limbo_path(self, trans_id):
1436
"""Generate a limbo path using the final path if possible.
1438
This optimizes the performance of applying the tree transform by
1439
avoiding renames. These renames can be avoided only when the parent
1440
directory is already scheduled for creation.
1442
If the final path cannot be used, falls back to using the trans_id as
1445
parent = self._new_parent.get(trans_id)
1446
# if the parent directory is already in limbo (e.g. when building a
1447
# tree), choose a limbo name inside the parent, to reduce further
1449
use_direct_path = False
1450
if self._new_contents.get(parent) == 'directory':
1451
filename = self._new_name.get(trans_id)
1452
if filename is not None:
1453
if parent not in self._limbo_children:
1454
self._limbo_children[parent] = set()
1455
self._limbo_children_names[parent] = {}
1456
use_direct_path = True
1457
# the direct path can only be used if no other file has
1458
# already taken this pathname, i.e. if the name is unused, or
1459
# if it is already associated with this trans_id.
1460
elif self._case_sensitive_target:
1461
if (self._limbo_children_names[parent].get(filename)
1462
in (trans_id, None)):
1463
use_direct_path = True
1465
for l_filename, l_trans_id in\
1466
self._limbo_children_names[parent].iteritems():
1467
if l_trans_id == trans_id:
1469
if l_filename.lower() == filename.lower():
1472
use_direct_path = True
1474
if not use_direct_path:
1475
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1477
limbo_name = pathjoin(self._limbo_files[parent], filename)
1478
self._limbo_children[parent].add(trans_id)
1479
self._limbo_children_names[parent][filename] = trans_id
1483
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1484
"""Apply all changes to the inventory and filesystem.
1486
If filesystem or inventory conflicts are present, MalformedTransform
1489
If apply succeeds, finalize is not necessary.
1491
:param no_conflicts: if True, the caller guarantees there are no
1492
conflicts, so no check is made.
1493
:param precomputed_delta: An inventory delta to use instead of
1495
:param _mover: Supply an alternate FileMover, for testing
1497
if not no_conflicts:
1498
self._check_malformed()
1499
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1501
if precomputed_delta is None:
1502
child_pb.update('Apply phase', 0, 2)
1503
inventory_delta = self._generate_inventory_delta()
1506
inventory_delta = precomputed_delta
1509
mover = _FileMover()
1513
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1514
self._apply_removals(mover)
1515
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1516
modified_paths = self._apply_insertions(mover)
1521
mover.apply_deletions()
1524
self._tree.apply_inventory_delta(inventory_delta)
1527
return _TransformResults(modified_paths, self.rename_count)
1529
def _generate_inventory_delta(self):
1530
"""Generate an inventory delta for the current transform."""
1531
inventory_delta = []
1532
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1533
new_paths = self._inventory_altered()
1534
total_entries = len(new_paths) + len(self._removed_id)
1536
for num, trans_id in enumerate(self._removed_id):
1538
child_pb.update('removing file', num, total_entries)
1539
if trans_id == self._new_root:
1540
file_id = self._tree.get_root_id()
1542
file_id = self.tree_file_id(trans_id)
1543
# File-id isn't really being deleted, just moved
1544
if file_id in self._r_new_id:
1546
path = self._tree_id_paths[trans_id]
1547
inventory_delta.append((path, None, file_id, None))
1548
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1550
entries = self._tree.iter_entries_by_dir(
1551
new_path_file_ids.values())
1552
old_paths = dict((e.file_id, p) for p, e in entries)
1554
for num, (path, trans_id) in enumerate(new_paths):
1556
child_pb.update('adding file',
1557
num + len(self._removed_id), total_entries)
1558
file_id = new_path_file_ids[trans_id]
1563
kind = self.final_kind(trans_id)
1565
kind = self._tree.stored_kind(file_id)
1566
parent_trans_id = self.final_parent(trans_id)
1567
parent_file_id = new_path_file_ids.get(parent_trans_id)
1568
if parent_file_id is None:
1569
parent_file_id = self.final_file_id(parent_trans_id)
1570
if trans_id in self._new_reference_revision:
1571
new_entry = inventory.TreeReference(
1573
self._new_name[trans_id],
1574
self.final_file_id(self._new_parent[trans_id]),
1575
None, self._new_reference_revision[trans_id])
1577
new_entry = inventory.make_entry(kind,
1578
self.final_name(trans_id),
1579
parent_file_id, file_id)
1580
old_path = old_paths.get(new_entry.file_id)
1581
new_executability = self._new_executability.get(trans_id)
1582
if new_executability is not None:
1583
new_entry.executable = new_executability
1584
inventory_delta.append(
1585
(old_path, path, new_entry.file_id, new_entry))
1588
return inventory_delta
1590
def _apply_removals(self, mover):
1591
"""Perform tree operations that remove directory/inventory names.
1593
That is, delete files that are to be deleted, and put any files that
1594
need renaming into limbo. This must be done in strict child-to-parent
1597
If inventory_delta is None, no inventory delta generation is performed.
1599
tree_paths = list(self._tree_path_ids.iteritems())
1600
tree_paths.sort(reverse=True)
1601
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1603
for num, data in enumerate(tree_paths):
1604
path, trans_id = data
1605
child_pb.update('removing file', num, len(tree_paths))
1606
full_path = self._tree.abspath(path)
1607
if trans_id in self._removed_contents:
1608
delete_path = os.path.join(self._deletiondir, trans_id)
1609
mover.pre_delete(full_path, delete_path)
1610
elif (trans_id in self._new_name
1611
or trans_id in self._new_parent):
1613
mover.rename(full_path, self._limbo_name(trans_id))
1615
if e.errno != errno.ENOENT:
1618
self.rename_count += 1
1622
def _apply_insertions(self, mover):
1623
"""Perform tree operations that insert directory/inventory names.
1625
That is, create any files that need to be created, and restore from
1626
limbo any files that needed renaming. This must be done in strict
1627
parent-to-child order.
1629
If inventory_delta is None, no inventory delta is calculated, and
1630
no list of modified paths is returned.
1632
new_paths = self.new_paths(filesystem_only=True)
1634
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1636
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1638
for num, (path, trans_id) in enumerate(new_paths):
1640
child_pb.update('adding file', num, len(new_paths))
1641
full_path = self._tree.abspath(path)
1642
if trans_id in self._needs_rename:
1644
mover.rename(self._limbo_name(trans_id), full_path)
1646
# We may be renaming a dangling inventory id
1647
if e.errno != errno.ENOENT:
1650
self.rename_count += 1
1651
if (trans_id in self._new_contents or
1652
self.path_changed(trans_id)):
1653
if trans_id in self._new_contents:
1654
modified_paths.append(full_path)
1655
if trans_id in self._new_executability:
1656
self._set_executability(path, trans_id)
1659
self._new_contents.clear()
1660
return modified_paths
1663
class TransformPreview(DiskTreeTransform):
1664
"""A TreeTransform for generating preview trees.
1666
Unlike TreeTransform, this version works when the input tree is a
1667
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1668
unversioned files in the input tree.
1671
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1673
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1674
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1676
def canonical_path(self, path):
1679
def tree_kind(self, trans_id):
1680
path = self._tree_id_paths.get(trans_id)
1682
raise NoSuchFile(None)
1683
file_id = self._tree.path2id(path)
1684
return self._tree.kind(file_id)
1686
def _set_mode(self, trans_id, mode_id, typefunc):
1687
"""Set the mode of new file contents.
1688
The mode_id is the existing file to get the mode from (often the same
1689
as trans_id). The operation is only performed if there's a mode match
1690
according to typefunc.
1692
# is it ok to ignore this? probably
1695
def iter_tree_children(self, parent_id):
1696
"""Iterate through the entry's tree children, if any"""
1698
path = self._tree_id_paths[parent_id]
1701
file_id = self.tree_file_id(parent_id)
1704
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1705
children = getattr(entry, 'children', {})
1706
for child in children:
1707
childpath = joinpath(path, child)
1708
yield self.trans_id_tree_path(childpath)
1711
class _PreviewTree(tree.Tree):
1712
"""Partial implementation of Tree to support show_diff_trees"""
1714
def __init__(self, transform):
1715
self._transform = transform
1716
self._final_paths = FinalPaths(transform)
1717
self.__by_parent = None
1718
self._parent_ids = []
1719
self._all_children_cache = {}
1720
self._path2trans_id_cache = {}
1721
self._final_name_cache = {}
1722
self._iter_changes_cache = dict((c[0], c) for c in
1723
self._transform.iter_changes())
1725
def _content_change(self, file_id):
1726
"""Return True if the content of this file changed"""
1727
changes = self._iter_changes_cache.get(file_id)
1728
# changes[2] is true if the file content changed. See
1729
# InterTree.iter_changes.
1730
return (changes is not None and changes[2])
1732
def _get_repository(self):
1733
repo = getattr(self._transform._tree, '_repository', None)
1735
repo = self._transform._tree.branch.repository
1738
def _iter_parent_trees(self):
1739
for revision_id in self.get_parent_ids():
1741
yield self.revision_tree(revision_id)
1742
except errors.NoSuchRevisionInTree:
1743
yield self._get_repository().revision_tree(revision_id)
1745
def _get_file_revision(self, file_id, vf, tree_revision):
1746
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1747
self._iter_parent_trees()]
1748
vf.add_lines((file_id, tree_revision), parent_keys,
1749
self.get_file(file_id).readlines())
1750
repo = self._get_repository()
1751
base_vf = repo.texts
1752
if base_vf not in vf.fallback_versionedfiles:
1753
vf.fallback_versionedfiles.append(base_vf)
1754
return tree_revision
1756
def _stat_limbo_file(self, file_id):
1757
trans_id = self._transform.trans_id_file_id(file_id)
1758
name = self._transform._limbo_name(trans_id)
1759
return os.lstat(name)
1762
def _by_parent(self):
1763
if self.__by_parent is None:
1764
self.__by_parent = self._transform.by_parent()
1765
return self.__by_parent
1767
def _comparison_data(self, entry, path):
1768
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1769
if kind == 'missing':
1773
file_id = self._transform.final_file_id(self._path2trans_id(path))
1774
executable = self.is_executable(file_id, path)
1775
return kind, executable, None
1777
def lock_read(self):
1778
# Perhaps in theory, this should lock the TreeTransform?
1785
def inventory(self):
1786
"""This Tree does not use inventory as its backing data."""
1787
raise NotImplementedError(_PreviewTree.inventory)
1789
def get_root_id(self):
1790
return self._transform.final_file_id(self._transform.root)
1792
def all_file_ids(self):
1793
tree_ids = set(self._transform._tree.all_file_ids())
1794
tree_ids.difference_update(self._transform.tree_file_id(t)
1795
for t in self._transform._removed_id)
1796
tree_ids.update(self._transform._new_id.values())
1800
return iter(self.all_file_ids())
1802
def _has_id(self, file_id, fallback_check):
1803
if file_id in self._transform._r_new_id:
1805
elif file_id in set([self._transform.tree_file_id(trans_id) for
1806
trans_id in self._transform._removed_id]):
1809
return fallback_check(file_id)
1811
def has_id(self, file_id):
1812
return self._has_id(file_id, self._transform._tree.has_id)
1814
def has_or_had_id(self, file_id):
1815
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1817
def _path2trans_id(self, path):
1818
# We must not use None here, because that is a valid value to store.
1819
trans_id = self._path2trans_id_cache.get(path, object)
1820
if trans_id is not object:
1822
segments = splitpath(path)
1823
cur_parent = self._transform.root
1824
for cur_segment in segments:
1825
for child in self._all_children(cur_parent):
1826
final_name = self._final_name_cache.get(child)
1827
if final_name is None:
1828
final_name = self._transform.final_name(child)
1829
self._final_name_cache[child] = final_name
1830
if final_name == cur_segment:
1834
self._path2trans_id_cache[path] = None
1836
self._path2trans_id_cache[path] = cur_parent
1839
def path2id(self, path):
1840
return self._transform.final_file_id(self._path2trans_id(path))
1842
def id2path(self, file_id):
1843
trans_id = self._transform.trans_id_file_id(file_id)
1845
return self._final_paths._determine_path(trans_id)
1847
raise errors.NoSuchId(self, file_id)
1849
def _all_children(self, trans_id):
1850
children = self._all_children_cache.get(trans_id)
1851
if children is not None:
1853
children = set(self._transform.iter_tree_children(trans_id))
1854
# children in the _new_parent set are provided by _by_parent.
1855
children.difference_update(self._transform._new_parent.keys())
1856
children.update(self._by_parent.get(trans_id, []))
1857
self._all_children_cache[trans_id] = children
1860
def iter_children(self, file_id):
1861
trans_id = self._transform.trans_id_file_id(file_id)
1862
for child_trans_id in self._all_children(trans_id):
1863
yield self._transform.final_file_id(child_trans_id)
1866
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1867
in self._transform._tree.extras())
1868
possible_extras.update(self._transform._new_contents)
1869
possible_extras.update(self._transform._removed_id)
1870
for trans_id in possible_extras:
1871
if self._transform.final_file_id(trans_id) is None:
1872
yield self._final_paths._determine_path(trans_id)
1874
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1875
yield_parents=False):
1876
for trans_id, parent_file_id in ordered_entries:
1877
file_id = self._transform.final_file_id(trans_id)
1880
if (specific_file_ids is not None
1881
and file_id not in specific_file_ids):
1884
kind = self._transform.final_kind(trans_id)
1886
kind = self._transform._tree.stored_kind(file_id)
1887
new_entry = inventory.make_entry(
1889
self._transform.final_name(trans_id),
1890
parent_file_id, file_id)
1891
yield new_entry, trans_id
1893
def _list_files_by_dir(self):
1894
todo = [ROOT_PARENT]
1896
while len(todo) > 0:
1898
parent_file_id = self._transform.final_file_id(parent)
1899
children = list(self._all_children(parent))
1900
paths = dict(zip(children, self._final_paths.get_paths(children)))
1901
children.sort(key=paths.get)
1902
todo.extend(reversed(children))
1903
for trans_id in children:
1904
ordered_ids.append((trans_id, parent_file_id))
1907
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1908
# This may not be a maximally efficient implementation, but it is
1909
# reasonably straightforward. An implementation that grafts the
1910
# TreeTransform changes onto the tree's iter_entries_by_dir results
1911
# might be more efficient, but requires tricky inferences about stack
1913
ordered_ids = self._list_files_by_dir()
1914
for entry, trans_id in self._make_inv_entries(ordered_ids,
1915
specific_file_ids, yield_parents=yield_parents):
1916
yield unicode(self._final_paths.get_path(trans_id)), entry
1918
def _iter_entries_for_dir(self, dir_path):
1919
"""Return path, entry for items in a directory without recursing down."""
1920
dir_file_id = self.path2id(dir_path)
1922
for file_id in self.iter_children(dir_file_id):
1923
trans_id = self._transform.trans_id_file_id(file_id)
1924
ordered_ids.append((trans_id, file_id))
1925
for entry, trans_id in self._make_inv_entries(ordered_ids):
1926
yield unicode(self._final_paths.get_path(trans_id)), entry
1928
def list_files(self, include_root=False, from_dir=None, recursive=True):
1929
"""See WorkingTree.list_files."""
1930
# XXX This should behave like WorkingTree.list_files, but is really
1931
# more like RevisionTree.list_files.
1935
prefix = from_dir + '/'
1936
entries = self.iter_entries_by_dir()
1937
for path, entry in entries:
1938
if entry.name == '' and not include_root:
1941
if not path.startswith(prefix):
1943
path = path[len(prefix):]
1944
yield path, 'V', entry.kind, entry.file_id, entry
1946
if from_dir is None and include_root is True:
1947
root_entry = inventory.make_entry('directory', '',
1948
ROOT_PARENT, self.get_root_id())
1949
yield '', 'V', 'directory', root_entry.file_id, root_entry
1950
entries = self._iter_entries_for_dir(from_dir or '')
1951
for path, entry in entries:
1952
yield path, 'V', entry.kind, entry.file_id, entry
1954
def kind(self, file_id):
1955
trans_id = self._transform.trans_id_file_id(file_id)
1956
return self._transform.final_kind(trans_id)
1958
def stored_kind(self, file_id):
1959
trans_id = self._transform.trans_id_file_id(file_id)
1961
return self._transform._new_contents[trans_id]
1963
return self._transform._tree.stored_kind(file_id)
1965
def get_file_mtime(self, file_id, path=None):
1966
"""See Tree.get_file_mtime"""
1967
if not self._content_change(file_id):
1968
return self._transform._tree.get_file_mtime(file_id, path)
1969
return self._stat_limbo_file(file_id).st_mtime
1971
def _file_size(self, entry, stat_value):
1972
return self.get_file_size(entry.file_id)
1974
def get_file_size(self, file_id):
1975
"""See Tree.get_file_size"""
1976
if self.kind(file_id) == 'file':
1977
return self._transform._tree.get_file_size(file_id)
1981
def get_file_sha1(self, file_id, path=None, stat_value=None):
1982
trans_id = self._transform.trans_id_file_id(file_id)
1983
kind = self._transform._new_contents.get(trans_id)
1985
return self._transform._tree.get_file_sha1(file_id)
1987
fileobj = self.get_file(file_id)
1989
return sha_file(fileobj)
1993
def is_executable(self, file_id, path=None):
1996
trans_id = self._transform.trans_id_file_id(file_id)
1998
return self._transform._new_executability[trans_id]
2001
return self._transform._tree.is_executable(file_id, path)
2003
if e.errno == errno.ENOENT:
2006
except errors.NoSuchId:
2009
def path_content_summary(self, path):
2010
trans_id = self._path2trans_id(path)
2011
tt = self._transform
2012
tree_path = tt._tree_id_paths.get(trans_id)
2013
kind = tt._new_contents.get(trans_id)
2015
if tree_path is None or trans_id in tt._removed_contents:
2016
return 'missing', None, None, None
2017
summary = tt._tree.path_content_summary(tree_path)
2018
kind, size, executable, link_or_sha1 = summary
2021
limbo_name = tt._limbo_name(trans_id)
2022
if trans_id in tt._new_reference_revision:
2023
kind = 'tree-reference'
2025
statval = os.lstat(limbo_name)
2026
size = statval.st_size
2027
if not supports_executable():
2030
executable = statval.st_mode & S_IEXEC
2034
if kind == 'symlink':
2035
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2036
if supports_executable():
2037
executable = tt._new_executability.get(trans_id, executable)
2038
return kind, size, executable, link_or_sha1
2040
def iter_changes(self, from_tree, include_unchanged=False,
2041
specific_files=None, pb=None, extra_trees=None,
2042
require_versioned=True, want_unversioned=False):
2043
"""See InterTree.iter_changes.
2045
This has a fast path that is only used when the from_tree matches
2046
the transform tree, and no fancy options are supplied.
2048
if (from_tree is not self._transform._tree or include_unchanged or
2049
specific_files or want_unversioned):
2050
return tree.InterTree(from_tree, self).iter_changes(
2051
include_unchanged=include_unchanged,
2052
specific_files=specific_files,
2054
extra_trees=extra_trees,
2055
require_versioned=require_versioned,
2056
want_unversioned=want_unversioned)
2057
if want_unversioned:
2058
raise ValueError('want_unversioned is not supported')
2059
return self._transform.iter_changes()
2061
def get_file(self, file_id, path=None):
2062
"""See Tree.get_file"""
2063
if not self._content_change(file_id):
2064
return self._transform._tree.get_file(file_id, path)
2065
trans_id = self._transform.trans_id_file_id(file_id)
2066
name = self._transform._limbo_name(trans_id)
2067
return open(name, 'rb')
2069
def get_file_with_stat(self, file_id, path=None):
2070
return self.get_file(file_id, path), None
2072
def annotate_iter(self, file_id,
2073
default_revision=_mod_revision.CURRENT_REVISION):
2074
changes = self._iter_changes_cache.get(file_id)
2078
changed_content, versioned, kind = (changes[2], changes[3],
2082
get_old = (kind[0] == 'file' and versioned[0])
2084
old_annotation = self._transform._tree.annotate_iter(file_id,
2085
default_revision=default_revision)
2089
return old_annotation
2090
if not changed_content:
2091
return old_annotation
2092
# TODO: This is doing something similar to what WT.annotate_iter is
2093
# doing, however it fails slightly because it doesn't know what
2094
# the *other* revision_id is, so it doesn't know how to give the
2095
# other as the origin for some lines, they all get
2096
# 'default_revision'
2097
# It would be nice to be able to use the new Annotator based
2098
# approach, as well.
2099
return annotate.reannotate([old_annotation],
2100
self.get_file(file_id).readlines(),
2103
def get_symlink_target(self, file_id):
2104
"""See Tree.get_symlink_target"""
2105
if not self._content_change(file_id):
2106
return self._transform._tree.get_symlink_target(file_id)
2107
trans_id = self._transform.trans_id_file_id(file_id)
2108
name = self._transform._limbo_name(trans_id)
2109
return osutils.readlink(name)
2111
def walkdirs(self, prefix=''):
2112
pending = [self._transform.root]
2113
while len(pending) > 0:
2114
parent_id = pending.pop()
2117
prefix = prefix.rstrip('/')
2118
parent_path = self._final_paths.get_path(parent_id)
2119
parent_file_id = self._transform.final_file_id(parent_id)
2120
for child_id in self._all_children(parent_id):
2121
path_from_root = self._final_paths.get_path(child_id)
2122
basename = self._transform.final_name(child_id)
2123
file_id = self._transform.final_file_id(child_id)
2125
kind = self._transform.final_kind(child_id)
2126
versioned_kind = kind
2129
versioned_kind = self._transform._tree.stored_kind(file_id)
2130
if versioned_kind == 'directory':
2131
subdirs.append(child_id)
2132
children.append((path_from_root, basename, kind, None,
2133
file_id, versioned_kind))
2135
if parent_path.startswith(prefix):
2136
yield (parent_path, parent_file_id), children
2137
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2140
def get_parent_ids(self):
2141
return self._parent_ids
2143
def set_parent_ids(self, parent_ids):
2144
self._parent_ids = parent_ids
2146
def get_revision_tree(self, revision_id):
2147
return self._transform._tree.get_revision_tree(revision_id)
1170
2150
def joinpath(parent, child):
1171
2151
"""Join tree-relative paths, handling the tree root specially"""