1110
1201
(from_executable, to_executable)))
1111
1202
return iter(sorted(results, key=lambda x:x[1]))
1113
def get_preview_tree(self):
1114
"""Return a tree representing the result of the transform.
1116
This tree only supports the subset of Tree functionality required
1117
by show_diff_trees. It must only be compared to tt._tree.
1119
return _PreviewTree(self)
1122
class TreeTransform(TreeTransformBase):
1123
"""Represent a tree transformation.
1125
This object is designed to support incremental generation of the transform,
1128
However, it gives optimum performance when parent directories are created
1129
before their contents. The transform is then able to put child files
1130
directly in their parent directory, avoiding later renames.
1132
It is easy to produce malformed transforms, but they are generally
1133
harmless. Attempting to apply a malformed transform will cause an
1134
exception to be raised before any modifications are made to the tree.
1136
Many kinds of malformed transforms can be corrected with the
1137
resolve_conflicts function. The remaining ones indicate programming error,
1138
such as trying to create a file with no path.
1140
Two sets of file creation methods are supplied. Convenience methods are:
1145
These are composed of the low-level methods:
1147
* create_file or create_directory or create_symlink
1151
Transform/Transaction ids
1152
-------------------------
1153
trans_ids are temporary ids assigned to all files involved in a transform.
1154
It's possible, even common, that not all files in the Tree have trans_ids.
1156
trans_ids are used because filenames and file_ids are not good enough
1157
identifiers; filenames change, and not all files have file_ids. File-ids
1158
are also associated with trans-ids, so that moving a file moves its
1161
trans_ids are only valid for the TreeTransform that generated them.
1165
Limbo is a temporary directory use to hold new versions of files.
1166
Files are added to limbo by create_file, create_directory, create_symlink,
1167
and their convenience variants (new_*). Files may be removed from limbo
1168
using cancel_creation. Files are renamed from limbo into their final
1169
location as part of TreeTransform.apply
1171
Limbo must be cleaned up, by either calling TreeTransform.apply or
1172
calling TreeTransform.finalize.
1174
Files are placed into limbo inside their parent directories, where
1175
possible. This reduces subsequent renames, and makes operations involving
1176
lots of files faster. This optimization is only possible if the parent
1177
directory is created *before* creating any of its children, so avoid
1178
creating children before parents, where possible.
1182
This temporary directory is used by _FileMover for storing files that are
1183
about to be deleted. In case of rollback, the files will be restored.
1184
FileMover does not delete files until it is sure that a rollback will not
1187
def __init__(self, tree, pb=DummyProgress()):
1188
"""Note: a tree_write lock is taken on the tree.
1190
Use TreeTransform.finalize() to release the lock (can be omitted if
1191
TreeTransform.apply() called).
1193
tree.lock_tree_write()
1196
limbodir = urlutils.local_path_from_url(
1197
tree._transport.abspath('limbo'))
1201
if e.errno == errno.EEXIST:
1202
raise ExistingLimbo(limbodir)
1203
deletiondir = urlutils.local_path_from_url(
1204
tree._transport.abspath('pending-deletion'))
1206
os.mkdir(deletiondir)
1208
if e.errno == errno.EEXIST:
1209
raise errors.ExistingPendingDeletion(deletiondir)
1214
TreeTransformBase.__init__(self, tree, limbodir, pb,
1215
tree.case_sensitive)
1216
self._deletiondir = deletiondir
1218
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1219
"""Apply all changes to the inventory and filesystem.
1221
If filesystem or inventory conflicts are present, MalformedTransform
1224
If apply succeeds, finalize is not necessary.
1226
:param no_conflicts: if True, the caller guarantees there are no
1227
conflicts, so no check is made.
1228
:param precomputed_delta: An inventory delta to use instead of
1230
:param _mover: Supply an alternate FileMover, for testing
1232
if not no_conflicts:
1233
conflicts = self.find_conflicts()
1234
if len(conflicts) != 0:
1235
raise MalformedTransform(conflicts=conflicts)
1236
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1238
if precomputed_delta is None:
1239
child_pb.update('Apply phase', 0, 2)
1240
inventory_delta = self._generate_inventory_delta()
1243
inventory_delta = precomputed_delta
1246
mover = _FileMover()
1250
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1251
self._apply_removals(mover)
1252
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1253
modified_paths = self._apply_insertions(mover)
1258
mover.apply_deletions()
1261
self._tree.apply_inventory_delta(inventory_delta)
1264
return _TransformResults(modified_paths, self.rename_count)
1266
def _generate_inventory_delta(self):
1267
"""Generate an inventory delta for the current transform."""
1268
inventory_delta = []
1269
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1270
new_paths = self._inventory_altered()
1271
total_entries = len(new_paths) + len(self._removed_id)
1273
for num, trans_id in enumerate(self._removed_id):
1275
child_pb.update('removing file', num, total_entries)
1276
if trans_id == self._new_root:
1277
file_id = self._tree.get_root_id()
1279
file_id = self.tree_file_id(trans_id)
1280
# File-id isn't really being deleted, just moved
1281
if file_id in self._r_new_id:
1283
path = self._tree_id_paths[trans_id]
1284
inventory_delta.append((path, None, file_id, None))
1285
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1287
entries = self._tree.iter_entries_by_dir(
1288
new_path_file_ids.values())
1289
old_paths = dict((e.file_id, p) for p, e in entries)
1291
for num, (path, trans_id) in enumerate(new_paths):
1293
child_pb.update('adding file',
1294
num + len(self._removed_id), total_entries)
1295
file_id = new_path_file_ids[trans_id]
1300
kind = self.final_kind(trans_id)
1302
kind = self._tree.stored_kind(file_id)
1303
parent_trans_id = self.final_parent(trans_id)
1304
parent_file_id = new_path_file_ids.get(parent_trans_id)
1305
if parent_file_id is None:
1306
parent_file_id = self.final_file_id(parent_trans_id)
1307
if trans_id in self._new_reference_revision:
1308
new_entry = inventory.TreeReference(
1310
self._new_name[trans_id],
1311
self.final_file_id(self._new_parent[trans_id]),
1312
None, self._new_reference_revision[trans_id])
1314
new_entry = inventory.make_entry(kind,
1315
self.final_name(trans_id),
1316
parent_file_id, file_id)
1317
old_path = old_paths.get(new_entry.file_id)
1318
new_executability = self._new_executability.get(trans_id)
1319
if new_executability is not None:
1320
new_entry.executable = new_executability
1321
inventory_delta.append(
1322
(old_path, path, new_entry.file_id, new_entry))
1325
return inventory_delta
1327
def _apply_removals(self, mover):
1328
"""Perform tree operations that remove directory/inventory names.
1330
That is, delete files that are to be deleted, and put any files that
1331
need renaming into limbo. This must be done in strict child-to-parent
1334
If inventory_delta is None, no inventory delta generation is performed.
1336
tree_paths = list(self._tree_path_ids.iteritems())
1337
tree_paths.sort(reverse=True)
1338
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1340
for num, data in enumerate(tree_paths):
1341
path, trans_id = data
1342
child_pb.update('removing file', num, len(tree_paths))
1343
full_path = self._tree.abspath(path)
1344
if trans_id in self._removed_contents:
1345
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1347
elif trans_id in self._new_name or trans_id in \
1350
mover.rename(full_path, self._limbo_name(trans_id))
1352
if e.errno != errno.ENOENT:
1355
self.rename_count += 1
1359
def _apply_insertions(self, mover):
1360
"""Perform tree operations that insert directory/inventory names.
1362
That is, create any files that need to be created, and restore from
1363
limbo any files that needed renaming. This must be done in strict
1364
parent-to-child order.
1366
If inventory_delta is None, no inventory delta is calculated, and
1367
no list of modified paths is returned.
1369
new_paths = self.new_paths(filesystem_only=True)
1371
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1373
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1375
for num, (path, trans_id) in enumerate(new_paths):
1377
child_pb.update('adding file', num, len(new_paths))
1378
full_path = self._tree.abspath(path)
1379
if trans_id in self._needs_rename:
1381
mover.rename(self._limbo_name(trans_id), full_path)
1383
# We may be renaming a dangling inventory id
1384
if e.errno != errno.ENOENT:
1387
self.rename_count += 1
1388
if (trans_id in self._new_contents or
1389
self.path_changed(trans_id)):
1390
if trans_id in self._new_contents:
1391
modified_paths.append(full_path)
1392
if trans_id in self._new_executability:
1393
self._set_executability(path, trans_id)
1396
self._new_contents.clear()
1397
return modified_paths
1400
class TransformPreview(TreeTransformBase):
1401
"""A TreeTransform for generating preview trees.
1403
Unlike TreeTransform, this version works when the input tree is a
1404
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1405
unversioned files in the input tree.
1408
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1410
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1411
TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1413
def canonical_path(self, path):
1416
def tree_kind(self, trans_id):
1417
path = self._tree_id_paths.get(trans_id)
1419
raise NoSuchFile(None)
1420
file_id = self._tree.path2id(path)
1421
return self._tree.kind(file_id)
1423
def _set_mode(self, trans_id, mode_id, typefunc):
1424
"""Set the mode of new file contents.
1425
The mode_id is the existing file to get the mode from (often the same
1426
as trans_id). The operation is only performed if there's a mode match
1427
according to typefunc.
1429
# is it ok to ignore this? probably
1432
def iter_tree_children(self, parent_id):
1433
"""Iterate through the entry's tree children, if any"""
1435
path = self._tree_id_paths[parent_id]
1438
file_id = self.tree_file_id(parent_id)
1441
children = getattr(self._tree.inventory[file_id], 'children', {})
1442
for child in children:
1443
childpath = joinpath(path, child)
1444
yield self.trans_id_tree_path(childpath)
1447
class _PreviewTree(tree.Tree):
1448
"""Partial implementation of Tree to support show_diff_trees"""
1450
def __init__(self, transform):
1451
self._transform = transform
1452
self._final_paths = FinalPaths(transform)
1453
self.__by_parent = None
1454
self._parent_ids = []
1456
def _changes(self, file_id):
1457
for changes in self._transform.iter_changes():
1458
if changes[0] == file_id:
1461
def _content_change(self, file_id):
1462
"""Return True if the content of this file changed"""
1463
changes = self._changes(file_id)
1464
# changes[2] is true if the file content changed. See
1465
# InterTree.iter_changes.
1466
return (changes is not None and changes[2])
1468
def _get_repository(self):
1469
repo = getattr(self._transform._tree, '_repository', None)
1471
repo = self._transform._tree.branch.repository
1474
def _iter_parent_trees(self):
1475
for revision_id in self.get_parent_ids():
1477
yield self.revision_tree(revision_id)
1478
except errors.NoSuchRevisionInTree:
1479
yield self._get_repository().revision_tree(revision_id)
1481
def _get_file_revision(self, file_id, vf, tree_revision):
1482
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1483
self._iter_parent_trees()]
1484
vf.add_lines((file_id, tree_revision), parent_keys,
1485
self.get_file(file_id).readlines())
1486
repo = self._get_repository()
1487
base_vf = repo.texts
1488
if base_vf not in vf.fallback_versionedfiles:
1489
vf.fallback_versionedfiles.append(base_vf)
1490
return tree_revision
1492
def _stat_limbo_file(self, file_id):
1493
trans_id = self._transform.trans_id_file_id(file_id)
1494
name = self._transform._limbo_name(trans_id)
1495
return os.lstat(name)
1498
def _by_parent(self):
1499
if self.__by_parent is None:
1500
self.__by_parent = self._transform.by_parent()
1501
return self.__by_parent
1503
def lock_read(self):
1504
# Perhaps in theory, this should lock the TreeTransform?
1511
def inventory(self):
1512
"""This Tree does not use inventory as its backing data."""
1513
raise NotImplementedError(_PreviewTree.inventory)
1515
def get_root_id(self):
1516
return self._transform.final_file_id(self._transform.root)
1518
def all_file_ids(self):
1519
tree_ids = set(self._transform._tree.all_file_ids())
1520
tree_ids.difference_update(self._transform.tree_file_id(t)
1521
for t in self._transform._removed_id)
1522
tree_ids.update(self._transform._new_id.values())
1526
return iter(self.all_file_ids())
1528
def paths2ids(self, specific_files, trees=None, require_versioned=False):
1529
"""See Tree.paths2ids"""
1530
to_find = set(specific_files)
1532
for (file_id, paths, changed, versioned, parent, name, kind,
1533
executable) in self._transform.iter_changes():
1534
if paths[1] in to_find:
1536
to_find.remove(paths[1])
1537
result.update(self._transform._tree.paths2ids(to_find,
1538
trees=[], require_versioned=require_versioned))
1541
def _path2trans_id(self, path):
1542
segments = splitpath(path)
1543
cur_parent = self._transform.root
1544
for cur_segment in segments:
1545
for child in self._all_children(cur_parent):
1546
if self._transform.final_name(child) == cur_segment:
1553
def path2id(self, path):
1554
return self._transform.final_file_id(self._path2trans_id(path))
1556
def id2path(self, file_id):
1557
trans_id = self._transform.trans_id_file_id(file_id)
1559
return self._final_paths._determine_path(trans_id)
1561
raise errors.NoSuchId(self, file_id)
1563
def _all_children(self, trans_id):
1564
children = set(self._transform.iter_tree_children(trans_id))
1565
# children in the _new_parent set are provided by _by_parent.
1566
children.difference_update(self._transform._new_parent.keys())
1567
children.update(self._by_parent.get(trans_id, []))
1570
def _make_inv_entries(self, ordered_entries, specific_file_ids):
1571
for trans_id, parent_file_id in ordered_entries:
1572
file_id = self._transform.final_file_id(trans_id)
1575
if (specific_file_ids is not None
1576
and file_id not in specific_file_ids):
1579
kind = self._transform.final_kind(trans_id)
1581
kind = self._transform._tree.stored_kind(file_id)
1582
new_entry = inventory.make_entry(
1584
self._transform.final_name(trans_id),
1585
parent_file_id, file_id)
1586
yield new_entry, trans_id
1588
def _list_files_by_dir(self):
1589
todo = [ROOT_PARENT]
1591
while len(todo) > 0:
1593
parent_file_id = self._transform.final_file_id(parent)
1594
children = list(self._all_children(parent))
1595
paths = dict(zip(children, self._final_paths.get_paths(children)))
1596
children.sort(key=paths.get)
1597
todo.extend(reversed(children))
1598
for trans_id in children:
1599
ordered_ids.append((trans_id, parent_file_id))
1602
def iter_entries_by_dir(self, specific_file_ids=None):
1603
# This may not be a maximally efficient implementation, but it is
1604
# reasonably straightforward. An implementation that grafts the
1605
# TreeTransform changes onto the tree's iter_entries_by_dir results
1606
# might be more efficient, but requires tricky inferences about stack
1608
ordered_ids = self._list_files_by_dir()
1609
for entry, trans_id in self._make_inv_entries(ordered_ids,
1611
yield unicode(self._final_paths.get_path(trans_id)), entry
1613
def list_files(self, include_root=False):
1614
"""See Tree.list_files."""
1615
# XXX This should behave like WorkingTree.list_files, but is really
1616
# more like RevisionTree.list_files.
1617
for path, entry in self.iter_entries_by_dir():
1618
if entry.name == '' and not include_root:
1620
yield path, 'V', entry.kind, entry.file_id, entry
1622
def kind(self, file_id):
1623
trans_id = self._transform.trans_id_file_id(file_id)
1624
return self._transform.final_kind(trans_id)
1626
def stored_kind(self, file_id):
1627
trans_id = self._transform.trans_id_file_id(file_id)
1629
return self._transform._new_contents[trans_id]
1631
return self._transform._tree.stored_kind(file_id)
1633
def get_file_mtime(self, file_id, path=None):
1634
"""See Tree.get_file_mtime"""
1635
if not self._content_change(file_id):
1636
return self._transform._tree.get_file_mtime(file_id, path)
1637
return self._stat_limbo_file(file_id).st_mtime
1639
def get_file_size(self, file_id):
1640
"""See Tree.get_file_size"""
1641
if self.kind(file_id) == 'file':
1642
return self._transform._tree.get_file_size(file_id)
1646
def get_file_sha1(self, file_id, path=None, stat_value=None):
1647
return self._transform._tree.get_file_sha1(file_id)
1649
def is_executable(self, file_id, path=None):
1650
trans_id = self._transform.trans_id_file_id(file_id)
1652
return self._transform._new_executability[trans_id]
1654
return self._transform._tree.is_executable(file_id, path)
1656
def path_content_summary(self, path):
1657
trans_id = self._path2trans_id(path)
1658
tt = self._transform
1659
tree_path = tt._tree_id_paths.get(trans_id)
1660
kind = tt._new_contents.get(trans_id)
1662
if tree_path is None or trans_id in tt._removed_contents:
1663
return 'missing', None, None, None
1664
summary = tt._tree.path_content_summary(tree_path)
1665
kind, size, executable, link_or_sha1 = summary
1668
limbo_name = tt._limbo_name(trans_id)
1669
if trans_id in tt._new_reference_revision:
1670
kind = 'tree-reference'
1672
statval = os.lstat(limbo_name)
1673
size = statval.st_size
1674
if not supports_executable():
1677
executable = statval.st_mode & S_IEXEC
1681
if kind == 'symlink':
1682
link_or_sha1 = os.readlink(limbo_name)
1683
if supports_executable():
1684
executable = tt._new_executability.get(trans_id, executable)
1685
return kind, size, executable, link_or_sha1
1687
def iter_changes(self, from_tree, include_unchanged=False,
1688
specific_files=None, pb=None, extra_trees=None,
1689
require_versioned=True, want_unversioned=False):
1690
"""See InterTree.iter_changes.
1692
This implementation does not support include_unchanged, specific_files,
1693
or want_unversioned. extra_trees, require_versioned, and pb are
1696
if from_tree is not self._transform._tree:
1697
raise ValueError('from_tree must be transform source tree.')
1698
if include_unchanged:
1699
raise ValueError('include_unchanged is not supported')
1700
if specific_files is not None:
1701
raise ValueError('specific_files is not supported')
1702
if want_unversioned:
1703
raise ValueError('want_unversioned is not supported')
1704
return self._transform.iter_changes()
1706
def get_file(self, file_id, path=None):
1707
"""See Tree.get_file"""
1708
if not self._content_change(file_id):
1709
return self._transform._tree.get_file(file_id, path)
1710
trans_id = self._transform.trans_id_file_id(file_id)
1711
name = self._transform._limbo_name(trans_id)
1712
return open(name, 'rb')
1714
def get_file_text(self, file_id):
1715
text_file = self.get_file(file_id)
1717
return text_file.read()
1721
def annotate_iter(self, file_id,
1722
default_revision=_mod_revision.CURRENT_REVISION):
1723
changes = self._changes(file_id)
1727
changed_content, versioned, kind = (changes[2], changes[3],
1731
get_old = (kind[0] == 'file' and versioned[0])
1733
old_annotation = self._transform._tree.annotate_iter(file_id,
1734
default_revision=default_revision)
1738
return old_annotation
1739
if not changed_content:
1740
return old_annotation
1741
return annotate.reannotate([old_annotation],
1742
self.get_file(file_id).readlines(),
1745
def get_symlink_target(self, file_id):
1746
"""See Tree.get_symlink_target"""
1747
if not self._content_change(file_id):
1748
return self._transform._tree.get_symlink_target(file_id)
1749
trans_id = self._transform.trans_id_file_id(file_id)
1750
name = self._transform._limbo_name(trans_id)
1751
return os.readlink(name)
1753
def walkdirs(self, prefix=''):
1754
pending = [self._transform.root]
1755
while len(pending) > 0:
1756
parent_id = pending.pop()
1759
prefix = prefix.rstrip('/')
1760
parent_path = self._final_paths.get_path(parent_id)
1761
parent_file_id = self._transform.final_file_id(parent_id)
1762
for child_id in self._all_children(parent_id):
1763
path_from_root = self._final_paths.get_path(child_id)
1764
basename = self._transform.final_name(child_id)
1765
file_id = self._transform.final_file_id(child_id)
1767
kind = self._transform.final_kind(child_id)
1768
versioned_kind = kind
1771
versioned_kind = self._transform._tree.stored_kind(file_id)
1772
if versioned_kind == 'directory':
1773
subdirs.append(child_id)
1774
children.append((path_from_root, basename, kind, None,
1775
file_id, versioned_kind))
1777
if parent_path.startswith(prefix):
1778
yield (parent_path, parent_file_id), children
1779
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1782
def get_parent_ids(self):
1783
return self._parent_ids
1785
def set_parent_ids(self, parent_ids):
1786
self._parent_ids = parent_ids
1788
def get_revision_tree(self, revision_id):
1789
return self._transform._tree.get_revision_tree(revision_id)
1792
1205
def joinpath(parent, child):
1793
1206
"""Join tree-relative paths, handling the tree root specially"""