395
528
def find_conflicts(self):
396
529
"""Find any violations of inventory or filesystem invariants"""
397
raise NotImplementedError(self.find_conflicts)
530
if self._done is True:
531
raise ReusingTransform()
533
# ensure all children of all existent parents are known
534
# all children of non-existent parents are known, by definition.
535
self._add_tree_children()
536
by_parent = self.by_parent()
537
conflicts.extend(self._unversioned_parents(by_parent))
538
conflicts.extend(self._parent_loops())
539
conflicts.extend(self._duplicate_entries(by_parent))
540
conflicts.extend(self._duplicate_ids())
541
conflicts.extend(self._parent_type_conflicts(by_parent))
542
conflicts.extend(self._improper_versioning())
543
conflicts.extend(self._executability_conflicts())
544
conflicts.extend(self._overwrite_conflicts())
547
def _check_malformed(self):
548
conflicts = self.find_conflicts()
549
if len(conflicts) != 0:
550
raise MalformedTransform(conflicts=conflicts)
552
def _add_tree_children(self):
553
"""Add all the children of all active parents to the known paths.
555
Active parents are those which gain children, and those which are
556
removed. This is a necessary first step in detecting conflicts.
558
parents = list(self.by_parent())
559
parents.extend([t for t in self._removed_contents if
560
self.tree_kind(t) == 'directory'])
561
for trans_id in self._removed_id:
562
path = self.tree_path(trans_id)
564
if self._tree.stored_kind(path) == 'directory':
565
parents.append(trans_id)
566
elif self.tree_kind(trans_id) == 'directory':
567
parents.append(trans_id)
569
for parent_id in parents:
570
# ensure that all children are registered with the transaction
571
list(self.iter_tree_children(parent_id))
573
def _has_named_child(self, name, parent_id, known_children):
574
"""Does a parent already have a name child.
576
:param name: The searched for name.
578
:param parent_id: The parent for which the check is made.
580
:param known_children: The already known children. This should have
581
been recently obtained from `self.by_parent.get(parent_id)`
582
(or will be if None is passed).
584
if known_children is None:
585
known_children = self.by_parent().get(parent_id, [])
586
for child in known_children:
587
if self.final_name(child) == name:
589
parent_path = self._tree_id_paths.get(parent_id, None)
590
if parent_path is None:
591
# No parent... no children
593
child_path = joinpath(parent_path, name)
594
child_id = self._tree_path_ids.get(child_path, None)
596
# Not known by the tree transform yet, check the filesystem
597
return osutils.lexists(self._tree.abspath(child_path))
599
raise AssertionError('child_id is missing: %s, %s, %s'
600
% (name, parent_id, child_id))
602
def _available_backup_name(self, name, target_id):
603
"""Find an available backup name.
605
:param name: The basename of the file.
607
:param target_id: The directory trans_id where the backup should
610
known_children = self.by_parent().get(target_id, [])
611
return osutils.available_backup_name(
613
lambda base: self._has_named_child(
614
base, target_id, known_children))
616
def _parent_loops(self):
617
"""No entry should be its own ancestor"""
619
for trans_id in self._new_parent:
622
while parent_id != ROOT_PARENT:
625
parent_id = self.final_parent(parent_id)
628
if parent_id == trans_id:
629
conflicts.append(('parent loop', trans_id))
630
if parent_id in seen:
634
def _unversioned_parents(self, by_parent):
635
"""If parent directories are versioned, children must be versioned."""
637
for parent_id, children in viewitems(by_parent):
638
if parent_id == ROOT_PARENT:
640
if self.final_file_id(parent_id) is not None:
642
for child_id in children:
643
if self.final_file_id(child_id) is not None:
644
conflicts.append(('unversioned parent', parent_id))
648
def _improper_versioning(self):
649
"""Cannot version a file with no contents, or a bad type.
651
However, existing entries with no contents are okay.
654
for trans_id in self._new_id:
655
kind = self.final_kind(trans_id)
657
conflicts.append(('versioning no contents', trans_id))
659
if not self._tree.versionable_kind(kind):
660
conflicts.append(('versioning bad kind', trans_id, kind))
663
def _executability_conflicts(self):
664
"""Check for bad executability changes.
666
Only versioned files may have their executability set, because
667
1. only versioned entries can have executability under windows
668
2. only files can be executable. (The execute bit on a directory
669
does not indicate searchability)
672
for trans_id in self._new_executability:
673
if self.final_file_id(trans_id) is None:
674
conflicts.append(('unversioned executability', trans_id))
676
if self.final_kind(trans_id) != "file":
677
conflicts.append(('non-file executability', trans_id))
680
def _overwrite_conflicts(self):
681
"""Check for overwrites (not permitted on Win32)"""
683
for trans_id in self._new_contents:
684
if self.tree_kind(trans_id) is None:
686
if trans_id not in self._removed_contents:
687
conflicts.append(('overwrite', trans_id,
688
self.final_name(trans_id)))
691
def _duplicate_entries(self, by_parent):
692
"""No directory may have two entries with the same name."""
694
if (self._new_name, self._new_parent) == ({}, {}):
696
for children in viewvalues(by_parent):
698
for child_tid in children:
699
name = self.final_name(child_tid)
701
# Keep children only if they still exist in the end
702
if not self._case_sensitive_target:
704
name_ids.append((name, child_tid))
708
for name, trans_id in name_ids:
709
kind = self.final_kind(trans_id)
710
file_id = self.final_file_id(trans_id)
711
if kind is None and file_id is None:
713
if name == last_name:
714
conflicts.append(('duplicate', last_trans_id, trans_id,
717
last_trans_id = trans_id
720
def _duplicate_ids(self):
721
"""Each inventory id may only be used once"""
723
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
725
all_ids = self._tree.all_file_ids()
726
active_tree_ids = all_ids.difference(removed_tree_ids)
727
for trans_id, file_id in viewitems(self._new_id):
728
if file_id in active_tree_ids:
729
path = self._tree.id2path(file_id)
730
old_trans_id = self.trans_id_tree_path(path)
731
conflicts.append(('duplicate id', old_trans_id, trans_id))
734
def _parent_type_conflicts(self, by_parent):
735
"""Children must have a directory parent"""
737
for parent_id, children in viewitems(by_parent):
738
if parent_id == ROOT_PARENT:
741
for child_id in children:
742
if self.final_kind(child_id) is not None:
747
# There is at least a child, so we need an existing directory to
749
kind = self.final_kind(parent_id)
751
# The directory will be deleted
752
conflicts.append(('missing parent', parent_id))
753
elif kind != "directory":
754
# Meh, we need a *directory* to put something in it
755
conflicts.append(('non-directory parent', parent_id))
758
def _set_executability(self, path, trans_id):
759
"""Set the executability of versioned files """
760
if self._tree._supports_executable():
761
new_executability = self._new_executability[trans_id]
762
abspath = self._tree.abspath(path)
763
current_mode = os.stat(abspath).st_mode
764
if new_executability:
767
to_mode = current_mode | (0o100 & ~umask)
768
# Enable x-bit for others only if they can read it.
769
if current_mode & 0o004:
770
to_mode |= 0o001 & ~umask
771
if current_mode & 0o040:
772
to_mode |= 0o010 & ~umask
774
to_mode = current_mode & ~0o111
775
osutils.chmod_if_possible(abspath, to_mode)
777
def _new_entry(self, name, parent_id, file_id):
778
"""Helper function to create a new filesystem entry."""
779
trans_id = self.create_path(name, parent_id)
780
if file_id is not None:
781
self.version_file(file_id, trans_id)
399
784
def new_file(self, name, parent_id, contents, file_id=None,
400
785
executable=None, sha1=None):
483
1035
may reduce performance for some non-native formats.)
484
1036
:return: The revision_id of the revision committed.
486
raise NotImplementedError(self.commit)
1038
self._check_malformed()
1040
unversioned = set(self._new_contents).difference(set(self._new_id))
1041
for trans_id in unversioned:
1042
if self.final_file_id(trans_id) is None:
1043
raise errors.StrictCommitFailed()
1045
revno, last_rev_id = branch.last_revision_info()
1046
if last_rev_id == _mod_revision.NULL_REVISION:
1047
if merge_parents is not None:
1048
raise ValueError('Cannot supply merge parents for first'
1052
parent_ids = [last_rev_id]
1053
if merge_parents is not None:
1054
parent_ids.extend(merge_parents)
1055
if self._tree.get_revision_id() != last_rev_id:
1056
raise ValueError('TreeTransform not based on branch basis: %s' %
1057
self._tree.get_revision_id())
1058
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1059
builder = branch.get_commit_builder(parent_ids,
1060
timestamp=timestamp,
1062
committer=committer,
1064
revision_id=revision_id)
1065
preview = self.get_preview_tree()
1066
list(builder.record_iter_changes(preview, last_rev_id,
1067
self.iter_changes()))
1068
builder.finish_inventory()
1069
revision_id = builder.commit(message)
1070
branch.set_last_revision_info(revno + 1, revision_id)
1073
def _text_parent(self, trans_id):
1074
path = self.tree_path(trans_id)
1076
if path is None or self._tree.kind(path) != 'file':
1078
except errors.NoSuchFile:
1082
def _get_parents_texts(self, trans_id):
1083
"""Get texts for compression parents of this file."""
1084
path = self._text_parent(trans_id)
1087
return (self._tree.get_file_text(path),)
1089
def _get_parents_lines(self, trans_id):
1090
"""Get lines for compression parents of this file."""
1091
path = self._text_parent(trans_id)
1094
return (self._tree.get_file_lines(path),)
1096
def serialize(self, serializer):
1097
"""Serialize this TreeTransform.
1099
:param serializer: A Serialiser like pack.ContainerSerializer.
1101
new_name = dict((k, v.encode('utf-8')) for k, v in
1102
viewitems(self._new_name))
1103
new_executability = dict((k, int(v)) for k, v in
1104
viewitems(self._new_executability))
1105
tree_path_ids = dict((k.encode('utf-8'), v)
1106
for k, v in viewitems(self._tree_path_ids))
1108
'_id_number': self._id_number,
1109
'_new_name': new_name,
1110
'_new_parent': self._new_parent,
1111
'_new_executability': new_executability,
1112
'_new_id': self._new_id,
1113
'_tree_path_ids': tree_path_ids,
1114
'_removed_id': list(self._removed_id),
1115
'_removed_contents': list(self._removed_contents),
1116
'_non_present_ids': self._non_present_ids,
1118
yield serializer.bytes_record(bencode.bencode(attribs),
1120
for trans_id, kind in viewitems(self._new_contents):
1122
with open(self._limbo_name(trans_id), 'rb') as cur_file:
1123
lines = cur_file.readlines()
1124
parents = self._get_parents_lines(trans_id)
1125
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1126
content = b''.join(mpdiff.to_patch())
1127
if kind == 'directory':
1129
if kind == 'symlink':
1130
content = self._read_symlink_target(trans_id)
1131
yield serializer.bytes_record(content, ((trans_id, kind),))
1133
def deserialize(self, records):
1134
"""Deserialize a stored TreeTransform.
1136
:param records: An iterable of (names, content) tuples, as per
1137
pack.ContainerPushParser.
1139
names, content = next(records)
1140
attribs = bencode.bdecode(content)
1141
self._id_number = attribs['_id_number']
1142
self._new_name = dict((k, v.decode('utf-8'))
1143
for k, v in viewitems(attribs['_new_name']))
1144
self._new_parent = attribs['_new_parent']
1145
self._new_executability = dict((k, bool(v))
1146
for k, v in viewitems(attribs['_new_executability']))
1147
self._new_id = attribs['_new_id']
1148
self._r_new_id = dict((v, k) for k, v in viewitems(self._new_id))
1149
self._tree_path_ids = {}
1150
self._tree_id_paths = {}
1151
for bytepath, trans_id in viewitems(attribs['_tree_path_ids']):
1152
path = bytepath.decode('utf-8')
1153
self._tree_path_ids[path] = trans_id
1154
self._tree_id_paths[trans_id] = path
1155
self._removed_id = set(attribs['_removed_id'])
1156
self._removed_contents = set(attribs['_removed_contents'])
1157
self._non_present_ids = attribs['_non_present_ids']
1158
for ((trans_id, kind),), content in records:
1160
mpdiff = multiparent.MultiParent.from_patch(content)
1161
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1162
self.create_file(lines, trans_id)
1163
if kind == 'directory':
1164
self.create_directory(trans_id)
1165
if kind == 'symlink':
1166
self.create_symlink(content.decode('utf-8'), trans_id)
1169
class DiskTreeTransform(TreeTransformBase):
1170
"""Tree transform storing its contents on disk."""
1172
def __init__(self, tree, limbodir, pb=None,
1173
case_sensitive=True):
1175
:param tree: The tree that will be transformed, but not necessarily
1177
:param limbodir: A directory where new files can be stored until
1178
they are installed in their proper places
1180
:param case_sensitive: If True, the target of the transform is
1181
case sensitive, not just case preserving.
1183
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1184
self._limbodir = limbodir
1185
self._deletiondir = None
1186
# A mapping of transform ids to their limbo filename
1187
self._limbo_files = {}
1188
self._possibly_stale_limbo_files = set()
1189
# A mapping of transform ids to a set of the transform ids of children
1190
# that their limbo directory has
1191
self._limbo_children = {}
1192
# Map transform ids to maps of child filename to child transform id
1193
self._limbo_children_names = {}
1194
# List of transform ids that need to be renamed from limbo into place
1195
self._needs_rename = set()
1196
self._creation_mtime = None
1199
"""Release the working tree lock, if held, clean up limbo dir.
1201
This is required if apply has not been invoked, but can be invoked
1204
if self._tree is None:
1207
limbo_paths = list(viewvalues(self._limbo_files))
1208
limbo_paths.extend(self._possibly_stale_limbo_files)
1209
limbo_paths.sort(reverse=True)
1210
for path in limbo_paths:
1213
except OSError as e:
1214
if e.errno != errno.ENOENT:
1216
# XXX: warn? perhaps we just got interrupted at an
1217
# inconvenient moment, but perhaps files are disappearing
1220
delete_any(self._limbodir)
1222
# We don't especially care *why* the dir is immortal.
1223
raise ImmortalLimbo(self._limbodir)
1225
if self._deletiondir is not None:
1226
delete_any(self._deletiondir)
1228
raise errors.ImmortalPendingDeletion(self._deletiondir)
1230
TreeTransformBase.finalize(self)
1232
def _limbo_supports_executable(self):
1233
"""Check if the limbo path supports the executable bit."""
1234
# FIXME: Check actual file system capabilities of limbodir
1235
return osutils.supports_executable()
1237
def _limbo_name(self, trans_id):
1238
"""Generate the limbo name of a file"""
1239
limbo_name = self._limbo_files.get(trans_id)
1240
if limbo_name is None:
1241
limbo_name = self._generate_limbo_path(trans_id)
1242
self._limbo_files[trans_id] = limbo_name
1245
def _generate_limbo_path(self, trans_id):
1246
"""Generate a limbo path using the trans_id as the relative path.
1248
This is suitable as a fallback, and when the transform should not be
1249
sensitive to the path encoding of the limbo directory.
1251
self._needs_rename.add(trans_id)
1252
return pathjoin(self._limbodir, trans_id)
1254
def adjust_path(self, name, parent, trans_id):
1255
previous_parent = self._new_parent.get(trans_id)
1256
previous_name = self._new_name.get(trans_id)
1257
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1258
if (trans_id in self._limbo_files and
1259
trans_id not in self._needs_rename):
1260
self._rename_in_limbo([trans_id])
1261
if previous_parent != parent:
1262
self._limbo_children[previous_parent].remove(trans_id)
1263
if previous_parent != parent or previous_name != name:
1264
del self._limbo_children_names[previous_parent][previous_name]
1266
def _rename_in_limbo(self, trans_ids):
1267
"""Fix limbo names so that the right final path is produced.
1269
This means we outsmarted ourselves-- we tried to avoid renaming
1270
these files later by creating them with their final names in their
1271
final parents. But now the previous name or parent is no longer
1272
suitable, so we have to rename them.
1274
Even for trans_ids that have no new contents, we must remove their
1275
entries from _limbo_files, because they are now stale.
1277
for trans_id in trans_ids:
1278
old_path = self._limbo_files[trans_id]
1279
self._possibly_stale_limbo_files.add(old_path)
1280
del self._limbo_files[trans_id]
1281
if trans_id not in self._new_contents:
1283
new_path = self._limbo_name(trans_id)
1284
os.rename(old_path, new_path)
1285
self._possibly_stale_limbo_files.remove(old_path)
1286
for descendant in self._limbo_descendants(trans_id):
1287
desc_path = self._limbo_files[descendant]
1288
desc_path = new_path + desc_path[len(old_path):]
1289
self._limbo_files[descendant] = desc_path
1291
def _limbo_descendants(self, trans_id):
1292
"""Return the set of trans_ids whose limbo paths descend from this."""
1293
descendants = set(self._limbo_children.get(trans_id, []))
1294
for descendant in list(descendants):
1295
descendants.update(self._limbo_descendants(descendant))
488
1298
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
489
1299
"""Schedule creation of a new file.
596
1465
invalid='warning')
1468
class TreeTransform(DiskTreeTransform):
1469
"""Represent a tree transformation.
1471
This object is designed to support incremental generation of the transform,
1474
However, it gives optimum performance when parent directories are created
1475
before their contents. The transform is then able to put child files
1476
directly in their parent directory, avoiding later renames.
1478
It is easy to produce malformed transforms, but they are generally
1479
harmless. Attempting to apply a malformed transform will cause an
1480
exception to be raised before any modifications are made to the tree.
1482
Many kinds of malformed transforms can be corrected with the
1483
resolve_conflicts function. The remaining ones indicate programming error,
1484
such as trying to create a file with no path.
1486
Two sets of file creation methods are supplied. Convenience methods are:
1491
These are composed of the low-level methods:
1493
* create_file or create_directory or create_symlink
1497
Transform/Transaction ids
1498
-------------------------
1499
trans_ids are temporary ids assigned to all files involved in a transform.
1500
It's possible, even common, that not all files in the Tree have trans_ids.
1502
trans_ids are used because filenames and file_ids are not good enough
1503
identifiers; filenames change, and not all files have file_ids. File-ids
1504
are also associated with trans-ids, so that moving a file moves its
1507
trans_ids are only valid for the TreeTransform that generated them.
1511
Limbo is a temporary directory use to hold new versions of files.
1512
Files are added to limbo by create_file, create_directory, create_symlink,
1513
and their convenience variants (new_*). Files may be removed from limbo
1514
using cancel_creation. Files are renamed from limbo into their final
1515
location as part of TreeTransform.apply
1517
Limbo must be cleaned up, by either calling TreeTransform.apply or
1518
calling TreeTransform.finalize.
1520
Files are placed into limbo inside their parent directories, where
1521
possible. This reduces subsequent renames, and makes operations involving
1522
lots of files faster. This optimization is only possible if the parent
1523
directory is created *before* creating any of its children, so avoid
1524
creating children before parents, where possible.
1528
This temporary directory is used by _FileMover for storing files that are
1529
about to be deleted. In case of rollback, the files will be restored.
1530
FileMover does not delete files until it is sure that a rollback will not
1533
def __init__(self, tree, pb=None):
1534
"""Note: a tree_write lock is taken on the tree.
1536
Use TreeTransform.finalize() to release the lock (can be omitted if
1537
TreeTransform.apply() called).
1539
tree.lock_tree_write()
1541
limbodir = urlutils.local_path_from_url(
1542
tree._transport.abspath('limbo'))
1543
osutils.ensure_empty_directory_exists(
1545
errors.ExistingLimbo)
1546
deletiondir = urlutils.local_path_from_url(
1547
tree._transport.abspath('pending-deletion'))
1548
osutils.ensure_empty_directory_exists(
1550
errors.ExistingPendingDeletion)
1555
# Cache of realpath results, to speed up canonical_path
1556
self._realpaths = {}
1557
# Cache of relpath results, to speed up canonical_path
1559
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1560
tree.case_sensitive)
1561
self._deletiondir = deletiondir
1563
def canonical_path(self, path):
1564
"""Get the canonical tree-relative path"""
1565
# don't follow final symlinks
1566
abs = self._tree.abspath(path)
1567
if abs in self._relpaths:
1568
return self._relpaths[abs]
1569
dirname, basename = os.path.split(abs)
1570
if dirname not in self._realpaths:
1571
self._realpaths[dirname] = os.path.realpath(dirname)
1572
dirname = self._realpaths[dirname]
1573
abs = pathjoin(dirname, basename)
1574
if dirname in self._relpaths:
1575
relpath = pathjoin(self._relpaths[dirname], basename)
1576
relpath = relpath.rstrip('/\\')
1578
relpath = self._tree.relpath(abs)
1579
self._relpaths[abs] = relpath
1582
def tree_kind(self, trans_id):
1583
"""Determine the file kind in the working tree.
1585
:returns: The file kind or None if the file does not exist
1587
path = self._tree_id_paths.get(trans_id)
1591
return file_kind(self._tree.abspath(path))
1592
except errors.NoSuchFile:
1595
def _set_mode(self, trans_id, mode_id, typefunc):
1596
"""Set the mode of new file contents.
1597
The mode_id is the existing file to get the mode from (often the same
1598
as trans_id). The operation is only performed if there's a mode match
1599
according to typefunc.
1604
old_path = self._tree_id_paths[mode_id]
1608
mode = os.stat(self._tree.abspath(old_path)).st_mode
1609
except OSError as e:
1610
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1611
# Either old_path doesn't exist, or the parent of the
1612
# target is not a directory (but will be one eventually)
1613
# Either way, we know it doesn't exist *right now*
1614
# See also bug #248448
1619
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1621
def iter_tree_children(self, parent_id):
1622
"""Iterate through the entry's tree children, if any"""
1624
path = self._tree_id_paths[parent_id]
1628
children = os.listdir(self._tree.abspath(path))
1629
except OSError as e:
1630
if not (osutils._is_error_enotdir(e)
1631
or e.errno in (errno.ENOENT, errno.ESRCH)):
1635
for child in children:
1636
childpath = joinpath(path, child)
1637
if self._tree.is_control_filename(childpath):
1639
yield self.trans_id_tree_path(childpath)
1641
def _generate_limbo_path(self, trans_id):
1642
"""Generate a limbo path using the final path if possible.
1644
This optimizes the performance of applying the tree transform by
1645
avoiding renames. These renames can be avoided only when the parent
1646
directory is already scheduled for creation.
1648
If the final path cannot be used, falls back to using the trans_id as
1651
parent = self._new_parent.get(trans_id)
1652
# if the parent directory is already in limbo (e.g. when building a
1653
# tree), choose a limbo name inside the parent, to reduce further
1655
use_direct_path = False
1656
if self._new_contents.get(parent) == 'directory':
1657
filename = self._new_name.get(trans_id)
1658
if filename is not None:
1659
if parent not in self._limbo_children:
1660
self._limbo_children[parent] = set()
1661
self._limbo_children_names[parent] = {}
1662
use_direct_path = True
1663
# the direct path can only be used if no other file has
1664
# already taken this pathname, i.e. if the name is unused, or
1665
# if it is already associated with this trans_id.
1666
elif self._case_sensitive_target:
1667
if (self._limbo_children_names[parent].get(filename)
1668
in (trans_id, None)):
1669
use_direct_path = True
1671
for l_filename, l_trans_id in viewitems(
1672
self._limbo_children_names[parent]):
1673
if l_trans_id == trans_id:
1675
if l_filename.lower() == filename.lower():
1678
use_direct_path = True
1680
if not use_direct_path:
1681
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1683
limbo_name = pathjoin(self._limbo_files[parent], filename)
1684
self._limbo_children[parent].add(trans_id)
1685
self._limbo_children_names[parent][filename] = trans_id
1689
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1690
"""Apply all changes to the inventory and filesystem.
1692
If filesystem or inventory conflicts are present, MalformedTransform
1695
If apply succeeds, finalize is not necessary.
1697
:param no_conflicts: if True, the caller guarantees there are no
1698
conflicts, so no check is made.
1699
:param precomputed_delta: An inventory delta to use instead of
1701
:param _mover: Supply an alternate FileMover, for testing
1703
for hook in MutableTree.hooks['pre_transform']:
1704
hook(self._tree, self)
1705
if not no_conflicts:
1706
self._check_malformed()
1707
with ui.ui_factory.nested_progress_bar() as child_pb:
1708
if precomputed_delta is None:
1709
child_pb.update(gettext('Apply phase'), 0, 2)
1710
inventory_delta = self._generate_inventory_delta()
1713
inventory_delta = precomputed_delta
1716
mover = _FileMover()
1720
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1721
self._apply_removals(mover)
1722
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1723
modified_paths = self._apply_insertions(mover)
1728
mover.apply_deletions()
1729
if self.final_file_id(self.root) is None:
1730
inventory_delta = [e for e in inventory_delta if e[0] != '']
1731
self._tree.apply_inventory_delta(inventory_delta)
1732
self._apply_observed_sha1s()
1735
return _TransformResults(modified_paths, self.rename_count)
1737
def _generate_inventory_delta(self):
1738
"""Generate an inventory delta for the current transform."""
1739
inventory_delta = []
1740
new_paths = self._inventory_altered()
1741
total_entries = len(new_paths) + len(self._removed_id)
1742
with ui.ui_factory.nested_progress_bar() as child_pb:
1743
for num, trans_id in enumerate(self._removed_id):
1745
child_pb.update(gettext('removing file'), num, total_entries)
1746
if trans_id == self._new_root:
1747
file_id = self._tree.get_root_id()
1749
file_id = self.tree_file_id(trans_id)
1750
# File-id isn't really being deleted, just moved
1751
if file_id in self._r_new_id:
1753
path = self._tree_id_paths[trans_id]
1754
inventory_delta.append((path, None, file_id, None))
1755
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1758
for num, (path, trans_id) in enumerate(new_paths):
1760
child_pb.update(gettext('adding file'),
1761
num + len(self._removed_id), total_entries)
1762
file_id = new_path_file_ids[trans_id]
1766
kind = self.final_kind(trans_id)
1768
kind = self._tree.stored_kind(
1769
self._tree.id2path(file_id), file_id)
1770
parent_trans_id = self.final_parent(trans_id)
1771
parent_file_id = new_path_file_ids.get(parent_trans_id)
1772
if parent_file_id is None:
1773
parent_file_id = self.final_file_id(parent_trans_id)
1774
if trans_id in self._new_reference_revision:
1775
new_entry = inventory.TreeReference(
1777
self._new_name[trans_id],
1778
self.final_file_id(self._new_parent[trans_id]),
1779
None, self._new_reference_revision[trans_id])
1781
new_entry = inventory.make_entry(kind,
1782
self.final_name(trans_id),
1783
parent_file_id, file_id)
1785
old_path = self._tree.id2path(new_entry.file_id)
1786
except errors.NoSuchId:
1788
new_executability = self._new_executability.get(trans_id)
1789
if new_executability is not None:
1790
new_entry.executable = new_executability
1791
inventory_delta.append(
1792
(old_path, path, new_entry.file_id, new_entry))
1793
return inventory_delta
1795
def _apply_removals(self, mover):
1796
"""Perform tree operations that remove directory/inventory names.
1798
That is, delete files that are to be deleted, and put any files that
1799
need renaming into limbo. This must be done in strict child-to-parent
1802
If inventory_delta is None, no inventory delta generation is performed.
1804
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1805
with ui.ui_factory.nested_progress_bar() as child_pb:
1806
for num, (path, trans_id) in enumerate(tree_paths):
1807
# do not attempt to move root into a subdirectory of itself.
1810
child_pb.update(gettext('removing file'), num, len(tree_paths))
1811
full_path = self._tree.abspath(path)
1812
if trans_id in self._removed_contents:
1813
delete_path = os.path.join(self._deletiondir, trans_id)
1814
mover.pre_delete(full_path, delete_path)
1815
elif (trans_id in self._new_name
1816
or trans_id in self._new_parent):
1818
mover.rename(full_path, self._limbo_name(trans_id))
1819
except errors.TransformRenameFailed as e:
1820
if e.errno != errno.ENOENT:
1823
self.rename_count += 1
1825
def _apply_insertions(self, mover):
1826
"""Perform tree operations that insert directory/inventory names.
1828
That is, create any files that need to be created, and restore from
1829
limbo any files that needed renaming. This must be done in strict
1830
parent-to-child order.
1832
If inventory_delta is None, no inventory delta is calculated, and
1833
no list of modified paths is returned.
1835
new_paths = self.new_paths(filesystem_only=True)
1837
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1839
with ui.ui_factory.nested_progress_bar() as child_pb:
1840
for num, (path, trans_id) in enumerate(new_paths):
1842
child_pb.update(gettext('adding file'), num, len(new_paths))
1843
full_path = self._tree.abspath(path)
1844
if trans_id in self._needs_rename:
1846
mover.rename(self._limbo_name(trans_id), full_path)
1847
except errors.TransformRenameFailed as e:
1848
# We may be renaming a dangling inventory id
1849
if e.errno != errno.ENOENT:
1852
self.rename_count += 1
1853
# TODO: if trans_id in self._observed_sha1s, we should
1854
# re-stat the final target, since ctime will be
1855
# updated by the change.
1856
if (trans_id in self._new_contents or
1857
self.path_changed(trans_id)):
1858
if trans_id in self._new_contents:
1859
modified_paths.append(full_path)
1860
if trans_id in self._new_executability:
1861
self._set_executability(path, trans_id)
1862
if trans_id in self._observed_sha1s:
1863
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1864
st = osutils.lstat(full_path)
1865
self._observed_sha1s[trans_id] = (o_sha1, st)
1866
for path, trans_id in new_paths:
1867
# new_paths includes stuff like workingtree conflicts. Only the
1868
# stuff in new_contents actually comes from limbo.
1869
if trans_id in self._limbo_files:
1870
del self._limbo_files[trans_id]
1871
self._new_contents.clear()
1872
return modified_paths
1874
def _apply_observed_sha1s(self):
1875
"""After we have finished renaming everything, update observed sha1s
1877
This has to be done after self._tree.apply_inventory_delta, otherwise
1878
it doesn't know anything about the files we are updating. Also, we want
1879
to do this as late as possible, so that most entries end up cached.
1881
# TODO: this doesn't update the stat information for directories. So
1882
# the first 'bzr status' will still need to rewrite
1883
# .bzr/checkout/dirstate. However, we at least don't need to
1884
# re-read all of the files.
1885
# TODO: If the operation took a while, we could do a time.sleep(3) here
1886
# to allow the clock to tick over and ensure we won't have any
1887
# problems. (we could observe start time, and finish time, and if
1888
# it is less than eg 10% overhead, add a sleep call.)
1889
paths = FinalPaths(self)
1890
for trans_id, observed in viewitems(self._observed_sha1s):
1891
path = paths.get_path(trans_id)
1892
# We could get the file_id, but dirstate prefers to use the path
1893
# anyway, and it is 'cheaper' to determine.
1894
# file_id = self._new_id[trans_id]
1895
self._tree._observed_sha1(None, path, observed)
1898
class TransformPreview(DiskTreeTransform):
1899
"""A TreeTransform for generating preview trees.
1901
Unlike TreeTransform, this version works when the input tree is a
1902
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1903
unversioned files in the input tree.
1906
def __init__(self, tree, pb=None, case_sensitive=True):
1908
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1909
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1911
def canonical_path(self, path):
1914
def tree_kind(self, trans_id):
1915
path = self._tree_id_paths.get(trans_id)
1918
kind = self._tree.path_content_summary(path)[0]
1919
if kind == 'missing':
1923
def _set_mode(self, trans_id, mode_id, typefunc):
1924
"""Set the mode of new file contents.
1925
The mode_id is the existing file to get the mode from (often the same
1926
as trans_id). The operation is only performed if there's a mode match
1927
according to typefunc.
1929
# is it ok to ignore this? probably
1932
def iter_tree_children(self, parent_id):
1933
"""Iterate through the entry's tree children, if any"""
1935
path = self._tree_id_paths[parent_id]
1938
entry = next(self._tree.iter_entries_by_dir(
1939
specific_files=[path]))[1]
1940
children = getattr(entry, 'children', {})
1941
for child in children:
1942
childpath = joinpath(path, child)
1943
yield self.trans_id_tree_path(childpath)
1945
def new_orphan(self, trans_id, parent_id):
1946
raise NotImplementedError(self.new_orphan)
1949
class _PreviewTree(inventorytree.InventoryTree):
1950
"""Partial implementation of Tree to support show_diff_trees"""
1952
def __init__(self, transform):
1953
self._transform = transform
1954
self._final_paths = FinalPaths(transform)
1955
self.__by_parent = None
1956
self._parent_ids = []
1957
self._all_children_cache = {}
1958
self._path2trans_id_cache = {}
1959
self._final_name_cache = {}
1960
self._iter_changes_cache = dict((c[0], c) for c in
1961
self._transform.iter_changes())
1963
def _content_change(self, file_id):
1964
"""Return True if the content of this file changed"""
1965
changes = self._iter_changes_cache.get(file_id)
1966
# changes[2] is true if the file content changed. See
1967
# InterTree.iter_changes.
1968
return (changes is not None and changes[2])
1970
def _get_repository(self):
1971
repo = getattr(self._transform._tree, '_repository', None)
1973
repo = self._transform._tree.branch.repository
1976
def _iter_parent_trees(self):
1977
for revision_id in self.get_parent_ids():
1979
yield self.revision_tree(revision_id)
1980
except errors.NoSuchRevisionInTree:
1981
yield self._get_repository().revision_tree(revision_id)
1983
def _get_file_revision(self, path, file_id, vf, tree_revision):
1985
(file_id, t.get_file_revision(t.id2path(file_id), file_id))
1986
for t in self._iter_parent_trees()]
1987
vf.add_lines((file_id, tree_revision), parent_keys,
1988
self.get_file_lines(path, file_id))
1989
repo = self._get_repository()
1990
base_vf = repo.texts
1991
if base_vf not in vf.fallback_versionedfiles:
1992
vf.fallback_versionedfiles.append(base_vf)
1993
return tree_revision
1995
def _stat_limbo_file(self, trans_id):
1996
name = self._transform._limbo_name(trans_id)
1997
return os.lstat(name)
2000
def _by_parent(self):
2001
if self.__by_parent is None:
2002
self.__by_parent = self._transform.by_parent()
2003
return self.__by_parent
2005
def _comparison_data(self, entry, path):
2006
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2007
if kind == 'missing':
2011
file_id = self._transform.final_file_id(self._path2trans_id(path))
2012
executable = self.is_executable(path, file_id)
2013
return kind, executable, None
2015
def is_locked(self):
2018
def lock_read(self):
2019
# Perhaps in theory, this should lock the TreeTransform?
2020
return lock.LogicalLockResult(self.unlock)
2026
def root_inventory(self):
2027
"""This Tree does not use inventory as its backing data."""
2028
raise NotImplementedError(_PreviewTree.root_inventory)
2030
def get_root_id(self):
2031
return self._transform.final_file_id(self._transform.root)
2033
def all_file_ids(self):
2034
tree_ids = set(self._transform._tree.all_file_ids())
2035
tree_ids.difference_update(self._transform.tree_file_id(t)
2036
for t in self._transform._removed_id)
2037
tree_ids.update(viewvalues(self._transform._new_id))
2040
def all_versioned_paths(self):
2041
return {self.id2path(fid) for fid in self.all_file_ids()}
2043
def _has_id(self, file_id, fallback_check):
2044
if file_id in self._transform._r_new_id:
2046
elif file_id in {self._transform.tree_file_id(trans_id) for
2047
trans_id in self._transform._removed_id}:
2050
return fallback_check(file_id)
2052
def has_id(self, file_id):
2053
return self._has_id(file_id, self._transform._tree.has_id)
2055
def has_or_had_id(self, file_id):
2056
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2058
def _path2trans_id(self, path):
2059
# We must not use None here, because that is a valid value to store.
2060
trans_id = self._path2trans_id_cache.get(path, object)
2061
if trans_id is not object:
2063
segments = splitpath(path)
2064
cur_parent = self._transform.root
2065
for cur_segment in segments:
2066
for child in self._all_children(cur_parent):
2067
final_name = self._final_name_cache.get(child)
2068
if final_name is None:
2069
final_name = self._transform.final_name(child)
2070
self._final_name_cache[child] = final_name
2071
if final_name == cur_segment:
2075
self._path2trans_id_cache[path] = None
2077
self._path2trans_id_cache[path] = cur_parent
2080
def path2id(self, path):
2081
if isinstance(path, list):
2084
path = osutils.pathjoin(*path)
2085
return self._transform.final_file_id(self._path2trans_id(path))
2087
def id2path(self, file_id):
2088
trans_id = self._transform.trans_id_file_id(file_id)
2090
return self._final_paths._determine_path(trans_id)
2092
raise errors.NoSuchId(self, file_id)
2094
def _all_children(self, trans_id):
2095
children = self._all_children_cache.get(trans_id)
2096
if children is not None:
2098
children = set(self._transform.iter_tree_children(trans_id))
2099
# children in the _new_parent set are provided by _by_parent.
2100
children.difference_update(self._transform._new_parent)
2101
children.update(self._by_parent.get(trans_id, []))
2102
self._all_children_cache[trans_id] = children
2105
def _iter_children(self, file_id):
2106
trans_id = self._transform.trans_id_file_id(file_id)
2107
for child_trans_id in self._all_children(trans_id):
2108
yield self._transform.final_file_id(child_trans_id)
2111
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2112
in self._transform._tree.extras())
2113
possible_extras.update(self._transform._new_contents)
2114
possible_extras.update(self._transform._removed_id)
2115
for trans_id in possible_extras:
2116
if self._transform.final_file_id(trans_id) is None:
2117
yield self._final_paths._determine_path(trans_id)
2119
def _make_inv_entries(self, ordered_entries, specific_files=None):
2120
for trans_id, parent_file_id in ordered_entries:
2121
file_id = self._transform.final_file_id(trans_id)
2124
if (specific_files is not None and
2125
unicode(self._final_paths.get_path(trans_id)) not in specific_files):
2127
kind = self._transform.final_kind(trans_id)
2129
kind = self._transform._tree.stored_kind(
2130
self._transform._tree.id2path(file_id),
2132
new_entry = inventory.make_entry(
2134
self._transform.final_name(trans_id),
2135
parent_file_id, file_id)
2136
yield new_entry, trans_id
2138
def _list_files_by_dir(self):
2139
todo = [ROOT_PARENT]
2141
while len(todo) > 0:
2143
parent_file_id = self._transform.final_file_id(parent)
2144
children = list(self._all_children(parent))
2145
paths = dict(zip(children, self._final_paths.get_paths(children)))
2146
children.sort(key=paths.get)
2147
todo.extend(reversed(children))
2148
for trans_id in children:
2149
ordered_ids.append((trans_id, parent_file_id))
2152
def iter_child_entries(self, path, file_id=None):
2153
trans_id = self._path2trans_id(path)
2154
if trans_id is None:
2155
raise errors.NoSuchFile(path)
2156
todo = [(child_trans_id, trans_id) for child_trans_id in
2157
self._all_children(trans_id)]
2158
for entry, trans_id in self._make_inv_entries(todo):
2161
def iter_entries_by_dir(self, specific_files=None):
2162
# This may not be a maximally efficient implementation, but it is
2163
# reasonably straightforward. An implementation that grafts the
2164
# TreeTransform changes onto the tree's iter_entries_by_dir results
2165
# might be more efficient, but requires tricky inferences about stack
2167
ordered_ids = self._list_files_by_dir()
2168
for entry, trans_id in self._make_inv_entries(ordered_ids,
2170
yield unicode(self._final_paths.get_path(trans_id)), entry
2172
def _iter_entries_for_dir(self, dir_path):
2173
"""Return path, entry for items in a directory without recursing down."""
2175
dir_trans_id = self._path2trans_id(dir_path)
2176
dir_id = self._transform.final_file_id(dir_trans_id)
2177
for child_trans_id in self._all_children(dir_trans_id):
2178
ordered_ids.append((child_trans_id, dir_id))
2179
for entry, trans_id in self._make_inv_entries(ordered_ids):
2180
yield unicode(self._final_paths.get_path(trans_id)), entry
2182
def list_files(self, include_root=False, from_dir=None, recursive=True):
2183
"""See WorkingTree.list_files."""
2184
# XXX This should behave like WorkingTree.list_files, but is really
2185
# more like RevisionTree.list_files.
2189
prefix = from_dir + '/'
2190
entries = self.iter_entries_by_dir()
2191
for path, entry in entries:
2192
if entry.name == '' and not include_root:
2195
if not path.startswith(prefix):
2197
path = path[len(prefix):]
2198
yield path, 'V', entry.kind, entry.file_id, entry
2200
if from_dir is None and include_root is True:
2201
root_entry = inventory.make_entry('directory', '',
2202
ROOT_PARENT, self.get_root_id())
2203
yield '', 'V', 'directory', root_entry.file_id, root_entry
2204
entries = self._iter_entries_for_dir(from_dir or '')
2205
for path, entry in entries:
2206
yield path, 'V', entry.kind, entry.file_id, entry
2208
def kind(self, path, file_id=None):
2209
trans_id = self._path2trans_id(path)
2210
if trans_id is None:
2211
raise errors.NoSuchFile(path)
2212
return self._transform.final_kind(trans_id)
2214
def stored_kind(self, path, file_id=None):
2215
trans_id = self._path2trans_id(path)
2216
if trans_id is None:
2217
raise errors.NoSuchFile(path)
2219
return self._transform._new_contents[trans_id]
2221
return self._transform._tree.stored_kind(path, file_id)
2223
def get_file_mtime(self, path, file_id=None):
2224
"""See Tree.get_file_mtime"""
2226
file_id = self.path2id(path)
2228
raise errors.NoSuchFile(path)
2229
if not self._content_change(file_id):
2230
return self._transform._tree.get_file_mtime(
2231
self._transform._tree.id2path(file_id), file_id)
2232
trans_id = self._path2trans_id(path)
2233
return self._stat_limbo_file(trans_id).st_mtime
2235
def get_file_size(self, path, file_id=None):
2236
"""See Tree.get_file_size"""
2237
trans_id = self._path2trans_id(path)
2238
if trans_id is None:
2239
raise errors.NoSuchFile(path)
2240
kind = self._transform.final_kind(trans_id)
2243
if trans_id in self._transform._new_contents:
2244
return self._stat_limbo_file(trans_id).st_size
2245
if self.kind(path, file_id) == 'file':
2246
return self._transform._tree.get_file_size(path, file_id)
2250
def get_file_verifier(self, path, file_id=None, stat_value=None):
2251
trans_id = self._path2trans_id(path)
2252
if trans_id is None:
2253
raise errors.NoSuchFile(path)
2254
kind = self._transform._new_contents.get(trans_id)
2256
return self._transform._tree.get_file_verifier(path, file_id)
2258
with self.get_file(path, file_id) as fileobj:
2259
return ("SHA1", sha_file(fileobj))
2261
def get_file_sha1(self, path, file_id=None, stat_value=None):
2262
trans_id = self._path2trans_id(path)
2263
if trans_id is None:
2264
raise errors.NoSuchFile(path)
2265
kind = self._transform._new_contents.get(trans_id)
2267
return self._transform._tree.get_file_sha1(path, file_id)
2269
with self.get_file(path, file_id) as fileobj:
2270
return sha_file(fileobj)
2272
def is_executable(self, path, file_id=None):
2273
trans_id = self._path2trans_id(path)
2274
if trans_id is None:
2277
return self._transform._new_executability[trans_id]
2280
return self._transform._tree.is_executable(path, file_id)
2281
except OSError as e:
2282
if e.errno == errno.ENOENT:
2285
except errors.NoSuchFile:
2288
def has_filename(self, path):
2289
trans_id = self._path2trans_id(path)
2290
if trans_id in self._transform._new_contents:
2292
elif trans_id in self._transform._removed_contents:
2295
return self._transform._tree.has_filename(path)
2297
def path_content_summary(self, path):
2298
trans_id = self._path2trans_id(path)
2299
tt = self._transform
2300
tree_path = tt._tree_id_paths.get(trans_id)
2301
kind = tt._new_contents.get(trans_id)
2303
if tree_path is None or trans_id in tt._removed_contents:
2304
return 'missing', None, None, None
2305
summary = tt._tree.path_content_summary(tree_path)
2306
kind, size, executable, link_or_sha1 = summary
2309
limbo_name = tt._limbo_name(trans_id)
2310
if trans_id in tt._new_reference_revision:
2311
kind = 'tree-reference'
2313
statval = os.lstat(limbo_name)
2314
size = statval.st_size
2315
if not tt._limbo_supports_executable():
2318
executable = statval.st_mode & S_IEXEC
2322
if kind == 'symlink':
2323
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2324
executable = tt._new_executability.get(trans_id, executable)
2325
return kind, size, executable, link_or_sha1
2327
def iter_changes(self, from_tree, include_unchanged=False,
2328
specific_files=None, pb=None, extra_trees=None,
2329
require_versioned=True, want_unversioned=False):
2330
"""See InterTree.iter_changes.
2332
This has a fast path that is only used when the from_tree matches
2333
the transform tree, and no fancy options are supplied.
2335
if (from_tree is not self._transform._tree or include_unchanged or
2336
specific_files or want_unversioned):
2337
return tree.InterTree(from_tree, self).iter_changes(
2338
include_unchanged=include_unchanged,
2339
specific_files=specific_files,
2341
extra_trees=extra_trees,
2342
require_versioned=require_versioned,
2343
want_unversioned=want_unversioned)
2344
if want_unversioned:
2345
raise ValueError('want_unversioned is not supported')
2346
return self._transform.iter_changes()
2348
def get_file(self, path, file_id=None):
2349
"""See Tree.get_file"""
2351
file_id = self.path2id(path)
2352
if not self._content_change(file_id):
2353
return self._transform._tree.get_file(path, file_id)
2354
trans_id = self._path2trans_id(path)
2355
name = self._transform._limbo_name(trans_id)
2356
return open(name, 'rb')
2358
def get_file_with_stat(self, path, file_id=None):
2359
return self.get_file(path, file_id), None
2361
def annotate_iter(self, path, file_id=None,
2362
default_revision=_mod_revision.CURRENT_REVISION):
2364
file_id = self.path2id(path)
2365
changes = self._iter_changes_cache.get(file_id)
2369
changed_content, versioned, kind = (changes[2], changes[3],
2373
get_old = (kind[0] == 'file' and versioned[0])
2375
old_annotation = self._transform._tree.annotate_iter(
2376
path, file_id=file_id, default_revision=default_revision)
2380
return old_annotation
2381
if not changed_content:
2382
return old_annotation
2383
# TODO: This is doing something similar to what WT.annotate_iter is
2384
# doing, however it fails slightly because it doesn't know what
2385
# the *other* revision_id is, so it doesn't know how to give the
2386
# other as the origin for some lines, they all get
2387
# 'default_revision'
2388
# It would be nice to be able to use the new Annotator based
2389
# approach, as well.
2390
return annotate.reannotate([old_annotation],
2391
self.get_file(path, file_id).readlines(),
2394
def get_symlink_target(self, path, file_id=None):
2395
"""See Tree.get_symlink_target"""
2397
file_id = self.path2id(path)
2398
if not self._content_change(file_id):
2399
return self._transform._tree.get_symlink_target(path)
2400
trans_id = self._path2trans_id(path)
2401
name = self._transform._limbo_name(trans_id)
2402
return osutils.readlink(name)
2404
def walkdirs(self, prefix=''):
2405
pending = [self._transform.root]
2406
while len(pending) > 0:
2407
parent_id = pending.pop()
2410
prefix = prefix.rstrip('/')
2411
parent_path = self._final_paths.get_path(parent_id)
2412
parent_file_id = self._transform.final_file_id(parent_id)
2413
for child_id in self._all_children(parent_id):
2414
path_from_root = self._final_paths.get_path(child_id)
2415
basename = self._transform.final_name(child_id)
2416
file_id = self._transform.final_file_id(child_id)
2417
kind = self._transform.final_kind(child_id)
2418
if kind is not None:
2419
versioned_kind = kind
2422
versioned_kind = self._transform._tree.stored_kind(
2423
self._transform._tree.id2path(file_id),
2425
if versioned_kind == 'directory':
2426
subdirs.append(child_id)
2427
children.append((path_from_root, basename, kind, None,
2428
file_id, versioned_kind))
2430
if parent_path.startswith(prefix):
2431
yield (parent_path, parent_file_id), children
2432
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2435
def get_parent_ids(self):
2436
return self._parent_ids
2438
def set_parent_ids(self, parent_ids):
2439
self._parent_ids = parent_ids
2441
def get_revision_tree(self, revision_id):
2442
return self._transform._tree.get_revision_tree(revision_id)
599
2445
def joinpath(parent, child):
600
2446
"""Join tree-relative paths, handling the tree root specially"""
601
2447
if parent is None or parent == "":