575
390
def new_contents(self, trans_id):
576
391
return (trans_id in self._new_contents)
578
def find_conflicts(self):
393
def find_raw_conflicts(self):
579
394
"""Find any violations of inventory or filesystem invariants"""
580
if self._done is True:
581
raise ReusingTransform()
583
# ensure all children of all existent parents are known
584
# all children of non-existent parents are known, by definition.
585
self._add_tree_children()
586
by_parent = self.by_parent()
587
conflicts.extend(self._unversioned_parents(by_parent))
588
conflicts.extend(self._parent_loops())
589
conflicts.extend(self._duplicate_entries(by_parent))
590
conflicts.extend(self._duplicate_ids())
591
conflicts.extend(self._parent_type_conflicts(by_parent))
592
conflicts.extend(self._improper_versioning())
593
conflicts.extend(self._executability_conflicts())
594
conflicts.extend(self._overwrite_conflicts())
597
def _check_malformed(self):
598
conflicts = self.find_conflicts()
599
if len(conflicts) != 0:
600
raise MalformedTransform(conflicts=conflicts)
602
def _add_tree_children(self):
603
"""Add all the children of all active parents to the known paths.
605
Active parents are those which gain children, and those which are
606
removed. This is a necessary first step in detecting conflicts.
608
parents = list(self.by_parent())
609
parents.extend([t for t in self._removed_contents if
610
self.tree_kind(t) == 'directory'])
611
for trans_id in self._removed_id:
612
path = self.tree_path(trans_id)
614
if self._tree.stored_kind(path) == 'directory':
615
parents.append(trans_id)
616
elif self.tree_kind(trans_id) == 'directory':
617
parents.append(trans_id)
619
for parent_id in parents:
620
# ensure that all children are registered with the transaction
621
list(self.iter_tree_children(parent_id))
623
def _has_named_child(self, name, parent_id, known_children):
624
"""Does a parent already have a name child.
626
:param name: The searched for name.
628
:param parent_id: The parent for which the check is made.
630
:param known_children: The already known children. This should have
631
been recently obtained from `self.by_parent.get(parent_id)`
632
(or will be if None is passed).
634
if known_children is None:
635
known_children = self.by_parent().get(parent_id, [])
636
for child in known_children:
637
if self.final_name(child) == name:
639
parent_path = self._tree_id_paths.get(parent_id, None)
640
if parent_path is None:
641
# No parent... no children
643
child_path = joinpath(parent_path, name)
644
child_id = self._tree_path_ids.get(child_path, None)
646
# Not known by the tree transform yet, check the filesystem
647
return osutils.lexists(self._tree.abspath(child_path))
649
raise AssertionError('child_id is missing: %s, %s, %s'
650
% (name, parent_id, child_id))
652
def _available_backup_name(self, name, target_id):
653
"""Find an available backup name.
655
:param name: The basename of the file.
657
:param target_id: The directory trans_id where the backup should
660
known_children = self.by_parent().get(target_id, [])
661
return osutils.available_backup_name(
663
lambda base: self._has_named_child(
664
base, target_id, known_children))
666
def _parent_loops(self):
667
"""No entry should be its own ancestor"""
669
for trans_id in self._new_parent:
672
while parent_id != ROOT_PARENT:
675
parent_id = self.final_parent(parent_id)
678
if parent_id == trans_id:
679
conflicts.append(('parent loop', trans_id))
680
if parent_id in seen:
684
def _unversioned_parents(self, by_parent):
685
"""If parent directories are versioned, children must be versioned."""
687
for parent_id, children in viewitems(by_parent):
688
if parent_id == ROOT_PARENT:
690
if self.final_file_id(parent_id) is not None:
692
for child_id in children:
693
if self.final_file_id(child_id) is not None:
694
conflicts.append(('unversioned parent', parent_id))
698
def _improper_versioning(self):
699
"""Cannot version a file with no contents, or a bad type.
701
However, existing entries with no contents are okay.
704
for trans_id in self._new_id:
705
kind = self.final_kind(trans_id)
706
if kind == 'symlink' and not self._tree.supports_symlinks():
707
# Ignore symlinks as they are not supported on this platform
710
conflicts.append(('versioning no contents', trans_id))
712
if not self._tree.versionable_kind(kind):
713
conflicts.append(('versioning bad kind', trans_id, kind))
716
def _executability_conflicts(self):
717
"""Check for bad executability changes.
719
Only versioned files may have their executability set, because
720
1. only versioned entries can have executability under windows
721
2. only files can be executable. (The execute bit on a directory
722
does not indicate searchability)
725
for trans_id in self._new_executability:
726
if self.final_file_id(trans_id) is None:
727
conflicts.append(('unversioned executability', trans_id))
729
if self.final_kind(trans_id) != "file":
730
conflicts.append(('non-file executability', trans_id))
733
def _overwrite_conflicts(self):
734
"""Check for overwrites (not permitted on Win32)"""
736
for trans_id in self._new_contents:
737
if self.tree_kind(trans_id) is None:
739
if trans_id not in self._removed_contents:
740
conflicts.append(('overwrite', trans_id,
741
self.final_name(trans_id)))
744
def _duplicate_entries(self, by_parent):
745
"""No directory may have two entries with the same name."""
747
if (self._new_name, self._new_parent) == ({}, {}):
749
for children in viewvalues(by_parent):
751
for child_tid in children:
752
name = self.final_name(child_tid)
754
# Keep children only if they still exist in the end
755
if not self._case_sensitive_target:
757
name_ids.append((name, child_tid))
761
for name, trans_id in name_ids:
762
kind = self.final_kind(trans_id)
763
file_id = self.final_file_id(trans_id)
764
if kind is None and file_id is None:
766
if name == last_name:
767
conflicts.append(('duplicate', last_trans_id, trans_id,
770
last_trans_id = trans_id
773
def _duplicate_ids(self):
774
"""Each inventory id may only be used once"""
777
all_ids = self._tree.all_file_ids()
778
except errors.UnsupportedOperation:
779
# it's okay for non-file-id trees to raise UnsupportedOperation.
781
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
783
active_tree_ids = all_ids.difference(removed_tree_ids)
784
for trans_id, file_id in viewitems(self._new_id):
785
if file_id in active_tree_ids:
786
path = self._tree.id2path(file_id)
787
old_trans_id = self.trans_id_tree_path(path)
788
conflicts.append(('duplicate id', old_trans_id, trans_id))
791
def _parent_type_conflicts(self, by_parent):
792
"""Children must have a directory parent"""
794
for parent_id, children in viewitems(by_parent):
795
if parent_id == ROOT_PARENT:
798
for child_id in children:
799
if self.final_kind(child_id) is not None:
804
# There is at least a child, so we need an existing directory to
806
kind = self.final_kind(parent_id)
808
# The directory will be deleted
809
conflicts.append(('missing parent', parent_id))
810
elif kind != "directory":
811
# Meh, we need a *directory* to put something in it
812
conflicts.append(('non-directory parent', parent_id))
815
def _set_executability(self, path, trans_id):
816
"""Set the executability of versioned files """
817
if self._tree._supports_executable():
818
new_executability = self._new_executability[trans_id]
819
abspath = self._tree.abspath(path)
820
current_mode = os.stat(abspath).st_mode
821
if new_executability:
824
to_mode = current_mode | (0o100 & ~umask)
825
# Enable x-bit for others only if they can read it.
826
if current_mode & 0o004:
827
to_mode |= 0o001 & ~umask
828
if current_mode & 0o040:
829
to_mode |= 0o010 & ~umask
831
to_mode = current_mode & ~0o111
832
osutils.chmod_if_possible(abspath, to_mode)
834
def _new_entry(self, name, parent_id, file_id):
835
"""Helper function to create a new filesystem entry."""
836
trans_id = self.create_path(name, parent_id)
837
if file_id is not None:
838
self.version_file(trans_id, file_id=file_id)
395
raise NotImplementedError(self.find_raw_conflicts)
841
397
def new_file(self, name, parent_id, contents, file_id=None,
842
398
executable=None, sha1=None):
1292
522
"""Cancel the creation of new file contents."""
1293
523
raise NotImplementedError(self.cancel_creation)
1296
class DiskTreeTransform(TreeTransformBase):
1297
"""Tree transform storing its contents on disk."""
1299
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1301
:param tree: The tree that will be transformed, but not necessarily
1303
:param limbodir: A directory where new files can be stored until
1304
they are installed in their proper places
1306
:param case_sensitive: If True, the target of the transform is
1307
case sensitive, not just case preserving.
1309
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1310
self._limbodir = limbodir
1311
self._deletiondir = None
1312
# A mapping of transform ids to their limbo filename
1313
self._limbo_files = {}
1314
self._possibly_stale_limbo_files = set()
1315
# A mapping of transform ids to a set of the transform ids of children
1316
# that their limbo directory has
1317
self._limbo_children = {}
1318
# Map transform ids to maps of child filename to child transform id
1319
self._limbo_children_names = {}
1320
# List of transform ids that need to be renamed from limbo into place
1321
self._needs_rename = set()
1322
self._creation_mtime = None
1323
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1326
"""Release the working tree lock, if held, clean up limbo dir.
1328
This is required if apply has not been invoked, but can be invoked
1331
if self._tree is None:
1334
limbo_paths = list(viewvalues(self._limbo_files))
1335
limbo_paths.extend(self._possibly_stale_limbo_files)
1336
limbo_paths.sort(reverse=True)
1337
for path in limbo_paths:
1340
except OSError as e:
1341
if e.errno != errno.ENOENT:
1343
# XXX: warn? perhaps we just got interrupted at an
1344
# inconvenient moment, but perhaps files are disappearing
1347
delete_any(self._limbodir)
1349
# We don't especially care *why* the dir is immortal.
1350
raise ImmortalLimbo(self._limbodir)
1352
if self._deletiondir is not None:
1353
delete_any(self._deletiondir)
1355
raise errors.ImmortalPendingDeletion(self._deletiondir)
1357
TreeTransformBase.finalize(self)
1359
def _limbo_supports_executable(self):
1360
"""Check if the limbo path supports the executable bit."""
1361
return osutils.supports_executable(self._limbodir)
1363
def _limbo_name(self, trans_id):
1364
"""Generate the limbo name of a file"""
1365
limbo_name = self._limbo_files.get(trans_id)
1366
if limbo_name is None:
1367
limbo_name = self._generate_limbo_path(trans_id)
1368
self._limbo_files[trans_id] = limbo_name
1371
def _generate_limbo_path(self, trans_id):
1372
"""Generate a limbo path using the trans_id as the relative path.
1374
This is suitable as a fallback, and when the transform should not be
1375
sensitive to the path encoding of the limbo directory.
1377
self._needs_rename.add(trans_id)
1378
return pathjoin(self._limbodir, trans_id)
1380
def adjust_path(self, name, parent, trans_id):
1381
previous_parent = self._new_parent.get(trans_id)
1382
previous_name = self._new_name.get(trans_id)
1383
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1384
if (trans_id in self._limbo_files
1385
and trans_id not in self._needs_rename):
1386
self._rename_in_limbo([trans_id])
1387
if previous_parent != parent:
1388
self._limbo_children[previous_parent].remove(trans_id)
1389
if previous_parent != parent or previous_name != name:
1390
del self._limbo_children_names[previous_parent][previous_name]
1392
def _rename_in_limbo(self, trans_ids):
1393
"""Fix limbo names so that the right final path is produced.
1395
This means we outsmarted ourselves-- we tried to avoid renaming
1396
these files later by creating them with their final names in their
1397
final parents. But now the previous name or parent is no longer
1398
suitable, so we have to rename them.
1400
Even for trans_ids that have no new contents, we must remove their
1401
entries from _limbo_files, because they are now stale.
1403
for trans_id in trans_ids:
1404
old_path = self._limbo_files[trans_id]
1405
self._possibly_stale_limbo_files.add(old_path)
1406
del self._limbo_files[trans_id]
1407
if trans_id not in self._new_contents:
1409
new_path = self._limbo_name(trans_id)
1410
os.rename(old_path, new_path)
1411
self._possibly_stale_limbo_files.remove(old_path)
1412
for descendant in self._limbo_descendants(trans_id):
1413
desc_path = self._limbo_files[descendant]
1414
desc_path = new_path + desc_path[len(old_path):]
1415
self._limbo_files[descendant] = desc_path
1417
def _limbo_descendants(self, trans_id):
1418
"""Return the set of trans_ids whose limbo paths descend from this."""
1419
descendants = set(self._limbo_children.get(trans_id, []))
1420
for descendant in list(descendants):
1421
descendants.update(self._limbo_descendants(descendant))
1424
def _set_mode(self, trans_id, mode_id, typefunc):
1425
raise NotImplementedError(self._set_mode)
1427
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1428
"""Schedule creation of a new file.
1432
:param contents: an iterator of strings, all of which will be written
1433
to the target destination.
1434
:param trans_id: TreeTransform handle
1435
:param mode_id: If not None, force the mode of the target file to match
1436
the mode of the object referenced by mode_id.
1437
Otherwise, we will try to preserve mode bits of an existing file.
1438
:param sha1: If the sha1 of this content is already known, pass it in.
1439
We can use it to prevent future sha1 computations.
1441
name = self._limbo_name(trans_id)
1442
with open(name, 'wb') as f:
1443
unique_add(self._new_contents, trans_id, 'file')
1444
f.writelines(contents)
1445
self._set_mtime(name)
1446
self._set_mode(trans_id, mode_id, S_ISREG)
1447
# It is unfortunate we have to use lstat instead of fstat, but we just
1448
# used utime and chmod on the file, so we need the accurate final
1450
if sha1 is not None:
1451
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1453
def _read_symlink_target(self, trans_id):
1454
return os.readlink(self._limbo_name(trans_id))
1456
def _set_mtime(self, path):
1457
"""All files that are created get the same mtime.
1459
This time is set by the first object to be created.
1461
if self._creation_mtime is None:
1462
self._creation_mtime = time.time()
1463
os.utime(path, (self._creation_mtime, self._creation_mtime))
1465
def create_hardlink(self, path, trans_id):
1466
"""Schedule creation of a hard link"""
1467
name = self._limbo_name(trans_id)
1470
except OSError as e:
1471
if e.errno != errno.EPERM:
1473
raise errors.HardLinkNotSupported(path)
1475
unique_add(self._new_contents, trans_id, 'file')
1476
except BaseException:
1477
# Clean up the file, it never got registered so
1478
# TreeTransform.finalize() won't clean it up.
1482
def create_directory(self, trans_id):
1483
"""Schedule creation of a new directory.
1485
See also new_directory.
1487
os.mkdir(self._limbo_name(trans_id))
1488
unique_add(self._new_contents, trans_id, 'directory')
1490
def create_symlink(self, target, trans_id):
1491
"""Schedule creation of a new symbolic link.
1493
target is a bytestring.
1494
See also new_symlink.
1496
if self._create_symlinks:
1497
os.symlink(target, self._limbo_name(trans_id))
1500
path = FinalPaths(self).get_path(trans_id)
1504
'Unable to create symlink "%s" on this filesystem.' % (path,))
1505
# We add symlink to _new_contents even if they are unsupported
1506
# and not created. These entries are subsequently used to avoid
1507
# conflicts on platforms that don't support symlink
1508
unique_add(self._new_contents, trans_id, 'symlink')
1510
def cancel_creation(self, trans_id):
1511
"""Cancel the creation of new file contents."""
1512
del self._new_contents[trans_id]
1513
if trans_id in self._observed_sha1s:
1514
del self._observed_sha1s[trans_id]
1515
children = self._limbo_children.get(trans_id)
1516
# if this is a limbo directory with children, move them before removing
1518
if children is not None:
1519
self._rename_in_limbo(children)
1520
del self._limbo_children[trans_id]
1521
del self._limbo_children_names[trans_id]
1522
delete_any(self._limbo_name(trans_id))
1524
def new_orphan(self, trans_id, parent_id):
1525
conf = self._tree.get_config_stack()
1526
handle_orphan = conf.get('transform.orphan_policy')
1527
handle_orphan(self, trans_id, parent_id)
525
def cook_conflicts(self, raw_conflicts):
528
raise NotImplementedError(self.cook_conflicts)
1530
531
class OrphaningError(errors.BzrError):
1598
599
invalid='warning')
1601
class TreeTransform(DiskTreeTransform):
1602
"""Represent a tree transformation.
1604
This object is designed to support incremental generation of the transform,
1607
However, it gives optimum performance when parent directories are created
1608
before their contents. The transform is then able to put child files
1609
directly in their parent directory, avoiding later renames.
1611
It is easy to produce malformed transforms, but they are generally
1612
harmless. Attempting to apply a malformed transform will cause an
1613
exception to be raised before any modifications are made to the tree.
1615
Many kinds of malformed transforms can be corrected with the
1616
resolve_conflicts function. The remaining ones indicate programming error,
1617
such as trying to create a file with no path.
1619
Two sets of file creation methods are supplied. Convenience methods are:
1624
These are composed of the low-level methods:
1626
* create_file or create_directory or create_symlink
1630
Transform/Transaction ids
1631
-------------------------
1632
trans_ids are temporary ids assigned to all files involved in a transform.
1633
It's possible, even common, that not all files in the Tree have trans_ids.
1635
trans_ids are used because filenames and file_ids are not good enough
1636
identifiers; filenames change, and not all files have file_ids. File-ids
1637
are also associated with trans-ids, so that moving a file moves its
1640
trans_ids are only valid for the TreeTransform that generated them.
1644
Limbo is a temporary directory use to hold new versions of files.
1645
Files are added to limbo by create_file, create_directory, create_symlink,
1646
and their convenience variants (new_*). Files may be removed from limbo
1647
using cancel_creation. Files are renamed from limbo into their final
1648
location as part of TreeTransform.apply
1650
Limbo must be cleaned up, by either calling TreeTransform.apply or
1651
calling TreeTransform.finalize.
1653
Files are placed into limbo inside their parent directories, where
1654
possible. This reduces subsequent renames, and makes operations involving
1655
lots of files faster. This optimization is only possible if the parent
1656
directory is created *before* creating any of its children, so avoid
1657
creating children before parents, where possible.
1661
This temporary directory is used by _FileMover for storing files that are
1662
about to be deleted. In case of rollback, the files will be restored.
1663
FileMover does not delete files until it is sure that a rollback will not
1667
def __init__(self, tree, pb=None):
1668
"""Note: a tree_write lock is taken on the tree.
1670
Use TreeTransform.finalize() to release the lock (can be omitted if
1671
TreeTransform.apply() called).
1673
tree.lock_tree_write()
1675
limbodir = urlutils.local_path_from_url(
1676
tree._transport.abspath('limbo'))
1677
osutils.ensure_empty_directory_exists(
1679
errors.ExistingLimbo)
1680
deletiondir = urlutils.local_path_from_url(
1681
tree._transport.abspath('pending-deletion'))
1682
osutils.ensure_empty_directory_exists(
1684
errors.ExistingPendingDeletion)
1685
except BaseException:
1689
# Cache of realpath results, to speed up canonical_path
1690
self._realpaths = {}
1691
# Cache of relpath results, to speed up canonical_path
1693
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1694
tree.case_sensitive)
1695
self._deletiondir = deletiondir
1697
def canonical_path(self, path):
1698
"""Get the canonical tree-relative path"""
1699
# don't follow final symlinks
1700
abs = self._tree.abspath(path)
1701
if abs in self._relpaths:
1702
return self._relpaths[abs]
1703
dirname, basename = os.path.split(abs)
1704
if dirname not in self._realpaths:
1705
self._realpaths[dirname] = os.path.realpath(dirname)
1706
dirname = self._realpaths[dirname]
1707
abs = pathjoin(dirname, basename)
1708
if dirname in self._relpaths:
1709
relpath = pathjoin(self._relpaths[dirname], basename)
1710
relpath = relpath.rstrip('/\\')
1712
relpath = self._tree.relpath(abs)
1713
self._relpaths[abs] = relpath
1716
def tree_kind(self, trans_id):
1717
"""Determine the file kind in the working tree.
1719
:returns: The file kind or None if the file does not exist
1721
path = self._tree_id_paths.get(trans_id)
1725
return file_kind(self._tree.abspath(path))
1726
except errors.NoSuchFile:
1729
def _set_mode(self, trans_id, mode_id, typefunc):
1730
"""Set the mode of new file contents.
1731
The mode_id is the existing file to get the mode from (often the same
1732
as trans_id). The operation is only performed if there's a mode match
1733
according to typefunc.
1738
old_path = self._tree_id_paths[mode_id]
1742
mode = os.stat(self._tree.abspath(old_path)).st_mode
1743
except OSError as e:
1744
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1745
# Either old_path doesn't exist, or the parent of the
1746
# target is not a directory (but will be one eventually)
1747
# Either way, we know it doesn't exist *right now*
1748
# See also bug #248448
1753
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1755
def iter_tree_children(self, parent_id):
1756
"""Iterate through the entry's tree children, if any"""
1758
path = self._tree_id_paths[parent_id]
1762
children = os.listdir(self._tree.abspath(path))
1763
except OSError as e:
1764
if not (osutils._is_error_enotdir(e) or
1765
e.errno in (errno.ENOENT, errno.ESRCH)):
1769
for child in children:
1770
childpath = joinpath(path, child)
1771
if self._tree.is_control_filename(childpath):
1773
yield self.trans_id_tree_path(childpath)
1775
def _generate_limbo_path(self, trans_id):
1776
"""Generate a limbo path using the final path if possible.
1778
This optimizes the performance of applying the tree transform by
1779
avoiding renames. These renames can be avoided only when the parent
1780
directory is already scheduled for creation.
1782
If the final path cannot be used, falls back to using the trans_id as
1785
parent = self._new_parent.get(trans_id)
1786
# if the parent directory is already in limbo (e.g. when building a
1787
# tree), choose a limbo name inside the parent, to reduce further
1789
use_direct_path = False
1790
if self._new_contents.get(parent) == 'directory':
1791
filename = self._new_name.get(trans_id)
1792
if filename is not None:
1793
if parent not in self._limbo_children:
1794
self._limbo_children[parent] = set()
1795
self._limbo_children_names[parent] = {}
1796
use_direct_path = True
1797
# the direct path can only be used if no other file has
1798
# already taken this pathname, i.e. if the name is unused, or
1799
# if it is already associated with this trans_id.
1800
elif self._case_sensitive_target:
1801
if (self._limbo_children_names[parent].get(filename)
1802
in (trans_id, None)):
1803
use_direct_path = True
1805
for l_filename, l_trans_id in viewitems(
1806
self._limbo_children_names[parent]):
1807
if l_trans_id == trans_id:
1809
if l_filename.lower() == filename.lower():
1812
use_direct_path = True
1814
if not use_direct_path:
1815
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1817
limbo_name = pathjoin(self._limbo_files[parent], filename)
1818
self._limbo_children[parent].add(trans_id)
1819
self._limbo_children_names[parent][filename] = trans_id
1822
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1823
"""Apply all changes to the inventory and filesystem.
1825
If filesystem or inventory conflicts are present, MalformedTransform
1828
If apply succeeds, finalize is not necessary.
1830
:param no_conflicts: if True, the caller guarantees there are no
1831
conflicts, so no check is made.
1832
:param precomputed_delta: An inventory delta to use instead of
1834
:param _mover: Supply an alternate FileMover, for testing
1836
for hook in MutableTree.hooks['pre_transform']:
1837
hook(self._tree, self)
1838
if not no_conflicts:
1839
self._check_malformed()
1840
with ui.ui_factory.nested_progress_bar() as child_pb:
1841
if precomputed_delta is None:
1842
child_pb.update(gettext('Apply phase'), 0, 2)
1843
inventory_delta = self._generate_inventory_delta()
1846
inventory_delta = precomputed_delta
1849
mover = _FileMover()
1853
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1854
self._apply_removals(mover)
1855
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1856
modified_paths = self._apply_insertions(mover)
1857
except BaseException:
1861
mover.apply_deletions()
1862
if self.final_file_id(self.root) is None:
1863
inventory_delta = [e for e in inventory_delta if e[0] != '']
1864
self._tree.apply_inventory_delta(inventory_delta)
1865
self._apply_observed_sha1s()
1868
return _TransformResults(modified_paths, self.rename_count)
1870
def _generate_inventory_delta(self):
1871
"""Generate an inventory delta for the current transform."""
1872
inventory_delta = []
1873
new_paths = self._inventory_altered()
1874
total_entries = len(new_paths) + len(self._removed_id)
1875
with ui.ui_factory.nested_progress_bar() as child_pb:
1876
for num, trans_id in enumerate(self._removed_id):
1878
child_pb.update(gettext('removing file'),
1880
if trans_id == self._new_root:
1881
file_id = self._tree.path2id('')
1883
file_id = self.tree_file_id(trans_id)
1884
# File-id isn't really being deleted, just moved
1885
if file_id in self._r_new_id:
1887
path = self._tree_id_paths[trans_id]
1888
inventory_delta.append((path, None, file_id, None))
1889
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1891
for num, (path, trans_id) in enumerate(new_paths):
1893
child_pb.update(gettext('adding file'),
1894
num + len(self._removed_id), total_entries)
1895
file_id = new_path_file_ids[trans_id]
1898
kind = self.final_kind(trans_id)
1900
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1901
parent_trans_id = self.final_parent(trans_id)
1902
parent_file_id = new_path_file_ids.get(parent_trans_id)
1903
if parent_file_id is None:
1904
parent_file_id = self.final_file_id(parent_trans_id)
1905
if trans_id in self._new_reference_revision:
1906
new_entry = inventory.TreeReference(
1908
self._new_name[trans_id],
1909
self.final_file_id(self._new_parent[trans_id]),
1910
None, self._new_reference_revision[trans_id])
1912
new_entry = inventory.make_entry(kind,
1913
self.final_name(trans_id),
1914
parent_file_id, file_id)
1916
old_path = self._tree.id2path(new_entry.file_id)
1917
except errors.NoSuchId:
1919
new_executability = self._new_executability.get(trans_id)
1920
if new_executability is not None:
1921
new_entry.executable = new_executability
1922
inventory_delta.append(
1923
(old_path, path, new_entry.file_id, new_entry))
1924
return inventory_delta
1926
def _apply_removals(self, mover):
1927
"""Perform tree operations that remove directory/inventory names.
1929
That is, delete files that are to be deleted, and put any files that
1930
need renaming into limbo. This must be done in strict child-to-parent
1933
If inventory_delta is None, no inventory delta generation is performed.
1935
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1936
with ui.ui_factory.nested_progress_bar() as child_pb:
1937
for num, (path, trans_id) in enumerate(tree_paths):
1938
# do not attempt to move root into a subdirectory of itself.
1941
child_pb.update(gettext('removing file'), num, len(tree_paths))
1942
full_path = self._tree.abspath(path)
1943
if trans_id in self._removed_contents:
1944
delete_path = os.path.join(self._deletiondir, trans_id)
1945
mover.pre_delete(full_path, delete_path)
1946
elif (trans_id in self._new_name or
1947
trans_id in self._new_parent):
1949
mover.rename(full_path, self._limbo_name(trans_id))
1950
except errors.TransformRenameFailed as e:
1951
if e.errno != errno.ENOENT:
1954
self.rename_count += 1
1956
def _apply_insertions(self, mover):
1957
"""Perform tree operations that insert directory/inventory names.
1959
That is, create any files that need to be created, and restore from
1960
limbo any files that needed renaming. This must be done in strict
1961
parent-to-child order.
1963
If inventory_delta is None, no inventory delta is calculated, and
1964
no list of modified paths is returned.
1966
new_paths = self.new_paths(filesystem_only=True)
1968
with ui.ui_factory.nested_progress_bar() as child_pb:
1969
for num, (path, trans_id) in enumerate(new_paths):
1971
child_pb.update(gettext('adding file'),
1972
num, len(new_paths))
1973
full_path = self._tree.abspath(path)
1974
if trans_id in self._needs_rename:
1976
mover.rename(self._limbo_name(trans_id), full_path)
1977
except errors.TransformRenameFailed as e:
1978
# We may be renaming a dangling inventory id
1979
if e.errno != errno.ENOENT:
1982
self.rename_count += 1
1983
# TODO: if trans_id in self._observed_sha1s, we should
1984
# re-stat the final target, since ctime will be
1985
# updated by the change.
1986
if (trans_id in self._new_contents
1987
or self.path_changed(trans_id)):
1988
if trans_id in self._new_contents:
1989
modified_paths.append(full_path)
1990
if trans_id in self._new_executability:
1991
self._set_executability(path, trans_id)
1992
if trans_id in self._observed_sha1s:
1993
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1994
st = osutils.lstat(full_path)
1995
self._observed_sha1s[trans_id] = (o_sha1, st)
1996
for path, trans_id in new_paths:
1997
# new_paths includes stuff like workingtree conflicts. Only the
1998
# stuff in new_contents actually comes from limbo.
1999
if trans_id in self._limbo_files:
2000
del self._limbo_files[trans_id]
2001
self._new_contents.clear()
2002
return modified_paths
2004
def _apply_observed_sha1s(self):
2005
"""After we have finished renaming everything, update observed sha1s
2007
This has to be done after self._tree.apply_inventory_delta, otherwise
2008
it doesn't know anything about the files we are updating. Also, we want
2009
to do this as late as possible, so that most entries end up cached.
2011
# TODO: this doesn't update the stat information for directories. So
2012
# the first 'bzr status' will still need to rewrite
2013
# .bzr/checkout/dirstate. However, we at least don't need to
2014
# re-read all of the files.
2015
# TODO: If the operation took a while, we could do a time.sleep(3) here
2016
# to allow the clock to tick over and ensure we won't have any
2017
# problems. (we could observe start time, and finish time, and if
2018
# it is less than eg 10% overhead, add a sleep call.)
2019
paths = FinalPaths(self)
2020
for trans_id, observed in viewitems(self._observed_sha1s):
2021
path = paths.get_path(trans_id)
2022
self._tree._observed_sha1(path, observed)
2025
class TransformPreview(DiskTreeTransform):
2026
"""A TreeTransform for generating preview trees.
2028
Unlike TreeTransform, this version works when the input tree is a
2029
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
2030
unversioned files in the input tree.
2033
def __init__(self, tree, pb=None, case_sensitive=True):
2035
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
2036
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
2038
def tree_kind(self, trans_id):
2039
path = self._tree_id_paths.get(trans_id)
2042
kind = self._tree.path_content_summary(path)[0]
2043
if kind == 'missing':
2047
def _set_mode(self, trans_id, mode_id, typefunc):
2048
"""Set the mode of new file contents.
2049
The mode_id is the existing file to get the mode from (often the same
2050
as trans_id). The operation is only performed if there's a mode match
2051
according to typefunc.
2053
# is it ok to ignore this? probably
2056
def iter_tree_children(self, parent_id):
2057
"""Iterate through the entry's tree children, if any"""
2059
path = self._tree_id_paths[parent_id]
2063
entry = next(self._tree.iter_entries_by_dir(
2064
specific_files=[path]))[1]
2065
except StopIteration:
2067
children = getattr(entry, 'children', {})
2068
for child in children:
2069
childpath = joinpath(path, child)
2070
yield self.trans_id_tree_path(childpath)
2072
def new_orphan(self, trans_id, parent_id):
2073
raise NotImplementedError(self.new_orphan)
2076
class _PreviewTree(inventorytree.InventoryTree):
2077
"""Partial implementation of Tree to support show_diff_trees"""
2079
def __init__(self, transform):
2080
self._transform = transform
2081
self._final_paths = FinalPaths(transform)
2082
self.__by_parent = None
2083
self._parent_ids = []
2084
self._all_children_cache = {}
2085
self._path2trans_id_cache = {}
2086
self._final_name_cache = {}
2087
self._iter_changes_cache = dict((c.file_id, c) for c in
2088
self._transform.iter_changes())
2090
def supports_tree_reference(self):
2091
# TODO(jelmer): Support tree references in _PreviewTree.
2092
# return self._transform._tree.supports_tree_reference()
2095
def _content_change(self, file_id):
2096
"""Return True if the content of this file changed"""
2097
changes = self._iter_changes_cache.get(file_id)
2098
return (changes is not None and changes.changed_content)
2100
def _get_repository(self):
2101
repo = getattr(self._transform._tree, '_repository', None)
2103
repo = self._transform._tree.branch.repository
2106
def _iter_parent_trees(self):
2107
for revision_id in self.get_parent_ids():
2109
yield self.revision_tree(revision_id)
2110
except errors.NoSuchRevisionInTree:
2111
yield self._get_repository().revision_tree(revision_id)
2113
def _get_file_revision(self, path, file_id, vf, tree_revision):
2115
(file_id, t.get_file_revision(t.id2path(file_id)))
2116
for t in self._iter_parent_trees()]
2117
vf.add_lines((file_id, tree_revision), parent_keys,
2118
self.get_file_lines(path))
2119
repo = self._get_repository()
2120
base_vf = repo.texts
2121
if base_vf not in vf.fallback_versionedfiles:
2122
vf.fallback_versionedfiles.append(base_vf)
2123
return tree_revision
2125
def _stat_limbo_file(self, trans_id):
2126
name = self._transform._limbo_name(trans_id)
2127
return os.lstat(name)
2130
def _by_parent(self):
2131
if self.__by_parent is None:
2132
self.__by_parent = self._transform.by_parent()
2133
return self.__by_parent
2135
def _comparison_data(self, entry, path):
2136
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2137
if kind == 'missing':
2141
file_id = self._transform.final_file_id(self._path2trans_id(path))
2142
executable = self.is_executable(path)
2143
return kind, executable, None
2145
def is_locked(self):
2148
def lock_read(self):
2149
# Perhaps in theory, this should lock the TreeTransform?
2150
return lock.LogicalLockResult(self.unlock)
2156
def root_inventory(self):
2157
"""This Tree does not use inventory as its backing data."""
2158
raise NotImplementedError(_PreviewTree.root_inventory)
2160
def all_file_ids(self):
2161
tree_ids = set(self._transform._tree.all_file_ids())
2162
tree_ids.difference_update(self._transform.tree_file_id(t)
2163
for t in self._transform._removed_id)
2164
tree_ids.update(viewvalues(self._transform._new_id))
2167
def all_versioned_paths(self):
2168
tree_paths = set(self._transform._tree.all_versioned_paths())
2170
tree_paths.difference_update(
2171
self._transform.trans_id_tree_path(t)
2172
for t in self._transform._removed_id)
2175
self._final_paths._determine_path(t)
2176
for t in self._transform._new_id)
2180
def _path2trans_id(self, path):
2181
# We must not use None here, because that is a valid value to store.
2182
trans_id = self._path2trans_id_cache.get(path, object)
2183
if trans_id is not object:
2185
segments = splitpath(path)
2186
cur_parent = self._transform.root
2187
for cur_segment in segments:
2188
for child in self._all_children(cur_parent):
2189
final_name = self._final_name_cache.get(child)
2190
if final_name is None:
2191
final_name = self._transform.final_name(child)
2192
self._final_name_cache[child] = final_name
2193
if final_name == cur_segment:
2197
self._path2trans_id_cache[path] = None
2199
self._path2trans_id_cache[path] = cur_parent
2202
def path2id(self, path):
2203
if isinstance(path, list):
2206
path = osutils.pathjoin(*path)
2207
return self._transform.final_file_id(self._path2trans_id(path))
2209
def id2path(self, file_id, recurse='down'):
2210
trans_id = self._transform.trans_id_file_id(file_id)
2212
return self._final_paths._determine_path(trans_id)
2214
raise errors.NoSuchId(self, file_id)
2216
def _all_children(self, trans_id):
2217
children = self._all_children_cache.get(trans_id)
2218
if children is not None:
2220
children = set(self._transform.iter_tree_children(trans_id))
2221
# children in the _new_parent set are provided by _by_parent.
2222
children.difference_update(self._transform._new_parent)
2223
children.update(self._by_parent.get(trans_id, []))
2224
self._all_children_cache[trans_id] = children
2228
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2229
in self._transform._tree.extras())
2230
possible_extras.update(self._transform._new_contents)
2231
possible_extras.update(self._transform._removed_id)
2232
for trans_id in possible_extras:
2233
if self._transform.final_file_id(trans_id) is None:
2234
yield self._final_paths._determine_path(trans_id)
2236
def _make_inv_entries(self, ordered_entries, specific_files=None):
2237
for trans_id, parent_file_id in ordered_entries:
2238
file_id = self._transform.final_file_id(trans_id)
2241
if (specific_files is not None
2242
and self._final_paths.get_path(trans_id) not in specific_files):
2244
kind = self._transform.final_kind(trans_id)
2246
kind = self._transform._tree.stored_kind(
2247
self._transform._tree.id2path(file_id))
2248
new_entry = inventory.make_entry(
2250
self._transform.final_name(trans_id),
2251
parent_file_id, file_id)
2252
yield new_entry, trans_id
2254
def _list_files_by_dir(self):
2255
todo = [ROOT_PARENT]
2257
while len(todo) > 0:
2259
parent_file_id = self._transform.final_file_id(parent)
2260
children = list(self._all_children(parent))
2261
paths = dict(zip(children, self._final_paths.get_paths(children)))
2262
children.sort(key=paths.get)
2263
todo.extend(reversed(children))
2264
for trans_id in children:
2265
ordered_ids.append((trans_id, parent_file_id))
2268
def iter_child_entries(self, path):
2269
trans_id = self._path2trans_id(path)
2270
if trans_id is None:
2271
raise errors.NoSuchFile(path)
2272
todo = [(child_trans_id, trans_id) for child_trans_id in
2273
self._all_children(trans_id)]
2274
for entry, trans_id in self._make_inv_entries(todo):
2277
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
2279
raise NotImplementedError(
2280
'follow tree references not yet supported')
2282
# This may not be a maximally efficient implementation, but it is
2283
# reasonably straightforward. An implementation that grafts the
2284
# TreeTransform changes onto the tree's iter_entries_by_dir results
2285
# might be more efficient, but requires tricky inferences about stack
2287
ordered_ids = self._list_files_by_dir()
2288
for entry, trans_id in self._make_inv_entries(ordered_ids,
2290
yield self._final_paths.get_path(trans_id), entry
2292
def _iter_entries_for_dir(self, dir_path):
2293
"""Return path, entry for items in a directory without recursing down."""
2295
dir_trans_id = self._path2trans_id(dir_path)
2296
dir_id = self._transform.final_file_id(dir_trans_id)
2297
for child_trans_id in self._all_children(dir_trans_id):
2298
ordered_ids.append((child_trans_id, dir_id))
2300
for entry, trans_id in self._make_inv_entries(ordered_ids):
2301
path_entries.append((self._final_paths.get_path(trans_id), entry))
2305
def list_files(self, include_root=False, from_dir=None, recursive=True,
2306
recurse_nested=False):
2307
"""See WorkingTree.list_files."""
2309
raise NotImplementedError(
2310
'follow tree references not yet supported')
2312
# XXX This should behave like WorkingTree.list_files, but is really
2313
# more like RevisionTree.list_files.
2319
prefix = from_dir + '/'
2320
entries = self.iter_entries_by_dir()
2321
for path, entry in entries:
2322
if entry.name == '' and not include_root:
2325
if not path.startswith(prefix):
2327
path = path[len(prefix):]
2328
yield path, 'V', entry.kind, entry
2330
if from_dir is None and include_root is True:
2331
root_entry = inventory.make_entry(
2332
'directory', '', ROOT_PARENT, self.path2id(''))
2333
yield '', 'V', 'directory', root_entry
2334
entries = self._iter_entries_for_dir(from_dir or '')
2335
for path, entry in entries:
2336
yield path, 'V', entry.kind, entry
2338
def kind(self, path):
2339
trans_id = self._path2trans_id(path)
2340
if trans_id is None:
2341
raise errors.NoSuchFile(path)
2342
return self._transform.final_kind(trans_id)
2344
def stored_kind(self, path):
2345
trans_id = self._path2trans_id(path)
2346
if trans_id is None:
2347
raise errors.NoSuchFile(path)
2349
return self._transform._new_contents[trans_id]
2351
return self._transform._tree.stored_kind(path)
2353
def get_file_mtime(self, path):
2354
"""See Tree.get_file_mtime"""
2355
file_id = self.path2id(path)
2357
raise errors.NoSuchFile(path)
2358
if not self._content_change(file_id):
2359
return self._transform._tree.get_file_mtime(
2360
self._transform._tree.id2path(file_id))
2361
trans_id = self._path2trans_id(path)
2362
return self._stat_limbo_file(trans_id).st_mtime
2364
def get_file_size(self, path):
2365
"""See Tree.get_file_size"""
2366
trans_id = self._path2trans_id(path)
2367
if trans_id is None:
2368
raise errors.NoSuchFile(path)
2369
kind = self._transform.final_kind(trans_id)
2372
if trans_id in self._transform._new_contents:
2373
return self._stat_limbo_file(trans_id).st_size
2374
if self.kind(path) == 'file':
2375
return self._transform._tree.get_file_size(path)
2379
def get_file_verifier(self, path, stat_value=None):
2380
trans_id = self._path2trans_id(path)
2381
if trans_id is None:
2382
raise errors.NoSuchFile(path)
2383
kind = self._transform._new_contents.get(trans_id)
2385
return self._transform._tree.get_file_verifier(path)
2387
with self.get_file(path) as fileobj:
2388
return ("SHA1", sha_file(fileobj))
2390
def get_file_sha1(self, path, stat_value=None):
2391
trans_id = self._path2trans_id(path)
2392
if trans_id is None:
2393
raise errors.NoSuchFile(path)
2394
kind = self._transform._new_contents.get(trans_id)
2396
return self._transform._tree.get_file_sha1(path)
2398
with self.get_file(path) as fileobj:
2399
return sha_file(fileobj)
2401
def get_reference_revision(self, path):
2402
trans_id = self._path2trans_id(path)
2403
if trans_id is None:
2404
raise errors.NoSuchFile(path)
2405
reference_revision = self._transform._new_reference_revision.get(trans_id)
2406
if reference_revision is None:
2407
return self._transform._tree.get_reference_revision(path)
2408
return reference_revision
2410
def is_executable(self, path):
2411
trans_id = self._path2trans_id(path)
2412
if trans_id is None:
2415
return self._transform._new_executability[trans_id]
2418
return self._transform._tree.is_executable(path)
2419
except OSError as e:
2420
if e.errno == errno.ENOENT:
2423
except errors.NoSuchFile:
2426
def has_filename(self, path):
2427
trans_id = self._path2trans_id(path)
2428
if trans_id in self._transform._new_contents:
2430
elif trans_id in self._transform._removed_contents:
2433
return self._transform._tree.has_filename(path)
2435
def path_content_summary(self, path):
2436
trans_id = self._path2trans_id(path)
2437
tt = self._transform
2438
tree_path = tt._tree_id_paths.get(trans_id)
2439
kind = tt._new_contents.get(trans_id)
2441
if tree_path is None or trans_id in tt._removed_contents:
2442
return 'missing', None, None, None
2443
summary = tt._tree.path_content_summary(tree_path)
2444
kind, size, executable, link_or_sha1 = summary
2447
limbo_name = tt._limbo_name(trans_id)
2448
if trans_id in tt._new_reference_revision:
2449
kind = 'tree-reference'
2451
statval = os.lstat(limbo_name)
2452
size = statval.st_size
2453
if not tt._limbo_supports_executable():
2456
executable = statval.st_mode & S_IEXEC
2460
if kind == 'symlink':
2461
link_or_sha1 = os.readlink(limbo_name)
2462
if not isinstance(link_or_sha1, text_type):
2463
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2464
executable = tt._new_executability.get(trans_id, executable)
2465
return kind, size, executable, link_or_sha1
2467
def iter_changes(self, from_tree, include_unchanged=False,
2468
specific_files=None, pb=None, extra_trees=None,
2469
require_versioned=True, want_unversioned=False):
2470
"""See InterTree.iter_changes.
2472
This has a fast path that is only used when the from_tree matches
2473
the transform tree, and no fancy options are supplied.
2475
if (from_tree is not self._transform._tree or include_unchanged
2476
or specific_files or want_unversioned):
2477
from .bzr.inventorytree import InterInventoryTree
2478
return InterInventoryTree(from_tree, self).iter_changes(
2479
include_unchanged=include_unchanged,
2480
specific_files=specific_files,
2482
extra_trees=extra_trees,
2483
require_versioned=require_versioned,
2484
want_unversioned=want_unversioned)
2485
if want_unversioned:
2486
raise ValueError('want_unversioned is not supported')
2487
return self._transform.iter_changes()
2489
def get_file(self, path):
2490
"""See Tree.get_file"""
2491
file_id = self.path2id(path)
2492
if not self._content_change(file_id):
2493
return self._transform._tree.get_file(path)
2494
trans_id = self._path2trans_id(path)
2495
name = self._transform._limbo_name(trans_id)
2496
return open(name, 'rb')
2498
def get_file_with_stat(self, path):
2499
return self.get_file(path), None
2501
def annotate_iter(self, path,
2502
default_revision=_mod_revision.CURRENT_REVISION):
2503
file_id = self.path2id(path)
2504
changes = self._iter_changes_cache.get(file_id)
2508
changed_content, versioned, kind = (
2509
changes.changed_content, changes.versioned, changes.kind)
2512
get_old = (kind[0] == 'file' and versioned[0])
2514
old_annotation = self._transform._tree.annotate_iter(
2515
path, default_revision=default_revision)
2519
return old_annotation
2520
if not changed_content:
2521
return old_annotation
2522
# TODO: This is doing something similar to what WT.annotate_iter is
2523
# doing, however it fails slightly because it doesn't know what
2524
# the *other* revision_id is, so it doesn't know how to give the
2525
# other as the origin for some lines, they all get
2526
# 'default_revision'
2527
# It would be nice to be able to use the new Annotator based
2528
# approach, as well.
2529
return annotate.reannotate([old_annotation],
2530
self.get_file(path).readlines(),
2533
def get_symlink_target(self, path):
2534
"""See Tree.get_symlink_target"""
2535
file_id = self.path2id(path)
2536
if not self._content_change(file_id):
2537
return self._transform._tree.get_symlink_target(path)
2538
trans_id = self._path2trans_id(path)
2539
name = self._transform._limbo_name(trans_id)
2540
return osutils.readlink(name)
2542
def walkdirs(self, prefix=''):
2543
pending = [self._transform.root]
2544
while len(pending) > 0:
2545
parent_id = pending.pop()
2548
prefix = prefix.rstrip('/')
2549
parent_path = self._final_paths.get_path(parent_id)
2550
parent_file_id = self._transform.final_file_id(parent_id)
2551
for child_id in self._all_children(parent_id):
2552
path_from_root = self._final_paths.get_path(child_id)
2553
basename = self._transform.final_name(child_id)
2554
file_id = self._transform.final_file_id(child_id)
2555
kind = self._transform.final_kind(child_id)
2556
if kind is not None:
2557
versioned_kind = kind
2560
versioned_kind = self._transform._tree.stored_kind(
2561
self._transform._tree.id2path(file_id))
2562
if versioned_kind == 'directory':
2563
subdirs.append(child_id)
2564
children.append((path_from_root, basename, kind, None,
2565
file_id, versioned_kind))
2567
if parent_path.startswith(prefix):
2568
yield (parent_path, parent_file_id), children
2569
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2572
def get_parent_ids(self):
2573
return self._parent_ids
2575
def set_parent_ids(self, parent_ids):
2576
self._parent_ids = parent_ids
2578
def get_revision_tree(self, revision_id):
2579
return self._transform._tree.get_revision_tree(revision_id)
2582
602
def joinpath(parent, child):
2583
603
"""Join tree-relative paths, handling the tree root specially"""
2584
604
if parent is None or parent == "":