895
1173
(from_executable, to_executable)))
896
1174
return iter(sorted(results, key=lambda x:x[1]))
898
def get_preview_tree(self):
899
"""Return a tree representing the result of the transform.
901
The tree is a snapshot, and altering the TreeTransform will invalidate
904
return _PreviewTree(self)
906
def commit(self, branch, message, merge_parents=None, strict=False,
907
timestamp=None, timezone=None, committer=None, authors=None,
908
revprops=None, revision_id=None):
909
"""Commit the result of this TreeTransform to a branch.
911
:param branch: The branch to commit to.
912
:param message: The message to attach to the commit.
913
:param merge_parents: Additional parent revision-ids specified by
915
:param strict: If True, abort the commit if there are unversioned
917
:param timestamp: if not None, seconds-since-epoch for the time and
918
date. (May be a float.)
919
:param timezone: Optional timezone for timestamp, as an offset in
921
:param committer: Optional committer in email-id format.
922
(e.g. "J Random Hacker <jrandom@example.com>")
923
:param authors: Optional list of authors in email-id format.
924
:param revprops: Optional dictionary of revision properties.
925
:param revision_id: Optional revision id. (Specifying a revision-id
926
may reduce performance for some non-native formats.)
927
:return: The revision_id of the revision committed.
929
self._check_malformed()
931
unversioned = set(self._new_contents).difference(set(self._new_id))
932
for trans_id in unversioned:
933
if self.final_file_id(trans_id) is None:
934
raise errors.StrictCommitFailed()
936
revno, last_rev_id = branch.last_revision_info()
937
if last_rev_id == _mod_revision.NULL_REVISION:
938
if merge_parents is not None:
939
raise ValueError('Cannot supply merge parents for first'
943
parent_ids = [last_rev_id]
944
if merge_parents is not None:
945
parent_ids.extend(merge_parents)
946
if self._tree.get_revision_id() != last_rev_id:
947
raise ValueError('TreeTransform not based on branch basis: %s' %
948
self._tree.get_revision_id())
949
revprops = commit.Commit.update_revprops(revprops, branch, authors)
950
builder = branch.get_commit_builder(parent_ids,
955
revision_id=revision_id)
956
preview = self.get_preview_tree()
957
list(builder.record_iter_changes(preview, last_rev_id,
958
self.iter_changes()))
959
builder.finish_inventory()
960
revision_id = builder.commit(message)
961
branch.set_last_revision_info(revno + 1, revision_id)
964
def _text_parent(self, trans_id):
965
file_id = self.tree_file_id(trans_id)
967
if file_id is None or self._tree.kind(file_id) != 'file':
969
except errors.NoSuchFile:
973
def _get_parents_texts(self, trans_id):
974
"""Get texts for compression parents of this file."""
975
file_id = self._text_parent(trans_id)
978
return (self._tree.get_file_text(file_id),)
980
def _get_parents_lines(self, trans_id):
981
"""Get lines for compression parents of this file."""
982
file_id = self._text_parent(trans_id)
985
return (self._tree.get_file_lines(file_id),)
987
def serialize(self, serializer):
988
"""Serialize this TreeTransform.
990
:param serializer: A Serialiser like pack.ContainerSerializer.
992
new_name = dict((k, v.encode('utf-8')) for k, v in
993
self._new_name.items())
994
new_executability = dict((k, int(v)) for k, v in
995
self._new_executability.items())
996
tree_path_ids = dict((k.encode('utf-8'), v)
997
for k, v in self._tree_path_ids.items())
999
'_id_number': self._id_number,
1000
'_new_name': new_name,
1001
'_new_parent': self._new_parent,
1002
'_new_executability': new_executability,
1003
'_new_id': self._new_id,
1004
'_tree_path_ids': tree_path_ids,
1005
'_removed_id': list(self._removed_id),
1006
'_removed_contents': list(self._removed_contents),
1007
'_non_present_ids': self._non_present_ids,
1009
yield serializer.bytes_record(bencode.bencode(attribs),
1011
for trans_id, kind in self._new_contents.items():
1013
lines = osutils.chunks_to_lines(
1014
self._read_file_chunks(trans_id))
1015
parents = self._get_parents_lines(trans_id)
1016
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1017
content = ''.join(mpdiff.to_patch())
1018
if kind == 'directory':
1020
if kind == 'symlink':
1021
content = self._read_symlink_target(trans_id)
1022
yield serializer.bytes_record(content, ((trans_id, kind),))
1024
def deserialize(self, records):
1025
"""Deserialize a stored TreeTransform.
1027
:param records: An iterable of (names, content) tuples, as per
1028
pack.ContainerPushParser.
1030
names, content = records.next()
1031
attribs = bencode.bdecode(content)
1032
self._id_number = attribs['_id_number']
1033
self._new_name = dict((k, v.decode('utf-8'))
1034
for k, v in attribs['_new_name'].items())
1035
self._new_parent = attribs['_new_parent']
1036
self._new_executability = dict((k, bool(v)) for k, v in
1037
attribs['_new_executability'].items())
1038
self._new_id = attribs['_new_id']
1039
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1040
self._tree_path_ids = {}
1041
self._tree_id_paths = {}
1042
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1043
path = bytepath.decode('utf-8')
1044
self._tree_path_ids[path] = trans_id
1045
self._tree_id_paths[trans_id] = path
1046
self._removed_id = set(attribs['_removed_id'])
1047
self._removed_contents = set(attribs['_removed_contents'])
1048
self._non_present_ids = attribs['_non_present_ids']
1049
for ((trans_id, kind),), content in records:
1051
mpdiff = multiparent.MultiParent.from_patch(content)
1052
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1053
self.create_file(lines, trans_id)
1054
if kind == 'directory':
1055
self.create_directory(trans_id)
1056
if kind == 'symlink':
1057
self.create_symlink(content.decode('utf-8'), trans_id)
1060
class DiskTreeTransform(TreeTransformBase):
1061
"""Tree transform storing its contents on disk."""
1063
def __init__(self, tree, limbodir, pb=None,
1064
case_sensitive=True):
1066
:param tree: The tree that will be transformed, but not necessarily
1068
:param limbodir: A directory where new files can be stored until
1069
they are installed in their proper places
1071
:param case_sensitive: If True, the target of the transform is
1072
case sensitive, not just case preserving.
1074
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1075
self._limbodir = limbodir
1076
self._deletiondir = None
1077
# A mapping of transform ids to their limbo filename
1078
self._limbo_files = {}
1079
# A mapping of transform ids to a set of the transform ids of children
1080
# that their limbo directory has
1081
self._limbo_children = {}
1082
# Map transform ids to maps of child filename to child transform id
1083
self._limbo_children_names = {}
1084
# List of transform ids that need to be renamed from limbo into place
1085
self._needs_rename = set()
1086
self._creation_mtime = None
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)
1161
for descendant in self._limbo_descendants(trans_id):
1162
desc_path = self._limbo_files[descendant]
1163
desc_path = new_path + desc_path[len(old_path):]
1164
self._limbo_files[descendant] = desc_path
1166
def _limbo_descendants(self, trans_id):
1167
"""Return the set of trans_ids whose limbo paths descend from this."""
1168
descendants = set(self._limbo_children.get(trans_id, []))
1169
for descendant in list(descendants):
1170
descendants.update(self._limbo_descendants(descendant))
1173
def create_file(self, contents, trans_id, mode_id=None):
1174
"""Schedule creation of a new file.
1178
Contents is an iterator of strings, all of which will be written
1179
to the target destination.
1181
New file takes the permissions of any existing file with that id,
1182
unless mode_id is specified.
1184
name = self._limbo_name(trans_id)
1185
f = open(name, 'wb')
1188
unique_add(self._new_contents, trans_id, 'file')
1190
# Clean up the file, it never got registered so
1191
# TreeTransform.finalize() won't clean it up.
1196
f.writelines(contents)
1199
self._set_mtime(name)
1200
self._set_mode(trans_id, mode_id, S_ISREG)
1202
def _read_file_chunks(self, trans_id):
1203
cur_file = open(self._limbo_name(trans_id), 'rb')
1205
return cur_file.readlines()
1209
def _read_symlink_target(self, trans_id):
1210
return os.readlink(self._limbo_name(trans_id))
1212
def _set_mtime(self, path):
1213
"""All files that are created get the same mtime.
1215
This time is set by the first object to be created.
1217
if self._creation_mtime is None:
1218
self._creation_mtime = time.time()
1219
os.utime(path, (self._creation_mtime, self._creation_mtime))
1221
def create_hardlink(self, path, trans_id):
1222
"""Schedule creation of a hard link"""
1223
name = self._limbo_name(trans_id)
1227
if e.errno != errno.EPERM:
1229
raise errors.HardLinkNotSupported(path)
1231
unique_add(self._new_contents, trans_id, 'file')
1233
# Clean up the file, it never got registered so
1234
# TreeTransform.finalize() won't clean it up.
1238
def create_directory(self, trans_id):
1239
"""Schedule creation of a new directory.
1241
See also new_directory.
1243
os.mkdir(self._limbo_name(trans_id))
1244
unique_add(self._new_contents, trans_id, 'directory')
1246
def create_symlink(self, target, trans_id):
1247
"""Schedule creation of a new symbolic link.
1249
target is a bytestring.
1250
See also new_symlink.
1253
os.symlink(target, self._limbo_name(trans_id))
1254
unique_add(self._new_contents, trans_id, 'symlink')
1257
path = FinalPaths(self).get_path(trans_id)
1260
raise UnableCreateSymlink(path=path)
1262
def cancel_creation(self, trans_id):
1263
"""Cancel the creation of new file contents."""
1264
del self._new_contents[trans_id]
1265
children = self._limbo_children.get(trans_id)
1266
# if this is a limbo directory with children, move them before removing
1268
if children is not None:
1269
self._rename_in_limbo(children)
1270
del self._limbo_children[trans_id]
1271
del self._limbo_children_names[trans_id]
1272
delete_any(self._limbo_name(trans_id))
1275
class TreeTransform(DiskTreeTransform):
1276
"""Represent a tree transformation.
1278
This object is designed to support incremental generation of the transform,
1281
However, it gives optimum performance when parent directories are created
1282
before their contents. The transform is then able to put child files
1283
directly in their parent directory, avoiding later renames.
1285
It is easy to produce malformed transforms, but they are generally
1286
harmless. Attempting to apply a malformed transform will cause an
1287
exception to be raised before any modifications are made to the tree.
1289
Many kinds of malformed transforms can be corrected with the
1290
resolve_conflicts function. The remaining ones indicate programming error,
1291
such as trying to create a file with no path.
1293
Two sets of file creation methods are supplied. Convenience methods are:
1298
These are composed of the low-level methods:
1300
* create_file or create_directory or create_symlink
1304
Transform/Transaction ids
1305
-------------------------
1306
trans_ids are temporary ids assigned to all files involved in a transform.
1307
It's possible, even common, that not all files in the Tree have trans_ids.
1309
trans_ids are used because filenames and file_ids are not good enough
1310
identifiers; filenames change, and not all files have file_ids. File-ids
1311
are also associated with trans-ids, so that moving a file moves its
1314
trans_ids are only valid for the TreeTransform that generated them.
1318
Limbo is a temporary directory use to hold new versions of files.
1319
Files are added to limbo by create_file, create_directory, create_symlink,
1320
and their convenience variants (new_*). Files may be removed from limbo
1321
using cancel_creation. Files are renamed from limbo into their final
1322
location as part of TreeTransform.apply
1324
Limbo must be cleaned up, by either calling TreeTransform.apply or
1325
calling TreeTransform.finalize.
1327
Files are placed into limbo inside their parent directories, where
1328
possible. This reduces subsequent renames, and makes operations involving
1329
lots of files faster. This optimization is only possible if the parent
1330
directory is created *before* creating any of its children, so avoid
1331
creating children before parents, where possible.
1335
This temporary directory is used by _FileMover for storing files that are
1336
about to be deleted. In case of rollback, the files will be restored.
1337
FileMover does not delete files until it is sure that a rollback will not
1340
def __init__(self, tree, pb=None):
1341
"""Note: a tree_write lock is taken on the tree.
1343
Use TreeTransform.finalize() to release the lock (can be omitted if
1344
TreeTransform.apply() called).
1346
tree.lock_tree_write()
1349
limbodir = urlutils.local_path_from_url(
1350
tree._transport.abspath('limbo'))
1354
if e.errno == errno.EEXIST:
1355
raise ExistingLimbo(limbodir)
1356
deletiondir = urlutils.local_path_from_url(
1357
tree._transport.abspath('pending-deletion'))
1359
os.mkdir(deletiondir)
1361
if e.errno == errno.EEXIST:
1362
raise errors.ExistingPendingDeletion(deletiondir)
1367
# Cache of realpath results, to speed up canonical_path
1368
self._realpaths = {}
1369
# Cache of relpath results, to speed up canonical_path
1371
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1372
tree.case_sensitive)
1373
self._deletiondir = deletiondir
1375
def canonical_path(self, path):
1376
"""Get the canonical tree-relative path"""
1377
# don't follow final symlinks
1378
abs = self._tree.abspath(path)
1379
if abs in self._relpaths:
1380
return self._relpaths[abs]
1381
dirname, basename = os.path.split(abs)
1382
if dirname not in self._realpaths:
1383
self._realpaths[dirname] = os.path.realpath(dirname)
1384
dirname = self._realpaths[dirname]
1385
abs = pathjoin(dirname, basename)
1386
if dirname in self._relpaths:
1387
relpath = pathjoin(self._relpaths[dirname], basename)
1388
relpath = relpath.rstrip('/\\')
1390
relpath = self._tree.relpath(abs)
1391
self._relpaths[abs] = relpath
1394
def tree_kind(self, trans_id):
1395
"""Determine the file kind in the working tree.
1397
:returns: The file kind or None if the file does not exist
1399
path = self._tree_id_paths.get(trans_id)
1403
return file_kind(self._tree.abspath(path))
1404
except errors.NoSuchFile:
1407
def _set_mode(self, trans_id, mode_id, typefunc):
1408
"""Set the mode of new file contents.
1409
The mode_id is the existing file to get the mode from (often the same
1410
as trans_id). The operation is only performed if there's a mode match
1411
according to typefunc.
1416
old_path = self._tree_id_paths[mode_id]
1420
mode = os.stat(self._tree.abspath(old_path)).st_mode
1422
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1423
# Either old_path doesn't exist, or the parent of the
1424
# target is not a directory (but will be one eventually)
1425
# Either way, we know it doesn't exist *right now*
1426
# See also bug #248448
1431
os.chmod(self._limbo_name(trans_id), mode)
1433
def iter_tree_children(self, parent_id):
1434
"""Iterate through the entry's tree children, if any"""
1436
path = self._tree_id_paths[parent_id]
1440
children = os.listdir(self._tree.abspath(path))
1442
if not (osutils._is_error_enotdir(e)
1443
or e.errno in (errno.ENOENT, errno.ESRCH)):
1447
for child in children:
1448
childpath = joinpath(path, child)
1449
if self._tree.is_control_filename(childpath):
1451
yield self.trans_id_tree_path(childpath)
1453
def _generate_limbo_path(self, trans_id):
1454
"""Generate a limbo path using the final path if possible.
1456
This optimizes the performance of applying the tree transform by
1457
avoiding renames. These renames can be avoided only when the parent
1458
directory is already scheduled for creation.
1460
If the final path cannot be used, falls back to using the trans_id as
1463
parent = self._new_parent.get(trans_id)
1464
# if the parent directory is already in limbo (e.g. when building a
1465
# tree), choose a limbo name inside the parent, to reduce further
1467
use_direct_path = False
1468
if self._new_contents.get(parent) == 'directory':
1469
filename = self._new_name.get(trans_id)
1470
if filename is not None:
1471
if parent not in self._limbo_children:
1472
self._limbo_children[parent] = set()
1473
self._limbo_children_names[parent] = {}
1474
use_direct_path = True
1475
# the direct path can only be used if no other file has
1476
# already taken this pathname, i.e. if the name is unused, or
1477
# if it is already associated with this trans_id.
1478
elif self._case_sensitive_target:
1479
if (self._limbo_children_names[parent].get(filename)
1480
in (trans_id, None)):
1481
use_direct_path = True
1483
for l_filename, l_trans_id in\
1484
self._limbo_children_names[parent].iteritems():
1485
if l_trans_id == trans_id:
1487
if l_filename.lower() == filename.lower():
1490
use_direct_path = True
1492
if not use_direct_path:
1493
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1495
limbo_name = pathjoin(self._limbo_files[parent], filename)
1496
self._limbo_children[parent].add(trans_id)
1497
self._limbo_children_names[parent][filename] = trans_id
1501
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1502
"""Apply all changes to the inventory and filesystem.
1504
If filesystem or inventory conflicts are present, MalformedTransform
1507
If apply succeeds, finalize is not necessary.
1509
:param no_conflicts: if True, the caller guarantees there are no
1510
conflicts, so no check is made.
1511
:param precomputed_delta: An inventory delta to use instead of
1513
:param _mover: Supply an alternate FileMover, for testing
1515
if not no_conflicts:
1516
self._check_malformed()
1517
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1519
if precomputed_delta is None:
1520
child_pb.update('Apply phase', 0, 2)
1521
inventory_delta = self._generate_inventory_delta()
1524
inventory_delta = precomputed_delta
1527
mover = _FileMover()
1531
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1532
self._apply_removals(mover)
1533
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1534
modified_paths = self._apply_insertions(mover)
1539
mover.apply_deletions()
1542
self._tree.apply_inventory_delta(inventory_delta)
1545
return _TransformResults(modified_paths, self.rename_count)
1547
def _generate_inventory_delta(self):
1548
"""Generate an inventory delta for the current transform."""
1549
inventory_delta = []
1550
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1551
new_paths = self._inventory_altered()
1552
total_entries = len(new_paths) + len(self._removed_id)
1554
for num, trans_id in enumerate(self._removed_id):
1556
child_pb.update('removing file', num, total_entries)
1557
if trans_id == self._new_root:
1558
file_id = self._tree.get_root_id()
1560
file_id = self.tree_file_id(trans_id)
1561
# File-id isn't really being deleted, just moved
1562
if file_id in self._r_new_id:
1564
path = self._tree_id_paths[trans_id]
1565
inventory_delta.append((path, None, file_id, None))
1566
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1568
entries = self._tree.iter_entries_by_dir(
1569
new_path_file_ids.values())
1570
old_paths = dict((e.file_id, p) for p, e in entries)
1572
for num, (path, trans_id) in enumerate(new_paths):
1574
child_pb.update('adding file',
1575
num + len(self._removed_id), total_entries)
1576
file_id = new_path_file_ids[trans_id]
1580
kind = self.final_kind(trans_id)
1582
kind = self._tree.stored_kind(file_id)
1583
parent_trans_id = self.final_parent(trans_id)
1584
parent_file_id = new_path_file_ids.get(parent_trans_id)
1585
if parent_file_id is None:
1586
parent_file_id = self.final_file_id(parent_trans_id)
1587
if trans_id in self._new_reference_revision:
1588
new_entry = inventory.TreeReference(
1590
self._new_name[trans_id],
1591
self.final_file_id(self._new_parent[trans_id]),
1592
None, self._new_reference_revision[trans_id])
1594
new_entry = inventory.make_entry(kind,
1595
self.final_name(trans_id),
1596
parent_file_id, file_id)
1597
old_path = old_paths.get(new_entry.file_id)
1598
new_executability = self._new_executability.get(trans_id)
1599
if new_executability is not None:
1600
new_entry.executable = new_executability
1601
inventory_delta.append(
1602
(old_path, path, new_entry.file_id, new_entry))
1605
return inventory_delta
1607
def _apply_removals(self, mover):
1608
"""Perform tree operations that remove directory/inventory names.
1610
That is, delete files that are to be deleted, and put any files that
1611
need renaming into limbo. This must be done in strict child-to-parent
1614
If inventory_delta is None, no inventory delta generation is performed.
1616
tree_paths = list(self._tree_path_ids.iteritems())
1617
tree_paths.sort(reverse=True)
1618
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1620
for num, data in enumerate(tree_paths):
1621
path, trans_id = data
1622
child_pb.update('removing file', num, len(tree_paths))
1623
full_path = self._tree.abspath(path)
1624
if trans_id in self._removed_contents:
1625
delete_path = os.path.join(self._deletiondir, trans_id)
1626
mover.pre_delete(full_path, delete_path)
1627
elif (trans_id in self._new_name
1628
or trans_id in self._new_parent):
1630
mover.rename(full_path, self._limbo_name(trans_id))
1631
except errors.TransformRenameFailed, e:
1632
if e.errno != errno.ENOENT:
1635
self.rename_count += 1
1639
def _apply_insertions(self, mover):
1640
"""Perform tree operations that insert directory/inventory names.
1642
That is, create any files that need to be created, and restore from
1643
limbo any files that needed renaming. This must be done in strict
1644
parent-to-child order.
1646
If inventory_delta is None, no inventory delta is calculated, and
1647
no list of modified paths is returned.
1649
new_paths = self.new_paths(filesystem_only=True)
1651
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1653
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1655
for num, (path, trans_id) in enumerate(new_paths):
1657
child_pb.update('adding file', num, len(new_paths))
1658
full_path = self._tree.abspath(path)
1659
if trans_id in self._needs_rename:
1661
mover.rename(self._limbo_name(trans_id), full_path)
1662
except errors.TransformRenameFailed, e:
1663
# We may be renaming a dangling inventory id
1664
if e.errno != errno.ENOENT:
1667
self.rename_count += 1
1668
if (trans_id in self._new_contents or
1669
self.path_changed(trans_id)):
1670
if trans_id in self._new_contents:
1671
modified_paths.append(full_path)
1672
if trans_id in self._new_executability:
1673
self._set_executability(path, trans_id)
1676
self._new_contents.clear()
1677
return modified_paths
1680
class TransformPreview(DiskTreeTransform):
1681
"""A TreeTransform for generating preview trees.
1683
Unlike TreeTransform, this version works when the input tree is a
1684
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1685
unversioned files in the input tree.
1688
def __init__(self, tree, pb=None, case_sensitive=True):
1690
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1691
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1693
def canonical_path(self, path):
1696
def tree_kind(self, trans_id):
1697
path = self._tree_id_paths.get(trans_id)
1700
file_id = self._tree.path2id(path)
1702
return self._tree.kind(file_id)
1703
except errors.NoSuchFile:
1706
def _set_mode(self, trans_id, mode_id, typefunc):
1707
"""Set the mode of new file contents.
1708
The mode_id is the existing file to get the mode from (often the same
1709
as trans_id). The operation is only performed if there's a mode match
1710
according to typefunc.
1712
# is it ok to ignore this? probably
1715
def iter_tree_children(self, parent_id):
1716
"""Iterate through the entry's tree children, if any"""
1718
path = self._tree_id_paths[parent_id]
1721
file_id = self.tree_file_id(parent_id)
1724
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1725
children = getattr(entry, 'children', {})
1726
for child in children:
1727
childpath = joinpath(path, child)
1728
yield self.trans_id_tree_path(childpath)
1731
class _PreviewTree(tree.Tree):
1732
"""Partial implementation of Tree to support show_diff_trees"""
1734
def __init__(self, transform):
1735
self._transform = transform
1736
self._final_paths = FinalPaths(transform)
1737
self.__by_parent = None
1738
self._parent_ids = []
1739
self._all_children_cache = {}
1740
self._path2trans_id_cache = {}
1741
self._final_name_cache = {}
1742
self._iter_changes_cache = dict((c[0], c) for c in
1743
self._transform.iter_changes())
1745
def _content_change(self, file_id):
1746
"""Return True if the content of this file changed"""
1747
changes = self._iter_changes_cache.get(file_id)
1748
# changes[2] is true if the file content changed. See
1749
# InterTree.iter_changes.
1750
return (changes is not None and changes[2])
1752
def _get_repository(self):
1753
repo = getattr(self._transform._tree, '_repository', None)
1755
repo = self._transform._tree.branch.repository
1758
def _iter_parent_trees(self):
1759
for revision_id in self.get_parent_ids():
1761
yield self.revision_tree(revision_id)
1762
except errors.NoSuchRevisionInTree:
1763
yield self._get_repository().revision_tree(revision_id)
1765
def _get_file_revision(self, file_id, vf, tree_revision):
1766
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1767
self._iter_parent_trees()]
1768
vf.add_lines((file_id, tree_revision), parent_keys,
1769
self.get_file_lines(file_id))
1770
repo = self._get_repository()
1771
base_vf = repo.texts
1772
if base_vf not in vf.fallback_versionedfiles:
1773
vf.fallback_versionedfiles.append(base_vf)
1774
return tree_revision
1776
def _stat_limbo_file(self, file_id):
1777
trans_id = self._transform.trans_id_file_id(file_id)
1778
name = self._transform._limbo_name(trans_id)
1779
return os.lstat(name)
1782
def _by_parent(self):
1783
if self.__by_parent is None:
1784
self.__by_parent = self._transform.by_parent()
1785
return self.__by_parent
1787
def _comparison_data(self, entry, path):
1788
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1789
if kind == 'missing':
1793
file_id = self._transform.final_file_id(self._path2trans_id(path))
1794
executable = self.is_executable(file_id, path)
1795
return kind, executable, None
1797
def is_locked(self):
1800
def lock_read(self):
1801
# Perhaps in theory, this should lock the TreeTransform?
1808
def inventory(self):
1809
"""This Tree does not use inventory as its backing data."""
1810
raise NotImplementedError(_PreviewTree.inventory)
1812
def get_root_id(self):
1813
return self._transform.final_file_id(self._transform.root)
1815
def all_file_ids(self):
1816
tree_ids = set(self._transform._tree.all_file_ids())
1817
tree_ids.difference_update(self._transform.tree_file_id(t)
1818
for t in self._transform._removed_id)
1819
tree_ids.update(self._transform._new_id.values())
1823
return iter(self.all_file_ids())
1825
def _has_id(self, file_id, fallback_check):
1826
if file_id in self._transform._r_new_id:
1828
elif file_id in set([self._transform.tree_file_id(trans_id) for
1829
trans_id in self._transform._removed_id]):
1832
return fallback_check(file_id)
1834
def has_id(self, file_id):
1835
return self._has_id(file_id, self._transform._tree.has_id)
1837
def has_or_had_id(self, file_id):
1838
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1840
def _path2trans_id(self, path):
1841
# We must not use None here, because that is a valid value to store.
1842
trans_id = self._path2trans_id_cache.get(path, object)
1843
if trans_id is not object:
1845
segments = splitpath(path)
1846
cur_parent = self._transform.root
1847
for cur_segment in segments:
1848
for child in self._all_children(cur_parent):
1849
final_name = self._final_name_cache.get(child)
1850
if final_name is None:
1851
final_name = self._transform.final_name(child)
1852
self._final_name_cache[child] = final_name
1853
if final_name == cur_segment:
1857
self._path2trans_id_cache[path] = None
1859
self._path2trans_id_cache[path] = cur_parent
1862
def path2id(self, path):
1863
return self._transform.final_file_id(self._path2trans_id(path))
1865
def id2path(self, file_id):
1866
trans_id = self._transform.trans_id_file_id(file_id)
1868
return self._final_paths._determine_path(trans_id)
1870
raise errors.NoSuchId(self, file_id)
1872
def _all_children(self, trans_id):
1873
children = self._all_children_cache.get(trans_id)
1874
if children is not None:
1876
children = set(self._transform.iter_tree_children(trans_id))
1877
# children in the _new_parent set are provided by _by_parent.
1878
children.difference_update(self._transform._new_parent.keys())
1879
children.update(self._by_parent.get(trans_id, []))
1880
self._all_children_cache[trans_id] = children
1883
def iter_children(self, file_id):
1884
trans_id = self._transform.trans_id_file_id(file_id)
1885
for child_trans_id in self._all_children(trans_id):
1886
yield self._transform.final_file_id(child_trans_id)
1889
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1890
in self._transform._tree.extras())
1891
possible_extras.update(self._transform._new_contents)
1892
possible_extras.update(self._transform._removed_id)
1893
for trans_id in possible_extras:
1894
if self._transform.final_file_id(trans_id) is None:
1895
yield self._final_paths._determine_path(trans_id)
1897
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1898
yield_parents=False):
1899
for trans_id, parent_file_id in ordered_entries:
1900
file_id = self._transform.final_file_id(trans_id)
1903
if (specific_file_ids is not None
1904
and file_id not in specific_file_ids):
1906
kind = self._transform.final_kind(trans_id)
1908
kind = self._transform._tree.stored_kind(file_id)
1909
new_entry = inventory.make_entry(
1911
self._transform.final_name(trans_id),
1912
parent_file_id, file_id)
1913
yield new_entry, trans_id
1915
def _list_files_by_dir(self):
1916
todo = [ROOT_PARENT]
1918
while len(todo) > 0:
1920
parent_file_id = self._transform.final_file_id(parent)
1921
children = list(self._all_children(parent))
1922
paths = dict(zip(children, self._final_paths.get_paths(children)))
1923
children.sort(key=paths.get)
1924
todo.extend(reversed(children))
1925
for trans_id in children:
1926
ordered_ids.append((trans_id, parent_file_id))
1929
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1930
# This may not be a maximally efficient implementation, but it is
1931
# reasonably straightforward. An implementation that grafts the
1932
# TreeTransform changes onto the tree's iter_entries_by_dir results
1933
# might be more efficient, but requires tricky inferences about stack
1935
ordered_ids = self._list_files_by_dir()
1936
for entry, trans_id in self._make_inv_entries(ordered_ids,
1937
specific_file_ids, yield_parents=yield_parents):
1938
yield unicode(self._final_paths.get_path(trans_id)), entry
1940
def _iter_entries_for_dir(self, dir_path):
1941
"""Return path, entry for items in a directory without recursing down."""
1942
dir_file_id = self.path2id(dir_path)
1944
for file_id in self.iter_children(dir_file_id):
1945
trans_id = self._transform.trans_id_file_id(file_id)
1946
ordered_ids.append((trans_id, file_id))
1947
for entry, trans_id in self._make_inv_entries(ordered_ids):
1948
yield unicode(self._final_paths.get_path(trans_id)), entry
1950
def list_files(self, include_root=False, from_dir=None, recursive=True):
1951
"""See WorkingTree.list_files."""
1952
# XXX This should behave like WorkingTree.list_files, but is really
1953
# more like RevisionTree.list_files.
1957
prefix = from_dir + '/'
1958
entries = self.iter_entries_by_dir()
1959
for path, entry in entries:
1960
if entry.name == '' and not include_root:
1963
if not path.startswith(prefix):
1965
path = path[len(prefix):]
1966
yield path, 'V', entry.kind, entry.file_id, entry
1968
if from_dir is None and include_root is True:
1969
root_entry = inventory.make_entry('directory', '',
1970
ROOT_PARENT, self.get_root_id())
1971
yield '', 'V', 'directory', root_entry.file_id, root_entry
1972
entries = self._iter_entries_for_dir(from_dir or '')
1973
for path, entry in entries:
1974
yield path, 'V', entry.kind, entry.file_id, entry
1976
def kind(self, file_id):
1977
trans_id = self._transform.trans_id_file_id(file_id)
1978
return self._transform.final_kind(trans_id)
1980
def stored_kind(self, file_id):
1981
trans_id = self._transform.trans_id_file_id(file_id)
1983
return self._transform._new_contents[trans_id]
1985
return self._transform._tree.stored_kind(file_id)
1987
def get_file_mtime(self, file_id, path=None):
1988
"""See Tree.get_file_mtime"""
1989
if not self._content_change(file_id):
1990
return self._transform._tree.get_file_mtime(file_id)
1991
return self._stat_limbo_file(file_id).st_mtime
1993
def _file_size(self, entry, stat_value):
1994
return self.get_file_size(entry.file_id)
1996
def get_file_size(self, file_id):
1997
"""See Tree.get_file_size"""
1998
if self.kind(file_id) == 'file':
1999
return self._transform._tree.get_file_size(file_id)
2003
def get_file_sha1(self, file_id, path=None, stat_value=None):
2004
trans_id = self._transform.trans_id_file_id(file_id)
2005
kind = self._transform._new_contents.get(trans_id)
2007
return self._transform._tree.get_file_sha1(file_id)
2009
fileobj = self.get_file(file_id)
2011
return sha_file(fileobj)
2015
def is_executable(self, file_id, path=None):
2018
trans_id = self._transform.trans_id_file_id(file_id)
2020
return self._transform._new_executability[trans_id]
2023
return self._transform._tree.is_executable(file_id, path)
2025
if e.errno == errno.ENOENT:
2028
except errors.NoSuchId:
2031
def path_content_summary(self, path):
2032
trans_id = self._path2trans_id(path)
2033
tt = self._transform
2034
tree_path = tt._tree_id_paths.get(trans_id)
2035
kind = tt._new_contents.get(trans_id)
2037
if tree_path is None or trans_id in tt._removed_contents:
2038
return 'missing', None, None, None
2039
summary = tt._tree.path_content_summary(tree_path)
2040
kind, size, executable, link_or_sha1 = summary
2043
limbo_name = tt._limbo_name(trans_id)
2044
if trans_id in tt._new_reference_revision:
2045
kind = 'tree-reference'
2047
statval = os.lstat(limbo_name)
2048
size = statval.st_size
2049
if not supports_executable():
2052
executable = statval.st_mode & S_IEXEC
2056
if kind == 'symlink':
2057
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2058
executable = tt._new_executability.get(trans_id, executable)
2059
return kind, size, executable, link_or_sha1
2061
def iter_changes(self, from_tree, include_unchanged=False,
2062
specific_files=None, pb=None, extra_trees=None,
2063
require_versioned=True, want_unversioned=False):
2064
"""See InterTree.iter_changes.
2066
This has a fast path that is only used when the from_tree matches
2067
the transform tree, and no fancy options are supplied.
2069
if (from_tree is not self._transform._tree or include_unchanged or
2070
specific_files or want_unversioned):
2071
return tree.InterTree(from_tree, self).iter_changes(
2072
include_unchanged=include_unchanged,
2073
specific_files=specific_files,
2075
extra_trees=extra_trees,
2076
require_versioned=require_versioned,
2077
want_unversioned=want_unversioned)
2078
if want_unversioned:
2079
raise ValueError('want_unversioned is not supported')
2080
return self._transform.iter_changes()
2082
def get_file(self, file_id, path=None):
2083
"""See Tree.get_file"""
2084
if not self._content_change(file_id):
2085
return self._transform._tree.get_file(file_id, path)
2086
trans_id = self._transform.trans_id_file_id(file_id)
2087
name = self._transform._limbo_name(trans_id)
2088
return open(name, 'rb')
2090
def get_file_with_stat(self, file_id, path=None):
2091
return self.get_file(file_id, path), None
2093
def annotate_iter(self, file_id,
2094
default_revision=_mod_revision.CURRENT_REVISION):
2095
changes = self._iter_changes_cache.get(file_id)
2099
changed_content, versioned, kind = (changes[2], changes[3],
2103
get_old = (kind[0] == 'file' and versioned[0])
2105
old_annotation = self._transform._tree.annotate_iter(file_id,
2106
default_revision=default_revision)
2110
return old_annotation
2111
if not changed_content:
2112
return old_annotation
2113
# TODO: This is doing something similar to what WT.annotate_iter is
2114
# doing, however it fails slightly because it doesn't know what
2115
# the *other* revision_id is, so it doesn't know how to give the
2116
# other as the origin for some lines, they all get
2117
# 'default_revision'
2118
# It would be nice to be able to use the new Annotator based
2119
# approach, as well.
2120
return annotate.reannotate([old_annotation],
2121
self.get_file(file_id).readlines(),
2124
def get_symlink_target(self, file_id):
2125
"""See Tree.get_symlink_target"""
2126
if not self._content_change(file_id):
2127
return self._transform._tree.get_symlink_target(file_id)
2128
trans_id = self._transform.trans_id_file_id(file_id)
2129
name = self._transform._limbo_name(trans_id)
2130
return osutils.readlink(name)
2132
def walkdirs(self, prefix=''):
2133
pending = [self._transform.root]
2134
while len(pending) > 0:
2135
parent_id = pending.pop()
2138
prefix = prefix.rstrip('/')
2139
parent_path = self._final_paths.get_path(parent_id)
2140
parent_file_id = self._transform.final_file_id(parent_id)
2141
for child_id in self._all_children(parent_id):
2142
path_from_root = self._final_paths.get_path(child_id)
2143
basename = self._transform.final_name(child_id)
2144
file_id = self._transform.final_file_id(child_id)
2145
kind = self._transform.final_kind(child_id)
2146
if kind is not None:
2147
versioned_kind = kind
2150
versioned_kind = self._transform._tree.stored_kind(file_id)
2151
if versioned_kind == 'directory':
2152
subdirs.append(child_id)
2153
children.append((path_from_root, basename, kind, None,
2154
file_id, versioned_kind))
2156
if parent_path.startswith(prefix):
2157
yield (parent_path, parent_file_id), children
2158
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2161
def get_parent_ids(self):
2162
return self._parent_ids
2164
def set_parent_ids(self, parent_ids):
2165
self._parent_ids = parent_ids
2167
def get_revision_tree(self, revision_id):
2168
return self._transform._tree.get_revision_tree(revision_id)
2171
1177
def joinpath(parent, child):
2172
1178
"""Join tree-relative paths, handling the tree root specially"""