974
1076
(from_executable, to_executable)))
975
1077
return iter(sorted(results, key=lambda x:x[1]))
977
def get_preview_tree(self):
978
"""Return a tree representing the result of the transform.
980
The tree is a snapshot, and altering the TreeTransform will invalidate
983
return _PreviewTree(self)
985
def commit(self, branch, message, merge_parents=None, strict=False,
986
timestamp=None, timezone=None, committer=None, authors=None,
987
revprops=None, revision_id=None):
988
"""Commit the result of this TreeTransform to a branch.
990
:param branch: The branch to commit to.
991
:param message: The message to attach to the commit.
992
:param merge_parents: Additional parent revision-ids specified by
994
:param strict: If True, abort the commit if there are unversioned
996
:param timestamp: if not None, seconds-since-epoch for the time and
997
date. (May be a float.)
998
:param timezone: Optional timezone for timestamp, as an offset in
1000
:param committer: Optional committer in email-id format.
1001
(e.g. "J Random Hacker <jrandom@example.com>")
1002
:param authors: Optional list of authors in email-id format.
1003
:param revprops: Optional dictionary of revision properties.
1004
:param revision_id: Optional revision id. (Specifying a revision-id
1005
may reduce performance for some non-native formats.)
1006
:return: The revision_id of the revision committed.
1008
self._check_malformed()
1010
unversioned = set(self._new_contents).difference(set(self._new_id))
1011
for trans_id in unversioned:
1012
if self.final_file_id(trans_id) is None:
1013
raise errors.StrictCommitFailed()
1015
revno, last_rev_id = branch.last_revision_info()
1016
if last_rev_id == _mod_revision.NULL_REVISION:
1017
if merge_parents is not None:
1018
raise ValueError('Cannot supply merge parents for first'
1022
parent_ids = [last_rev_id]
1023
if merge_parents is not None:
1024
parent_ids.extend(merge_parents)
1025
if self._tree.get_revision_id() != last_rev_id:
1026
raise ValueError('TreeTransform not based on branch basis: %s' %
1027
self._tree.get_revision_id())
1028
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1029
builder = branch.get_commit_builder(parent_ids,
1030
timestamp=timestamp,
1032
committer=committer,
1034
revision_id=revision_id)
1035
preview = self.get_preview_tree()
1036
list(builder.record_iter_changes(preview, last_rev_id,
1037
self.iter_changes()))
1038
builder.finish_inventory()
1039
revision_id = builder.commit(message)
1040
branch.set_last_revision_info(revno + 1, revision_id)
1043
def _text_parent(self, trans_id):
1044
file_id = self.tree_file_id(trans_id)
1046
if file_id is None or self._tree.kind(file_id) != 'file':
1048
except errors.NoSuchFile:
1052
def _get_parents_texts(self, trans_id):
1053
"""Get texts for compression parents of this file."""
1054
file_id = self._text_parent(trans_id)
1057
return (self._tree.get_file_text(file_id),)
1059
def _get_parents_lines(self, trans_id):
1060
"""Get lines for compression parents of this file."""
1061
file_id = self._text_parent(trans_id)
1064
return (self._tree.get_file_lines(file_id),)
1066
def serialize(self, serializer):
1067
"""Serialize this TreeTransform.
1069
:param serializer: A Serialiser like pack.ContainerSerializer.
1071
new_name = dict((k, v.encode('utf-8')) for k, v in
1072
self._new_name.items())
1073
new_executability = dict((k, int(v)) for k, v in
1074
self._new_executability.items())
1075
tree_path_ids = dict((k.encode('utf-8'), v)
1076
for k, v in self._tree_path_ids.items())
1078
'_id_number': self._id_number,
1079
'_new_name': new_name,
1080
'_new_parent': self._new_parent,
1081
'_new_executability': new_executability,
1082
'_new_id': self._new_id,
1083
'_tree_path_ids': tree_path_ids,
1084
'_removed_id': list(self._removed_id),
1085
'_removed_contents': list(self._removed_contents),
1086
'_non_present_ids': self._non_present_ids,
1088
yield serializer.bytes_record(bencode.bencode(attribs),
1090
for trans_id, kind in self._new_contents.items():
1092
lines = osutils.chunks_to_lines(
1093
self._read_file_chunks(trans_id))
1094
parents = self._get_parents_lines(trans_id)
1095
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1096
content = ''.join(mpdiff.to_patch())
1097
if kind == 'directory':
1099
if kind == 'symlink':
1100
content = self._read_symlink_target(trans_id)
1101
yield serializer.bytes_record(content, ((trans_id, kind),))
1103
def deserialize(self, records):
1104
"""Deserialize a stored TreeTransform.
1106
:param records: An iterable of (names, content) tuples, as per
1107
pack.ContainerPushParser.
1109
names, content = records.next()
1110
attribs = bencode.bdecode(content)
1111
self._id_number = attribs['_id_number']
1112
self._new_name = dict((k, v.decode('utf-8'))
1113
for k, v in attribs['_new_name'].items())
1114
self._new_parent = attribs['_new_parent']
1115
self._new_executability = dict((k, bool(v)) for k, v in
1116
attribs['_new_executability'].items())
1117
self._new_id = attribs['_new_id']
1118
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1119
self._tree_path_ids = {}
1120
self._tree_id_paths = {}
1121
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1122
path = bytepath.decode('utf-8')
1123
self._tree_path_ids[path] = trans_id
1124
self._tree_id_paths[trans_id] = path
1125
self._removed_id = set(attribs['_removed_id'])
1126
self._removed_contents = set(attribs['_removed_contents'])
1127
self._non_present_ids = attribs['_non_present_ids']
1128
for ((trans_id, kind),), content in records:
1130
mpdiff = multiparent.MultiParent.from_patch(content)
1131
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1132
self.create_file(lines, trans_id)
1133
if kind == 'directory':
1134
self.create_directory(trans_id)
1135
if kind == 'symlink':
1136
self.create_symlink(content.decode('utf-8'), trans_id)
1139
class DiskTreeTransform(TreeTransformBase):
1140
"""Tree transform storing its contents on disk."""
1142
def __init__(self, tree, limbodir, pb=None,
1143
case_sensitive=True):
1145
:param tree: The tree that will be transformed, but not necessarily
1147
:param limbodir: A directory where new files can be stored until
1148
they are installed in their proper places
1150
:param case_sensitive: If True, the target of the transform is
1151
case sensitive, not just case preserving.
1153
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1154
self._limbodir = limbodir
1155
self._deletiondir = None
1156
# A mapping of transform ids to their limbo filename
1157
self._limbo_files = {}
1158
# A mapping of transform ids to a set of the transform ids of children
1159
# that their limbo directory has
1160
self._limbo_children = {}
1161
# Map transform ids to maps of child filename to child transform id
1162
self._limbo_children_names = {}
1163
# List of transform ids that need to be renamed from limbo into place
1164
self._needs_rename = set()
1165
self._creation_mtime = None
1168
"""Release the working tree lock, if held, clean up limbo dir.
1170
This is required if apply has not been invoked, but can be invoked
1173
if self._tree is None:
1176
entries = [(self._limbo_name(t), t, k) for t, k in
1177
self._new_contents.iteritems()]
1178
entries.sort(reverse=True)
1179
for path, trans_id, kind in entries:
1182
delete_any(self._limbodir)
1184
# We don't especially care *why* the dir is immortal.
1185
raise ImmortalLimbo(self._limbodir)
1187
if self._deletiondir is not None:
1188
delete_any(self._deletiondir)
1190
raise errors.ImmortalPendingDeletion(self._deletiondir)
1192
TreeTransformBase.finalize(self)
1194
def _limbo_name(self, trans_id):
1195
"""Generate the limbo name of a file"""
1196
limbo_name = self._limbo_files.get(trans_id)
1197
if limbo_name is None:
1198
limbo_name = self._generate_limbo_path(trans_id)
1199
self._limbo_files[trans_id] = limbo_name
1202
def _generate_limbo_path(self, trans_id):
1203
"""Generate a limbo path using the trans_id as the relative path.
1205
This is suitable as a fallback, and when the transform should not be
1206
sensitive to the path encoding of the limbo directory.
1208
self._needs_rename.add(trans_id)
1209
return pathjoin(self._limbodir, trans_id)
1211
def adjust_path(self, name, parent, trans_id):
1212
previous_parent = self._new_parent.get(trans_id)
1213
previous_name = self._new_name.get(trans_id)
1214
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1215
if (trans_id in self._limbo_files and
1216
trans_id not in self._needs_rename):
1217
self._rename_in_limbo([trans_id])
1218
if previous_parent != parent:
1219
self._limbo_children[previous_parent].remove(trans_id)
1220
if previous_parent != parent or previous_name != name:
1221
del self._limbo_children_names[previous_parent][previous_name]
1223
def _rename_in_limbo(self, trans_ids):
1224
"""Fix limbo names so that the right final path is produced.
1226
This means we outsmarted ourselves-- we tried to avoid renaming
1227
these files later by creating them with their final names in their
1228
final parents. But now the previous name or parent is no longer
1229
suitable, so we have to rename them.
1231
Even for trans_ids that have no new contents, we must remove their
1232
entries from _limbo_files, because they are now stale.
1234
for trans_id in trans_ids:
1235
old_path = self._limbo_files.pop(trans_id)
1236
if trans_id not in self._new_contents:
1238
new_path = self._limbo_name(trans_id)
1239
os.rename(old_path, new_path)
1240
for descendant in self._limbo_descendants(trans_id):
1241
desc_path = self._limbo_files[descendant]
1242
desc_path = new_path + desc_path[len(old_path):]
1243
self._limbo_files[descendant] = desc_path
1245
def _limbo_descendants(self, trans_id):
1246
"""Return the set of trans_ids whose limbo paths descend from this."""
1247
descendants = set(self._limbo_children.get(trans_id, []))
1248
for descendant in list(descendants):
1249
descendants.update(self._limbo_descendants(descendant))
1252
def create_file(self, contents, trans_id, mode_id=None):
1253
"""Schedule creation of a new file.
1257
Contents is an iterator of strings, all of which will be written
1258
to the target destination.
1260
New file takes the permissions of any existing file with that id,
1261
unless mode_id is specified.
1263
name = self._limbo_name(trans_id)
1264
f = open(name, 'wb')
1267
unique_add(self._new_contents, trans_id, 'file')
1269
# Clean up the file, it never got registered so
1270
# TreeTransform.finalize() won't clean it up.
1275
f.writelines(contents)
1278
self._set_mtime(name)
1279
self._set_mode(trans_id, mode_id, S_ISREG)
1281
def _read_file_chunks(self, trans_id):
1282
cur_file = open(self._limbo_name(trans_id), 'rb')
1284
return cur_file.readlines()
1288
def _read_symlink_target(self, trans_id):
1289
return os.readlink(self._limbo_name(trans_id))
1291
def _set_mtime(self, path):
1292
"""All files that are created get the same mtime.
1294
This time is set by the first object to be created.
1296
if self._creation_mtime is None:
1297
self._creation_mtime = time.time()
1298
os.utime(path, (self._creation_mtime, self._creation_mtime))
1300
def create_hardlink(self, path, trans_id):
1301
"""Schedule creation of a hard link"""
1302
name = self._limbo_name(trans_id)
1306
if e.errno != errno.EPERM:
1308
raise errors.HardLinkNotSupported(path)
1310
unique_add(self._new_contents, trans_id, 'file')
1312
# Clean up the file, it never got registered so
1313
# TreeTransform.finalize() won't clean it up.
1317
def create_directory(self, trans_id):
1318
"""Schedule creation of a new directory.
1320
See also new_directory.
1322
os.mkdir(self._limbo_name(trans_id))
1323
unique_add(self._new_contents, trans_id, 'directory')
1325
def create_symlink(self, target, trans_id):
1326
"""Schedule creation of a new symbolic link.
1328
target is a bytestring.
1329
See also new_symlink.
1332
os.symlink(target, self._limbo_name(trans_id))
1333
unique_add(self._new_contents, trans_id, 'symlink')
1336
path = FinalPaths(self).get_path(trans_id)
1339
raise UnableCreateSymlink(path=path)
1341
def cancel_creation(self, trans_id):
1342
"""Cancel the creation of new file contents."""
1343
del self._new_contents[trans_id]
1344
children = self._limbo_children.get(trans_id)
1345
# if this is a limbo directory with children, move them before removing
1347
if children is not None:
1348
self._rename_in_limbo(children)
1349
del self._limbo_children[trans_id]
1350
del self._limbo_children_names[trans_id]
1351
delete_any(self._limbo_name(trans_id))
1353
def new_orphan(self, trans_id, parent_id):
1354
# FIXME: There is no tree config, so we use the branch one (it's weird
1355
# to define it this way as orphaning can only occur in a working tree,
1356
# but that's all we have (for now). It will find the option in
1357
# locations.conf or bazaar.conf though) -- vila 20100916
1358
conf = self._tree.branch.get_config()
1359
conf_var_name = 'bzr.transform.orphan_policy'
1360
orphan_policy = conf.get_user_option(conf_var_name)
1361
default_policy = orphaning_registry.default_key
1362
if orphan_policy is None:
1363
orphan_policy = default_policy
1364
if orphan_policy not in orphaning_registry:
1365
trace.warning('%s (from %s) is not a known policy, defaulting to %s'
1366
% (orphan_policy, conf_var_name, default_policy))
1367
orphan_policy = default_policy
1368
handle_orphan = orphaning_registry.get(orphan_policy)
1369
handle_orphan(self, trans_id, parent_id)
1372
class OrphaningError(errors.BzrError):
1374
# Only bugs could lead to such exception being seen by the user
1375
internal_error = True
1376
_fmt = "Error while orphaning %s in %s directory"
1378
def __init__(self, orphan, parent):
1379
errors.BzrError.__init__(self)
1380
self.orphan = orphan
1381
self.parent = parent
1384
class OrphaningForbidden(OrphaningError):
1386
_fmt = "Policy: %s doesn't allow creating orphans."
1388
def __init__(self, policy):
1389
errors.BzrError.__init__(self)
1390
self.policy = policy
1393
def move_orphan(tt, orphan_id, parent_id):
1394
"""See TreeTransformBase.new_orphan.
1396
This creates a new orphan in the `bzr-orphans` dir at the root of the
1399
:param tt: The TreeTransform orphaning `trans_id`.
1401
:param orphan_id: The trans id that should be orphaned.
1403
:param parent_id: The orphan parent trans id.
1405
# Add the orphan dir if it doesn't exist
1406
orphan_dir_basename = 'bzr-orphans'
1407
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1408
if tt.final_kind(od_id) is None:
1409
tt.create_directory(od_id)
1410
parent_path = tt._tree_id_paths[parent_id]
1411
# Find a name that doesn't exist yet in the orphan dir
1412
actual_name = tt.final_name(orphan_id)
1413
new_name = tt._available_backup_name(actual_name, od_id)
1414
tt.adjust_path(new_name, od_id, orphan_id)
1415
trace.warning('%s has been orphaned in %s'
1416
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1419
def refuse_orphan(tt, orphan_id, parent_id):
1420
"""See TreeTransformBase.new_orphan.
1422
This refuses to create orphan, letting the caller handle the conflict.
1424
raise OrphaningForbidden('never')
1427
orphaning_registry = registry.Registry()
1428
orphaning_registry.register(
1429
'conflict', refuse_orphan,
1430
'Leave orphans in place and create a conflict on the directory.')
1431
orphaning_registry.register(
1432
'move', move_orphan,
1433
'Move orphans into the bzr-orphans directory.')
1434
orphaning_registry._set_default_key('conflict')
1437
class TreeTransform(DiskTreeTransform):
1438
"""Represent a tree transformation.
1440
This object is designed to support incremental generation of the transform,
1443
However, it gives optimum performance when parent directories are created
1444
before their contents. The transform is then able to put child files
1445
directly in their parent directory, avoiding later renames.
1447
It is easy to produce malformed transforms, but they are generally
1448
harmless. Attempting to apply a malformed transform will cause an
1449
exception to be raised before any modifications are made to the tree.
1451
Many kinds of malformed transforms can be corrected with the
1452
resolve_conflicts function. The remaining ones indicate programming error,
1453
such as trying to create a file with no path.
1455
Two sets of file creation methods are supplied. Convenience methods are:
1460
These are composed of the low-level methods:
1462
* create_file or create_directory or create_symlink
1466
Transform/Transaction ids
1467
-------------------------
1468
trans_ids are temporary ids assigned to all files involved in a transform.
1469
It's possible, even common, that not all files in the Tree have trans_ids.
1471
trans_ids are used because filenames and file_ids are not good enough
1472
identifiers; filenames change, and not all files have file_ids. File-ids
1473
are also associated with trans-ids, so that moving a file moves its
1476
trans_ids are only valid for the TreeTransform that generated them.
1480
Limbo is a temporary directory use to hold new versions of files.
1481
Files are added to limbo by create_file, create_directory, create_symlink,
1482
and their convenience variants (new_*). Files may be removed from limbo
1483
using cancel_creation. Files are renamed from limbo into their final
1484
location as part of TreeTransform.apply
1486
Limbo must be cleaned up, by either calling TreeTransform.apply or
1487
calling TreeTransform.finalize.
1489
Files are placed into limbo inside their parent directories, where
1490
possible. This reduces subsequent renames, and makes operations involving
1491
lots of files faster. This optimization is only possible if the parent
1492
directory is created *before* creating any of its children, so avoid
1493
creating children before parents, where possible.
1497
This temporary directory is used by _FileMover for storing files that are
1498
about to be deleted. In case of rollback, the files will be restored.
1499
FileMover does not delete files until it is sure that a rollback will not
1502
def __init__(self, tree, pb=None):
1503
"""Note: a tree_write lock is taken on the tree.
1505
Use TreeTransform.finalize() to release the lock (can be omitted if
1506
TreeTransform.apply() called).
1508
tree.lock_tree_write()
1511
limbodir = urlutils.local_path_from_url(
1512
tree._transport.abspath('limbo'))
1516
if e.errno == errno.EEXIST:
1517
raise ExistingLimbo(limbodir)
1518
deletiondir = urlutils.local_path_from_url(
1519
tree._transport.abspath('pending-deletion'))
1521
os.mkdir(deletiondir)
1523
if e.errno == errno.EEXIST:
1524
raise errors.ExistingPendingDeletion(deletiondir)
1529
# Cache of realpath results, to speed up canonical_path
1530
self._realpaths = {}
1531
# Cache of relpath results, to speed up canonical_path
1533
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1534
tree.case_sensitive)
1535
self._deletiondir = deletiondir
1537
def canonical_path(self, path):
1538
"""Get the canonical tree-relative path"""
1539
# don't follow final symlinks
1540
abs = self._tree.abspath(path)
1541
if abs in self._relpaths:
1542
return self._relpaths[abs]
1543
dirname, basename = os.path.split(abs)
1544
if dirname not in self._realpaths:
1545
self._realpaths[dirname] = os.path.realpath(dirname)
1546
dirname = self._realpaths[dirname]
1547
abs = pathjoin(dirname, basename)
1548
if dirname in self._relpaths:
1549
relpath = pathjoin(self._relpaths[dirname], basename)
1550
relpath = relpath.rstrip('/\\')
1552
relpath = self._tree.relpath(abs)
1553
self._relpaths[abs] = relpath
1556
def tree_kind(self, trans_id):
1557
"""Determine the file kind in the working tree.
1559
:returns: The file kind or None if the file does not exist
1561
path = self._tree_id_paths.get(trans_id)
1565
return file_kind(self._tree.abspath(path))
1566
except errors.NoSuchFile:
1569
def _set_mode(self, trans_id, mode_id, typefunc):
1570
"""Set the mode of new file contents.
1571
The mode_id is the existing file to get the mode from (often the same
1572
as trans_id). The operation is only performed if there's a mode match
1573
according to typefunc.
1578
old_path = self._tree_id_paths[mode_id]
1582
mode = os.stat(self._tree.abspath(old_path)).st_mode
1584
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1585
# Either old_path doesn't exist, or the parent of the
1586
# target is not a directory (but will be one eventually)
1587
# Either way, we know it doesn't exist *right now*
1588
# See also bug #248448
1593
os.chmod(self._limbo_name(trans_id), mode)
1595
def iter_tree_children(self, parent_id):
1596
"""Iterate through the entry's tree children, if any"""
1598
path = self._tree_id_paths[parent_id]
1602
children = os.listdir(self._tree.abspath(path))
1604
if not (osutils._is_error_enotdir(e)
1605
or e.errno in (errno.ENOENT, errno.ESRCH)):
1609
for child in children:
1610
childpath = joinpath(path, child)
1611
if self._tree.is_control_filename(childpath):
1613
yield self.trans_id_tree_path(childpath)
1615
def _generate_limbo_path(self, trans_id):
1616
"""Generate a limbo path using the final path if possible.
1618
This optimizes the performance of applying the tree transform by
1619
avoiding renames. These renames can be avoided only when the parent
1620
directory is already scheduled for creation.
1622
If the final path cannot be used, falls back to using the trans_id as
1625
parent = self._new_parent.get(trans_id)
1626
# if the parent directory is already in limbo (e.g. when building a
1627
# tree), choose a limbo name inside the parent, to reduce further
1629
use_direct_path = False
1630
if self._new_contents.get(parent) == 'directory':
1631
filename = self._new_name.get(trans_id)
1632
if filename is not None:
1633
if parent not in self._limbo_children:
1634
self._limbo_children[parent] = set()
1635
self._limbo_children_names[parent] = {}
1636
use_direct_path = True
1637
# the direct path can only be used if no other file has
1638
# already taken this pathname, i.e. if the name is unused, or
1639
# if it is already associated with this trans_id.
1640
elif self._case_sensitive_target:
1641
if (self._limbo_children_names[parent].get(filename)
1642
in (trans_id, None)):
1643
use_direct_path = True
1645
for l_filename, l_trans_id in\
1646
self._limbo_children_names[parent].iteritems():
1647
if l_trans_id == trans_id:
1649
if l_filename.lower() == filename.lower():
1652
use_direct_path = True
1654
if not use_direct_path:
1655
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1657
limbo_name = pathjoin(self._limbo_files[parent], filename)
1658
self._limbo_children[parent].add(trans_id)
1659
self._limbo_children_names[parent][filename] = trans_id
1663
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1664
"""Apply all changes to the inventory and filesystem.
1666
If filesystem or inventory conflicts are present, MalformedTransform
1669
If apply succeeds, finalize is not necessary.
1671
:param no_conflicts: if True, the caller guarantees there are no
1672
conflicts, so no check is made.
1673
:param precomputed_delta: An inventory delta to use instead of
1675
:param _mover: Supply an alternate FileMover, for testing
1677
if not no_conflicts:
1678
self._check_malformed()
1679
child_pb = ui.ui_factory.nested_progress_bar()
1681
if precomputed_delta is None:
1682
child_pb.update('Apply phase', 0, 2)
1683
inventory_delta = self._generate_inventory_delta()
1686
inventory_delta = precomputed_delta
1689
mover = _FileMover()
1693
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1694
self._apply_removals(mover)
1695
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1696
modified_paths = self._apply_insertions(mover)
1701
mover.apply_deletions()
1704
self._tree.apply_inventory_delta(inventory_delta)
1707
return _TransformResults(modified_paths, self.rename_count)
1709
def _generate_inventory_delta(self):
1710
"""Generate an inventory delta for the current transform."""
1711
inventory_delta = []
1712
child_pb = ui.ui_factory.nested_progress_bar()
1713
new_paths = self._inventory_altered()
1714
total_entries = len(new_paths) + len(self._removed_id)
1716
for num, trans_id in enumerate(self._removed_id):
1718
child_pb.update('removing file', num, total_entries)
1719
if trans_id == self._new_root:
1720
file_id = self._tree.get_root_id()
1722
file_id = self.tree_file_id(trans_id)
1723
# File-id isn't really being deleted, just moved
1724
if file_id in self._r_new_id:
1726
path = self._tree_id_paths[trans_id]
1727
inventory_delta.append((path, None, file_id, None))
1728
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1730
entries = self._tree.iter_entries_by_dir(
1731
new_path_file_ids.values())
1732
old_paths = dict((e.file_id, p) for p, e in entries)
1734
for num, (path, trans_id) in enumerate(new_paths):
1736
child_pb.update('adding file',
1737
num + len(self._removed_id), total_entries)
1738
file_id = new_path_file_ids[trans_id]
1742
kind = self.final_kind(trans_id)
1744
kind = self._tree.stored_kind(file_id)
1745
parent_trans_id = self.final_parent(trans_id)
1746
parent_file_id = new_path_file_ids.get(parent_trans_id)
1747
if parent_file_id is None:
1748
parent_file_id = self.final_file_id(parent_trans_id)
1749
if trans_id in self._new_reference_revision:
1750
new_entry = inventory.TreeReference(
1752
self._new_name[trans_id],
1753
self.final_file_id(self._new_parent[trans_id]),
1754
None, self._new_reference_revision[trans_id])
1756
new_entry = inventory.make_entry(kind,
1757
self.final_name(trans_id),
1758
parent_file_id, file_id)
1759
old_path = old_paths.get(new_entry.file_id)
1760
new_executability = self._new_executability.get(trans_id)
1761
if new_executability is not None:
1762
new_entry.executable = new_executability
1763
inventory_delta.append(
1764
(old_path, path, new_entry.file_id, new_entry))
1767
return inventory_delta
1769
def _apply_removals(self, mover):
1770
"""Perform tree operations that remove directory/inventory names.
1772
That is, delete files that are to be deleted, and put any files that
1773
need renaming into limbo. This must be done in strict child-to-parent
1776
If inventory_delta is None, no inventory delta generation is performed.
1778
tree_paths = list(self._tree_path_ids.iteritems())
1779
tree_paths.sort(reverse=True)
1780
child_pb = ui.ui_factory.nested_progress_bar()
1782
for num, data in enumerate(tree_paths):
1783
path, trans_id = data
1784
child_pb.update('removing file', num, len(tree_paths))
1785
full_path = self._tree.abspath(path)
1786
if trans_id in self._removed_contents:
1787
delete_path = os.path.join(self._deletiondir, trans_id)
1788
mover.pre_delete(full_path, delete_path)
1789
elif (trans_id in self._new_name
1790
or trans_id in self._new_parent):
1792
mover.rename(full_path, self._limbo_name(trans_id))
1793
except errors.TransformRenameFailed, e:
1794
if e.errno != errno.ENOENT:
1797
self.rename_count += 1
1801
def _apply_insertions(self, mover):
1802
"""Perform tree operations that insert directory/inventory names.
1804
That is, create any files that need to be created, and restore from
1805
limbo any files that needed renaming. This must be done in strict
1806
parent-to-child order.
1808
If inventory_delta is None, no inventory delta is calculated, and
1809
no list of modified paths is returned.
1811
new_paths = self.new_paths(filesystem_only=True)
1813
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1815
child_pb = ui.ui_factory.nested_progress_bar()
1817
for num, (path, trans_id) in enumerate(new_paths):
1819
child_pb.update('adding file', num, len(new_paths))
1820
full_path = self._tree.abspath(path)
1821
if trans_id in self._needs_rename:
1823
mover.rename(self._limbo_name(trans_id), full_path)
1824
except errors.TransformRenameFailed, e:
1825
# We may be renaming a dangling inventory id
1826
if e.errno != errno.ENOENT:
1829
self.rename_count += 1
1830
if (trans_id in self._new_contents or
1831
self.path_changed(trans_id)):
1832
if trans_id in self._new_contents:
1833
modified_paths.append(full_path)
1834
if trans_id in self._new_executability:
1835
self._set_executability(path, trans_id)
1838
self._new_contents.clear()
1839
return modified_paths
1842
class TransformPreview(DiskTreeTransform):
1843
"""A TreeTransform for generating preview trees.
1845
Unlike TreeTransform, this version works when the input tree is a
1846
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1847
unversioned files in the input tree.
1850
def __init__(self, tree, pb=None, case_sensitive=True):
1852
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1853
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1855
def canonical_path(self, path):
1858
def tree_kind(self, trans_id):
1859
path = self._tree_id_paths.get(trans_id)
1862
file_id = self._tree.path2id(path)
1864
return self._tree.kind(file_id)
1865
except errors.NoSuchFile:
1868
def _set_mode(self, trans_id, mode_id, typefunc):
1869
"""Set the mode of new file contents.
1870
The mode_id is the existing file to get the mode from (often the same
1871
as trans_id). The operation is only performed if there's a mode match
1872
according to typefunc.
1874
# is it ok to ignore this? probably
1877
def iter_tree_children(self, parent_id):
1878
"""Iterate through the entry's tree children, if any"""
1880
path = self._tree_id_paths[parent_id]
1883
file_id = self.tree_file_id(parent_id)
1886
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1887
children = getattr(entry, 'children', {})
1888
for child in children:
1889
childpath = joinpath(path, child)
1890
yield self.trans_id_tree_path(childpath)
1892
def new_orphan(self, trans_id, parent_id):
1893
raise NotImplementedError(self.new_orphan)
1896
class _PreviewTree(tree.Tree):
1897
"""Partial implementation of Tree to support show_diff_trees"""
1899
def __init__(self, transform):
1900
self._transform = transform
1901
self._final_paths = FinalPaths(transform)
1902
self.__by_parent = None
1903
self._parent_ids = []
1904
self._all_children_cache = {}
1905
self._path2trans_id_cache = {}
1906
self._final_name_cache = {}
1907
self._iter_changes_cache = dict((c[0], c) for c in
1908
self._transform.iter_changes())
1910
def _content_change(self, file_id):
1911
"""Return True if the content of this file changed"""
1912
changes = self._iter_changes_cache.get(file_id)
1913
# changes[2] is true if the file content changed. See
1914
# InterTree.iter_changes.
1915
return (changes is not None and changes[2])
1917
def _get_repository(self):
1918
repo = getattr(self._transform._tree, '_repository', None)
1920
repo = self._transform._tree.branch.repository
1923
def _iter_parent_trees(self):
1924
for revision_id in self.get_parent_ids():
1926
yield self.revision_tree(revision_id)
1927
except errors.NoSuchRevisionInTree:
1928
yield self._get_repository().revision_tree(revision_id)
1930
def _get_file_revision(self, file_id, vf, tree_revision):
1931
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1932
self._iter_parent_trees()]
1933
vf.add_lines((file_id, tree_revision), parent_keys,
1934
self.get_file_lines(file_id))
1935
repo = self._get_repository()
1936
base_vf = repo.texts
1937
if base_vf not in vf.fallback_versionedfiles:
1938
vf.fallback_versionedfiles.append(base_vf)
1939
return tree_revision
1941
def _stat_limbo_file(self, file_id):
1942
trans_id = self._transform.trans_id_file_id(file_id)
1943
name = self._transform._limbo_name(trans_id)
1944
return os.lstat(name)
1947
def _by_parent(self):
1948
if self.__by_parent is None:
1949
self.__by_parent = self._transform.by_parent()
1950
return self.__by_parent
1952
def _comparison_data(self, entry, path):
1953
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1954
if kind == 'missing':
1958
file_id = self._transform.final_file_id(self._path2trans_id(path))
1959
executable = self.is_executable(file_id, path)
1960
return kind, executable, None
1962
def is_locked(self):
1965
def lock_read(self):
1966
# Perhaps in theory, this should lock the TreeTransform?
1973
def inventory(self):
1974
"""This Tree does not use inventory as its backing data."""
1975
raise NotImplementedError(_PreviewTree.inventory)
1977
def get_root_id(self):
1978
return self._transform.final_file_id(self._transform.root)
1980
def all_file_ids(self):
1981
tree_ids = set(self._transform._tree.all_file_ids())
1982
tree_ids.difference_update(self._transform.tree_file_id(t)
1983
for t in self._transform._removed_id)
1984
tree_ids.update(self._transform._new_id.values())
1988
return iter(self.all_file_ids())
1990
def _has_id(self, file_id, fallback_check):
1991
if file_id in self._transform._r_new_id:
1993
elif file_id in set([self._transform.tree_file_id(trans_id) for
1994
trans_id in self._transform._removed_id]):
1997
return fallback_check(file_id)
1999
def has_id(self, file_id):
2000
return self._has_id(file_id, self._transform._tree.has_id)
2002
def has_or_had_id(self, file_id):
2003
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2005
def _path2trans_id(self, path):
2006
# We must not use None here, because that is a valid value to store.
2007
trans_id = self._path2trans_id_cache.get(path, object)
2008
if trans_id is not object:
2010
segments = splitpath(path)
2011
cur_parent = self._transform.root
2012
for cur_segment in segments:
2013
for child in self._all_children(cur_parent):
2014
final_name = self._final_name_cache.get(child)
2015
if final_name is None:
2016
final_name = self._transform.final_name(child)
2017
self._final_name_cache[child] = final_name
2018
if final_name == cur_segment:
2022
self._path2trans_id_cache[path] = None
2024
self._path2trans_id_cache[path] = cur_parent
2027
def path2id(self, path):
2028
return self._transform.final_file_id(self._path2trans_id(path))
2030
def id2path(self, file_id):
2031
trans_id = self._transform.trans_id_file_id(file_id)
2033
return self._final_paths._determine_path(trans_id)
2035
raise errors.NoSuchId(self, file_id)
2037
def _all_children(self, trans_id):
2038
children = self._all_children_cache.get(trans_id)
2039
if children is not None:
2041
children = set(self._transform.iter_tree_children(trans_id))
2042
# children in the _new_parent set are provided by _by_parent.
2043
children.difference_update(self._transform._new_parent.keys())
2044
children.update(self._by_parent.get(trans_id, []))
2045
self._all_children_cache[trans_id] = children
2048
def iter_children(self, file_id):
2049
trans_id = self._transform.trans_id_file_id(file_id)
2050
for child_trans_id in self._all_children(trans_id):
2051
yield self._transform.final_file_id(child_trans_id)
2054
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2055
in self._transform._tree.extras())
2056
possible_extras.update(self._transform._new_contents)
2057
possible_extras.update(self._transform._removed_id)
2058
for trans_id in possible_extras:
2059
if self._transform.final_file_id(trans_id) is None:
2060
yield self._final_paths._determine_path(trans_id)
2062
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
2063
yield_parents=False):
2064
for trans_id, parent_file_id in ordered_entries:
2065
file_id = self._transform.final_file_id(trans_id)
2068
if (specific_file_ids is not None
2069
and file_id not in specific_file_ids):
2071
kind = self._transform.final_kind(trans_id)
2073
kind = self._transform._tree.stored_kind(file_id)
2074
new_entry = inventory.make_entry(
2076
self._transform.final_name(trans_id),
2077
parent_file_id, file_id)
2078
yield new_entry, trans_id
2080
def _list_files_by_dir(self):
2081
todo = [ROOT_PARENT]
2083
while len(todo) > 0:
2085
parent_file_id = self._transform.final_file_id(parent)
2086
children = list(self._all_children(parent))
2087
paths = dict(zip(children, self._final_paths.get_paths(children)))
2088
children.sort(key=paths.get)
2089
todo.extend(reversed(children))
2090
for trans_id in children:
2091
ordered_ids.append((trans_id, parent_file_id))
2094
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
2095
# This may not be a maximally efficient implementation, but it is
2096
# reasonably straightforward. An implementation that grafts the
2097
# TreeTransform changes onto the tree's iter_entries_by_dir results
2098
# might be more efficient, but requires tricky inferences about stack
2100
ordered_ids = self._list_files_by_dir()
2101
for entry, trans_id in self._make_inv_entries(ordered_ids,
2102
specific_file_ids, yield_parents=yield_parents):
2103
yield unicode(self._final_paths.get_path(trans_id)), entry
2105
def _iter_entries_for_dir(self, dir_path):
2106
"""Return path, entry for items in a directory without recursing down."""
2107
dir_file_id = self.path2id(dir_path)
2109
for file_id in self.iter_children(dir_file_id):
2110
trans_id = self._transform.trans_id_file_id(file_id)
2111
ordered_ids.append((trans_id, file_id))
2112
for entry, trans_id in self._make_inv_entries(ordered_ids):
2113
yield unicode(self._final_paths.get_path(trans_id)), entry
2115
def list_files(self, include_root=False, from_dir=None, recursive=True):
2116
"""See WorkingTree.list_files."""
2117
# XXX This should behave like WorkingTree.list_files, but is really
2118
# more like RevisionTree.list_files.
2122
prefix = from_dir + '/'
2123
entries = self.iter_entries_by_dir()
2124
for path, entry in entries:
2125
if entry.name == '' and not include_root:
2128
if not path.startswith(prefix):
2130
path = path[len(prefix):]
2131
yield path, 'V', entry.kind, entry.file_id, entry
2133
if from_dir is None and include_root is True:
2134
root_entry = inventory.make_entry('directory', '',
2135
ROOT_PARENT, self.get_root_id())
2136
yield '', 'V', 'directory', root_entry.file_id, root_entry
2137
entries = self._iter_entries_for_dir(from_dir or '')
2138
for path, entry in entries:
2139
yield path, 'V', entry.kind, entry.file_id, entry
2141
def kind(self, file_id):
2142
trans_id = self._transform.trans_id_file_id(file_id)
2143
return self._transform.final_kind(trans_id)
2145
def stored_kind(self, file_id):
2146
trans_id = self._transform.trans_id_file_id(file_id)
2148
return self._transform._new_contents[trans_id]
2150
return self._transform._tree.stored_kind(file_id)
2152
def get_file_mtime(self, file_id, path=None):
2153
"""See Tree.get_file_mtime"""
2154
if not self._content_change(file_id):
2155
return self._transform._tree.get_file_mtime(file_id)
2156
return self._stat_limbo_file(file_id).st_mtime
2158
def _file_size(self, entry, stat_value):
2159
return self.get_file_size(entry.file_id)
2161
def get_file_size(self, file_id):
2162
"""See Tree.get_file_size"""
2163
if self.kind(file_id) == 'file':
2164
return self._transform._tree.get_file_size(file_id)
2168
def get_file_sha1(self, file_id, path=None, stat_value=None):
2169
trans_id = self._transform.trans_id_file_id(file_id)
2170
kind = self._transform._new_contents.get(trans_id)
2172
return self._transform._tree.get_file_sha1(file_id)
2174
fileobj = self.get_file(file_id)
2176
return sha_file(fileobj)
2180
def is_executable(self, file_id, path=None):
2183
trans_id = self._transform.trans_id_file_id(file_id)
2185
return self._transform._new_executability[trans_id]
2188
return self._transform._tree.is_executable(file_id, path)
2190
if e.errno == errno.ENOENT:
2193
except errors.NoSuchId:
2196
def path_content_summary(self, path):
2197
trans_id = self._path2trans_id(path)
2198
tt = self._transform
2199
tree_path = tt._tree_id_paths.get(trans_id)
2200
kind = tt._new_contents.get(trans_id)
2202
if tree_path is None or trans_id in tt._removed_contents:
2203
return 'missing', None, None, None
2204
summary = tt._tree.path_content_summary(tree_path)
2205
kind, size, executable, link_or_sha1 = summary
2208
limbo_name = tt._limbo_name(trans_id)
2209
if trans_id in tt._new_reference_revision:
2210
kind = 'tree-reference'
2212
statval = os.lstat(limbo_name)
2213
size = statval.st_size
2214
if not supports_executable():
2217
executable = statval.st_mode & S_IEXEC
2221
if kind == 'symlink':
2222
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2223
executable = tt._new_executability.get(trans_id, executable)
2224
return kind, size, executable, link_or_sha1
2226
def iter_changes(self, from_tree, include_unchanged=False,
2227
specific_files=None, pb=None, extra_trees=None,
2228
require_versioned=True, want_unversioned=False):
2229
"""See InterTree.iter_changes.
2231
This has a fast path that is only used when the from_tree matches
2232
the transform tree, and no fancy options are supplied.
2234
if (from_tree is not self._transform._tree or include_unchanged or
2235
specific_files or want_unversioned):
2236
return tree.InterTree(from_tree, self).iter_changes(
2237
include_unchanged=include_unchanged,
2238
specific_files=specific_files,
2240
extra_trees=extra_trees,
2241
require_versioned=require_versioned,
2242
want_unversioned=want_unversioned)
2243
if want_unversioned:
2244
raise ValueError('want_unversioned is not supported')
2245
return self._transform.iter_changes()
2247
def get_file(self, file_id, path=None):
2248
"""See Tree.get_file"""
2249
if not self._content_change(file_id):
2250
return self._transform._tree.get_file(file_id, path)
2251
trans_id = self._transform.trans_id_file_id(file_id)
2252
name = self._transform._limbo_name(trans_id)
2253
return open(name, 'rb')
2255
def get_file_with_stat(self, file_id, path=None):
2256
return self.get_file(file_id, path), None
2258
def annotate_iter(self, file_id,
2259
default_revision=_mod_revision.CURRENT_REVISION):
2260
changes = self._iter_changes_cache.get(file_id)
2264
changed_content, versioned, kind = (changes[2], changes[3],
2268
get_old = (kind[0] == 'file' and versioned[0])
2270
old_annotation = self._transform._tree.annotate_iter(file_id,
2271
default_revision=default_revision)
2275
return old_annotation
2276
if not changed_content:
2277
return old_annotation
2278
# TODO: This is doing something similar to what WT.annotate_iter is
2279
# doing, however it fails slightly because it doesn't know what
2280
# the *other* revision_id is, so it doesn't know how to give the
2281
# other as the origin for some lines, they all get
2282
# 'default_revision'
2283
# It would be nice to be able to use the new Annotator based
2284
# approach, as well.
2285
return annotate.reannotate([old_annotation],
2286
self.get_file(file_id).readlines(),
2289
def get_symlink_target(self, file_id):
2290
"""See Tree.get_symlink_target"""
2291
if not self._content_change(file_id):
2292
return self._transform._tree.get_symlink_target(file_id)
2293
trans_id = self._transform.trans_id_file_id(file_id)
2294
name = self._transform._limbo_name(trans_id)
2295
return osutils.readlink(name)
2297
def walkdirs(self, prefix=''):
2298
pending = [self._transform.root]
2299
while len(pending) > 0:
2300
parent_id = pending.pop()
2303
prefix = prefix.rstrip('/')
2304
parent_path = self._final_paths.get_path(parent_id)
2305
parent_file_id = self._transform.final_file_id(parent_id)
2306
for child_id in self._all_children(parent_id):
2307
path_from_root = self._final_paths.get_path(child_id)
2308
basename = self._transform.final_name(child_id)
2309
file_id = self._transform.final_file_id(child_id)
2310
kind = self._transform.final_kind(child_id)
2311
if kind is not None:
2312
versioned_kind = kind
2315
versioned_kind = self._transform._tree.stored_kind(file_id)
2316
if versioned_kind == 'directory':
2317
subdirs.append(child_id)
2318
children.append((path_from_root, basename, kind, None,
2319
file_id, versioned_kind))
2321
if parent_path.startswith(prefix):
2322
yield (parent_path, parent_file_id), children
2323
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2326
def get_parent_ids(self):
2327
return self._parent_ids
2329
def set_parent_ids(self, parent_ids):
2330
self._parent_ids = parent_ids
2332
def get_revision_tree(self, revision_id):
2333
return self._transform._tree.get_revision_tree(revision_id)
2336
1080
def joinpath(parent, child):
2337
1081
"""Join tree-relative paths, handling the tree root specially"""