395
534
def find_conflicts(self):
396
535
"""Find any violations of inventory or filesystem invariants"""
397
raise NotImplementedError(self.find_conflicts)
536
if self._done is True:
537
raise ReusingTransform()
539
# ensure all children of all existent parents are known
540
# all children of non-existent parents are known, by definition.
541
self._add_tree_children()
542
by_parent = self.by_parent()
543
conflicts.extend(self._unversioned_parents(by_parent))
544
conflicts.extend(self._parent_loops())
545
conflicts.extend(self._duplicate_entries(by_parent))
546
conflicts.extend(self._duplicate_ids())
547
conflicts.extend(self._parent_type_conflicts(by_parent))
548
conflicts.extend(self._improper_versioning())
549
conflicts.extend(self._executability_conflicts())
550
conflicts.extend(self._overwrite_conflicts())
553
def _check_malformed(self):
554
conflicts = self.find_conflicts()
555
if len(conflicts) != 0:
556
raise MalformedTransform(conflicts=conflicts)
558
def _add_tree_children(self):
559
"""Add all the children of all active parents to the known paths.
561
Active parents are those which gain children, and those which are
562
removed. This is a necessary first step in detecting conflicts.
564
parents = list(self.by_parent())
565
parents.extend([t for t in self._removed_contents if
566
self.tree_kind(t) == 'directory'])
567
for trans_id in self._removed_id:
568
file_id = self.tree_file_id(trans_id)
569
if file_id is not None:
570
if self._tree.stored_kind(file_id) == 'directory':
571
parents.append(trans_id)
572
elif self.tree_kind(trans_id) == 'directory':
573
parents.append(trans_id)
575
for parent_id in parents:
576
# ensure that all children are registered with the transaction
577
list(self.iter_tree_children(parent_id))
579
def _has_named_child(self, name, parent_id, known_children):
580
"""Does a parent already have a name child.
582
:param name: The searched for name.
584
:param parent_id: The parent for which the check is made.
586
:param known_children: The already known children. This should have
587
been recently obtained from `self.by_parent.get(parent_id)`
588
(or will be if None is passed).
590
if known_children is None:
591
known_children = self.by_parent().get(parent_id, [])
592
for child in known_children:
593
if self.final_name(child) == name:
595
parent_path = self._tree_id_paths.get(parent_id, None)
596
if parent_path is None:
597
# No parent... no children
599
child_path = joinpath(parent_path, name)
600
child_id = self._tree_path_ids.get(child_path, None)
602
# Not known by the tree transform yet, check the filesystem
603
return osutils.lexists(self._tree.abspath(child_path))
605
raise AssertionError('child_id is missing: %s, %s, %s'
606
% (name, parent_id, child_id))
608
def _available_backup_name(self, name, target_id):
609
"""Find an available backup name.
611
:param name: The basename of the file.
613
:param target_id: The directory trans_id where the backup should
616
known_children = self.by_parent().get(target_id, [])
617
return osutils.available_backup_name(
619
lambda base: self._has_named_child(
620
base, target_id, known_children))
622
def _parent_loops(self):
623
"""No entry should be its own ancestor"""
625
for trans_id in self._new_parent:
628
while parent_id != ROOT_PARENT:
631
parent_id = self.final_parent(parent_id)
634
if parent_id == trans_id:
635
conflicts.append(('parent loop', trans_id))
636
if parent_id in seen:
640
def _unversioned_parents(self, by_parent):
641
"""If parent directories are versioned, children must be versioned."""
643
for parent_id, children in viewitems(by_parent):
644
if parent_id == ROOT_PARENT:
646
if self.final_file_id(parent_id) is not None:
648
for child_id in children:
649
if self.final_file_id(child_id) is not None:
650
conflicts.append(('unversioned parent', parent_id))
654
def _improper_versioning(self):
655
"""Cannot version a file with no contents, or a bad type.
657
However, existing entries with no contents are okay.
660
for trans_id in self._new_id:
661
kind = self.final_kind(trans_id)
663
conflicts.append(('versioning no contents', trans_id))
665
if not inventory.InventoryEntry.versionable_kind(kind):
666
conflicts.append(('versioning bad kind', trans_id, kind))
669
def _executability_conflicts(self):
670
"""Check for bad executability changes.
672
Only versioned files may have their executability set, because
673
1. only versioned entries can have executability under windows
674
2. only files can be executable. (The execute bit on a directory
675
does not indicate searchability)
678
for trans_id in self._new_executability:
679
if self.final_file_id(trans_id) is None:
680
conflicts.append(('unversioned executability', trans_id))
682
if self.final_kind(trans_id) != "file":
683
conflicts.append(('non-file executability', trans_id))
686
def _overwrite_conflicts(self):
687
"""Check for overwrites (not permitted on Win32)"""
689
for trans_id in self._new_contents:
690
if self.tree_kind(trans_id) is None:
692
if trans_id not in self._removed_contents:
693
conflicts.append(('overwrite', trans_id,
694
self.final_name(trans_id)))
697
def _duplicate_entries(self, by_parent):
698
"""No directory may have two entries with the same name."""
700
if (self._new_name, self._new_parent) == ({}, {}):
702
for children in viewvalues(by_parent):
704
for child_tid in children:
705
name = self.final_name(child_tid)
707
# Keep children only if they still exist in the end
708
if not self._case_sensitive_target:
710
name_ids.append((name, child_tid))
714
for name, trans_id in name_ids:
715
kind = self.final_kind(trans_id)
716
file_id = self.final_file_id(trans_id)
717
if kind is None and file_id is None:
719
if name == last_name:
720
conflicts.append(('duplicate', last_trans_id, trans_id,
723
last_trans_id = trans_id
726
def _duplicate_ids(self):
727
"""Each inventory id may only be used once"""
729
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
731
all_ids = self._tree.all_file_ids()
732
active_tree_ids = all_ids.difference(removed_tree_ids)
733
for trans_id, file_id in viewitems(self._new_id):
734
if file_id in active_tree_ids:
735
old_trans_id = self.trans_id_tree_file_id(file_id)
736
conflicts.append(('duplicate id', old_trans_id, trans_id))
739
def _parent_type_conflicts(self, by_parent):
740
"""Children must have a directory parent"""
742
for parent_id, children in viewitems(by_parent):
743
if parent_id == ROOT_PARENT:
746
for child_id in children:
747
if self.final_kind(child_id) is not None:
752
# There is at least a child, so we need an existing directory to
754
kind = self.final_kind(parent_id)
756
# The directory will be deleted
757
conflicts.append(('missing parent', parent_id))
758
elif kind != "directory":
759
# Meh, we need a *directory* to put something in it
760
conflicts.append(('non-directory parent', parent_id))
763
def _set_executability(self, path, trans_id):
764
"""Set the executability of versioned files """
765
if self._tree._supports_executable():
766
new_executability = self._new_executability[trans_id]
767
abspath = self._tree.abspath(path)
768
current_mode = os.stat(abspath).st_mode
769
if new_executability:
772
to_mode = current_mode | (0o100 & ~umask)
773
# Enable x-bit for others only if they can read it.
774
if current_mode & 0o004:
775
to_mode |= 0o001 & ~umask
776
if current_mode & 0o040:
777
to_mode |= 0o010 & ~umask
779
to_mode = current_mode & ~0o111
780
osutils.chmod_if_possible(abspath, to_mode)
782
def _new_entry(self, name, parent_id, file_id):
783
"""Helper function to create a new filesystem entry."""
784
trans_id = self.create_path(name, parent_id)
785
if file_id is not None:
786
self.version_file(file_id, trans_id)
399
789
def new_file(self, name, parent_id, contents, file_id=None,
400
790
executable=None, sha1=None):
483
1038
may reduce performance for some non-native formats.)
484
1039
:return: The revision_id of the revision committed.
486
raise NotImplementedError(self.commit)
1041
self._check_malformed()
1043
unversioned = set(self._new_contents).difference(set(self._new_id))
1044
for trans_id in unversioned:
1045
if self.final_file_id(trans_id) is None:
1046
raise errors.StrictCommitFailed()
1048
revno, last_rev_id = branch.last_revision_info()
1049
if last_rev_id == _mod_revision.NULL_REVISION:
1050
if merge_parents is not None:
1051
raise ValueError('Cannot supply merge parents for first'
1055
parent_ids = [last_rev_id]
1056
if merge_parents is not None:
1057
parent_ids.extend(merge_parents)
1058
if self._tree.get_revision_id() != last_rev_id:
1059
raise ValueError('TreeTransform not based on branch basis: %s' %
1060
self._tree.get_revision_id())
1061
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1062
builder = branch.get_commit_builder(parent_ids,
1063
timestamp=timestamp,
1065
committer=committer,
1067
revision_id=revision_id)
1068
preview = self.get_preview_tree()
1069
list(builder.record_iter_changes(preview, last_rev_id,
1070
self.iter_changes()))
1071
builder.finish_inventory()
1072
revision_id = builder.commit(message)
1073
branch.set_last_revision_info(revno + 1, revision_id)
1076
def _text_parent(self, trans_id):
1077
file_id = self.tree_file_id(trans_id)
1079
if file_id is None or self._tree.kind(file_id) != 'file':
1081
except errors.NoSuchFile:
1085
def _get_parents_texts(self, trans_id):
1086
"""Get texts for compression parents of this file."""
1087
file_id = self._text_parent(trans_id)
1090
return (self._tree.get_file_text(file_id),)
1092
def _get_parents_lines(self, trans_id):
1093
"""Get lines for compression parents of this file."""
1094
file_id = self._text_parent(trans_id)
1097
return (self._tree.get_file_lines(file_id),)
1099
def serialize(self, serializer):
1100
"""Serialize this TreeTransform.
1102
:param serializer: A Serialiser like pack.ContainerSerializer.
1104
new_name = dict((k, v.encode('utf-8')) for k, v in
1105
viewitems(self._new_name))
1106
new_executability = dict((k, int(v)) for k, v in
1107
viewitems(self._new_executability))
1108
tree_path_ids = dict((k.encode('utf-8'), v)
1109
for k, v in viewitems(self._tree_path_ids))
1111
'_id_number': self._id_number,
1112
'_new_name': new_name,
1113
'_new_parent': self._new_parent,
1114
'_new_executability': new_executability,
1115
'_new_id': self._new_id,
1116
'_tree_path_ids': tree_path_ids,
1117
'_removed_id': list(self._removed_id),
1118
'_removed_contents': list(self._removed_contents),
1119
'_non_present_ids': self._non_present_ids,
1121
yield serializer.bytes_record(bencode.bencode(attribs),
1123
for trans_id, kind in viewitems(self._new_contents):
1125
lines = osutils.chunks_to_lines(
1126
self._read_file_chunks(trans_id))
1127
parents = self._get_parents_lines(trans_id)
1128
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1129
content = ''.join(mpdiff.to_patch())
1130
if kind == 'directory':
1132
if kind == 'symlink':
1133
content = self._read_symlink_target(trans_id)
1134
yield serializer.bytes_record(content, ((trans_id, kind),))
1136
def deserialize(self, records):
1137
"""Deserialize a stored TreeTransform.
1139
:param records: An iterable of (names, content) tuples, as per
1140
pack.ContainerPushParser.
1142
names, content = next(records)
1143
attribs = bencode.bdecode(content)
1144
self._id_number = attribs['_id_number']
1145
self._new_name = dict((k, v.decode('utf-8'))
1146
for k, v in viewitems(attribs['_new_name']))
1147
self._new_parent = attribs['_new_parent']
1148
self._new_executability = dict((k, bool(v))
1149
for k, v in viewitems(attribs['_new_executability']))
1150
self._new_id = attribs['_new_id']
1151
self._r_new_id = dict((v, k) for k, v in viewitems(self._new_id))
1152
self._tree_path_ids = {}
1153
self._tree_id_paths = {}
1154
for bytepath, trans_id in viewitems(attribs['_tree_path_ids']):
1155
path = bytepath.decode('utf-8')
1156
self._tree_path_ids[path] = trans_id
1157
self._tree_id_paths[trans_id] = path
1158
self._removed_id = set(attribs['_removed_id'])
1159
self._removed_contents = set(attribs['_removed_contents'])
1160
self._non_present_ids = attribs['_non_present_ids']
1161
for ((trans_id, kind),), content in records:
1163
mpdiff = multiparent.MultiParent.from_patch(content)
1164
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1165
self.create_file(lines, trans_id)
1166
if kind == 'directory':
1167
self.create_directory(trans_id)
1168
if kind == 'symlink':
1169
self.create_symlink(content.decode('utf-8'), trans_id)
1172
class DiskTreeTransform(TreeTransformBase):
1173
"""Tree transform storing its contents on disk."""
1175
def __init__(self, tree, limbodir, pb=None,
1176
case_sensitive=True):
1178
:param tree: The tree that will be transformed, but not necessarily
1180
:param limbodir: A directory where new files can be stored until
1181
they are installed in their proper places
1183
:param case_sensitive: If True, the target of the transform is
1184
case sensitive, not just case preserving.
1186
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1187
self._limbodir = limbodir
1188
self._deletiondir = None
1189
# A mapping of transform ids to their limbo filename
1190
self._limbo_files = {}
1191
self._possibly_stale_limbo_files = set()
1192
# A mapping of transform ids to a set of the transform ids of children
1193
# that their limbo directory has
1194
self._limbo_children = {}
1195
# Map transform ids to maps of child filename to child transform id
1196
self._limbo_children_names = {}
1197
# List of transform ids that need to be renamed from limbo into place
1198
self._needs_rename = set()
1199
self._creation_mtime = None
1202
"""Release the working tree lock, if held, clean up limbo dir.
1204
This is required if apply has not been invoked, but can be invoked
1207
if self._tree is None:
1210
limbo_paths = list(viewvalues(self._limbo_files))
1211
limbo_paths.extend(self._possibly_stale_limbo_files)
1212
limbo_paths.sort(reverse=True)
1213
for path in limbo_paths:
1216
except OSError as e:
1217
if e.errno != errno.ENOENT:
1219
# XXX: warn? perhaps we just got interrupted at an
1220
# inconvenient moment, but perhaps files are disappearing
1223
delete_any(self._limbodir)
1225
# We don't especially care *why* the dir is immortal.
1226
raise ImmortalLimbo(self._limbodir)
1228
if self._deletiondir is not None:
1229
delete_any(self._deletiondir)
1231
raise errors.ImmortalPendingDeletion(self._deletiondir)
1233
TreeTransformBase.finalize(self)
1235
def _limbo_supports_executable(self):
1236
"""Check if the limbo path supports the executable bit."""
1237
# FIXME: Check actual file system capabilities of limbodir
1238
return osutils.supports_executable()
1240
def _limbo_name(self, trans_id):
1241
"""Generate the limbo name of a file"""
1242
limbo_name = self._limbo_files.get(trans_id)
1243
if limbo_name is None:
1244
limbo_name = self._generate_limbo_path(trans_id)
1245
self._limbo_files[trans_id] = limbo_name
1248
def _generate_limbo_path(self, trans_id):
1249
"""Generate a limbo path using the trans_id as the relative path.
1251
This is suitable as a fallback, and when the transform should not be
1252
sensitive to the path encoding of the limbo directory.
1254
self._needs_rename.add(trans_id)
1255
return pathjoin(self._limbodir, trans_id)
1257
def adjust_path(self, name, parent, trans_id):
1258
previous_parent = self._new_parent.get(trans_id)
1259
previous_name = self._new_name.get(trans_id)
1260
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1261
if (trans_id in self._limbo_files and
1262
trans_id not in self._needs_rename):
1263
self._rename_in_limbo([trans_id])
1264
if previous_parent != parent:
1265
self._limbo_children[previous_parent].remove(trans_id)
1266
if previous_parent != parent or previous_name != name:
1267
del self._limbo_children_names[previous_parent][previous_name]
1269
def _rename_in_limbo(self, trans_ids):
1270
"""Fix limbo names so that the right final path is produced.
1272
This means we outsmarted ourselves-- we tried to avoid renaming
1273
these files later by creating them with their final names in their
1274
final parents. But now the previous name or parent is no longer
1275
suitable, so we have to rename them.
1277
Even for trans_ids that have no new contents, we must remove their
1278
entries from _limbo_files, because they are now stale.
1280
for trans_id in trans_ids:
1281
old_path = self._limbo_files[trans_id]
1282
self._possibly_stale_limbo_files.add(old_path)
1283
del self._limbo_files[trans_id]
1284
if trans_id not in self._new_contents:
1286
new_path = self._limbo_name(trans_id)
1287
os.rename(old_path, new_path)
1288
self._possibly_stale_limbo_files.remove(old_path)
1289
for descendant in self._limbo_descendants(trans_id):
1290
desc_path = self._limbo_files[descendant]
1291
desc_path = new_path + desc_path[len(old_path):]
1292
self._limbo_files[descendant] = desc_path
1294
def _limbo_descendants(self, trans_id):
1295
"""Return the set of trans_ids whose limbo paths descend from this."""
1296
descendants = set(self._limbo_children.get(trans_id, []))
1297
for descendant in list(descendants):
1298
descendants.update(self._limbo_descendants(descendant))
488
1301
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
489
1302
"""Schedule creation of a new file.
583
1465
orphaning_registry = registry.Registry()
584
1466
orphaning_registry.register(
585
u'conflict', refuse_orphan,
1467
'conflict', refuse_orphan,
586
1468
'Leave orphans in place and create a conflict on the directory.')
587
1469
orphaning_registry.register(
588
u'move', move_orphan,
589
'Move orphans into the brz-orphans directory.')
590
orphaning_registry._set_default_key(u'conflict')
1470
'move', move_orphan,
1471
'Move orphans into the bzr-orphans directory.')
1472
orphaning_registry._set_default_key('conflict')
593
1475
opt_transform_orphan = _mod_config.RegistryOption(
594
'transform.orphan_policy', orphaning_registry,
1476
'bzr.transform.orphan_policy', orphaning_registry,
595
1477
help='Policy for orphaned files during transform operations.',
596
1478
invalid='warning')
1481
class TreeTransform(DiskTreeTransform):
1482
"""Represent a tree transformation.
1484
This object is designed to support incremental generation of the transform,
1487
However, it gives optimum performance when parent directories are created
1488
before their contents. The transform is then able to put child files
1489
directly in their parent directory, avoiding later renames.
1491
It is easy to produce malformed transforms, but they are generally
1492
harmless. Attempting to apply a malformed transform will cause an
1493
exception to be raised before any modifications are made to the tree.
1495
Many kinds of malformed transforms can be corrected with the
1496
resolve_conflicts function. The remaining ones indicate programming error,
1497
such as trying to create a file with no path.
1499
Two sets of file creation methods are supplied. Convenience methods are:
1504
These are composed of the low-level methods:
1506
* create_file or create_directory or create_symlink
1510
Transform/Transaction ids
1511
-------------------------
1512
trans_ids are temporary ids assigned to all files involved in a transform.
1513
It's possible, even common, that not all files in the Tree have trans_ids.
1515
trans_ids are used because filenames and file_ids are not good enough
1516
identifiers; filenames change, and not all files have file_ids. File-ids
1517
are also associated with trans-ids, so that moving a file moves its
1520
trans_ids are only valid for the TreeTransform that generated them.
1524
Limbo is a temporary directory use to hold new versions of files.
1525
Files are added to limbo by create_file, create_directory, create_symlink,
1526
and their convenience variants (new_*). Files may be removed from limbo
1527
using cancel_creation. Files are renamed from limbo into their final
1528
location as part of TreeTransform.apply
1530
Limbo must be cleaned up, by either calling TreeTransform.apply or
1531
calling TreeTransform.finalize.
1533
Files are placed into limbo inside their parent directories, where
1534
possible. This reduces subsequent renames, and makes operations involving
1535
lots of files faster. This optimization is only possible if the parent
1536
directory is created *before* creating any of its children, so avoid
1537
creating children before parents, where possible.
1541
This temporary directory is used by _FileMover for storing files that are
1542
about to be deleted. In case of rollback, the files will be restored.
1543
FileMover does not delete files until it is sure that a rollback will not
1546
def __init__(self, tree, pb=None):
1547
"""Note: a tree_write lock is taken on the tree.
1549
Use TreeTransform.finalize() to release the lock (can be omitted if
1550
TreeTransform.apply() called).
1552
tree.lock_tree_write()
1555
limbodir = urlutils.local_path_from_url(
1556
tree._transport.abspath('limbo'))
1557
osutils.ensure_empty_directory_exists(
1559
errors.ExistingLimbo)
1560
deletiondir = urlutils.local_path_from_url(
1561
tree._transport.abspath('pending-deletion'))
1562
osutils.ensure_empty_directory_exists(
1564
errors.ExistingPendingDeletion)
1569
# Cache of realpath results, to speed up canonical_path
1570
self._realpaths = {}
1571
# Cache of relpath results, to speed up canonical_path
1573
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1574
tree.case_sensitive)
1575
self._deletiondir = deletiondir
1577
def canonical_path(self, path):
1578
"""Get the canonical tree-relative path"""
1579
# don't follow final symlinks
1580
abs = self._tree.abspath(path)
1581
if abs in self._relpaths:
1582
return self._relpaths[abs]
1583
dirname, basename = os.path.split(abs)
1584
if dirname not in self._realpaths:
1585
self._realpaths[dirname] = os.path.realpath(dirname)
1586
dirname = self._realpaths[dirname]
1587
abs = pathjoin(dirname, basename)
1588
if dirname in self._relpaths:
1589
relpath = pathjoin(self._relpaths[dirname], basename)
1590
relpath = relpath.rstrip('/\\')
1592
relpath = self._tree.relpath(abs)
1593
self._relpaths[abs] = relpath
1596
def tree_kind(self, trans_id):
1597
"""Determine the file kind in the working tree.
1599
:returns: The file kind or None if the file does not exist
1601
path = self._tree_id_paths.get(trans_id)
1605
return file_kind(self._tree.abspath(path))
1606
except errors.NoSuchFile:
1609
def _set_mode(self, trans_id, mode_id, typefunc):
1610
"""Set the mode of new file contents.
1611
The mode_id is the existing file to get the mode from (often the same
1612
as trans_id). The operation is only performed if there's a mode match
1613
according to typefunc.
1618
old_path = self._tree_id_paths[mode_id]
1622
mode = os.stat(self._tree.abspath(old_path)).st_mode
1623
except OSError as e:
1624
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1625
# Either old_path doesn't exist, or the parent of the
1626
# target is not a directory (but will be one eventually)
1627
# Either way, we know it doesn't exist *right now*
1628
# See also bug #248448
1633
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1635
def iter_tree_children(self, parent_id):
1636
"""Iterate through the entry's tree children, if any"""
1638
path = self._tree_id_paths[parent_id]
1642
children = os.listdir(self._tree.abspath(path))
1643
except OSError as e:
1644
if not (osutils._is_error_enotdir(e)
1645
or e.errno in (errno.ENOENT, errno.ESRCH)):
1649
for child in children:
1650
childpath = joinpath(path, child)
1651
if self._tree.is_control_filename(childpath):
1653
yield self.trans_id_tree_path(childpath)
1655
def _generate_limbo_path(self, trans_id):
1656
"""Generate a limbo path using the final path if possible.
1658
This optimizes the performance of applying the tree transform by
1659
avoiding renames. These renames can be avoided only when the parent
1660
directory is already scheduled for creation.
1662
If the final path cannot be used, falls back to using the trans_id as
1665
parent = self._new_parent.get(trans_id)
1666
# if the parent directory is already in limbo (e.g. when building a
1667
# tree), choose a limbo name inside the parent, to reduce further
1669
use_direct_path = False
1670
if self._new_contents.get(parent) == 'directory':
1671
filename = self._new_name.get(trans_id)
1672
if filename is not None:
1673
if parent not in self._limbo_children:
1674
self._limbo_children[parent] = set()
1675
self._limbo_children_names[parent] = {}
1676
use_direct_path = True
1677
# the direct path can only be used if no other file has
1678
# already taken this pathname, i.e. if the name is unused, or
1679
# if it is already associated with this trans_id.
1680
elif self._case_sensitive_target:
1681
if (self._limbo_children_names[parent].get(filename)
1682
in (trans_id, None)):
1683
use_direct_path = True
1685
for l_filename, l_trans_id in viewitems(
1686
self._limbo_children_names[parent]):
1687
if l_trans_id == trans_id:
1689
if l_filename.lower() == filename.lower():
1692
use_direct_path = True
1694
if not use_direct_path:
1695
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1697
limbo_name = pathjoin(self._limbo_files[parent], filename)
1698
self._limbo_children[parent].add(trans_id)
1699
self._limbo_children_names[parent][filename] = trans_id
1703
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1704
"""Apply all changes to the inventory and filesystem.
1706
If filesystem or inventory conflicts are present, MalformedTransform
1709
If apply succeeds, finalize is not necessary.
1711
:param no_conflicts: if True, the caller guarantees there are no
1712
conflicts, so no check is made.
1713
:param precomputed_delta: An inventory delta to use instead of
1715
:param _mover: Supply an alternate FileMover, for testing
1717
for hook in MutableTree.hooks['pre_transform']:
1718
hook(self._tree, self)
1719
if not no_conflicts:
1720
self._check_malformed()
1721
child_pb = ui.ui_factory.nested_progress_bar()
1723
if precomputed_delta is None:
1724
child_pb.update(gettext('Apply phase'), 0, 2)
1725
inventory_delta = self._generate_inventory_delta()
1728
inventory_delta = precomputed_delta
1731
mover = _FileMover()
1735
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1736
self._apply_removals(mover)
1737
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1738
modified_paths = self._apply_insertions(mover)
1743
mover.apply_deletions()
1746
if self.final_file_id(self.root) is None:
1747
inventory_delta = [e for e in inventory_delta if e[0] != '']
1748
self._tree.apply_inventory_delta(inventory_delta)
1749
self._apply_observed_sha1s()
1752
return _TransformResults(modified_paths, self.rename_count)
1754
def _generate_inventory_delta(self):
1755
"""Generate an inventory delta for the current transform."""
1756
inventory_delta = []
1757
child_pb = ui.ui_factory.nested_progress_bar()
1758
new_paths = self._inventory_altered()
1759
total_entries = len(new_paths) + len(self._removed_id)
1761
for num, trans_id in enumerate(self._removed_id):
1763
child_pb.update(gettext('removing file'), num, total_entries)
1764
if trans_id == self._new_root:
1765
file_id = self._tree.get_root_id()
1767
file_id = self.tree_file_id(trans_id)
1768
# File-id isn't really being deleted, just moved
1769
if file_id in self._r_new_id:
1771
path = self._tree_id_paths[trans_id]
1772
inventory_delta.append((path, None, file_id, None))
1773
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1775
entries = self._tree.iter_entries_by_dir(
1776
viewvalues(new_path_file_ids))
1777
old_paths = dict((e.file_id, p) for p, e in entries)
1779
for num, (path, trans_id) in enumerate(new_paths):
1781
child_pb.update(gettext('adding file'),
1782
num + len(self._removed_id), total_entries)
1783
file_id = new_path_file_ids[trans_id]
1787
kind = self.final_kind(trans_id)
1789
kind = self._tree.stored_kind(file_id)
1790
parent_trans_id = self.final_parent(trans_id)
1791
parent_file_id = new_path_file_ids.get(parent_trans_id)
1792
if parent_file_id is None:
1793
parent_file_id = self.final_file_id(parent_trans_id)
1794
if trans_id in self._new_reference_revision:
1795
new_entry = inventory.TreeReference(
1797
self._new_name[trans_id],
1798
self.final_file_id(self._new_parent[trans_id]),
1799
None, self._new_reference_revision[trans_id])
1801
new_entry = inventory.make_entry(kind,
1802
self.final_name(trans_id),
1803
parent_file_id, file_id)
1804
old_path = old_paths.get(new_entry.file_id)
1805
new_executability = self._new_executability.get(trans_id)
1806
if new_executability is not None:
1807
new_entry.executable = new_executability
1808
inventory_delta.append(
1809
(old_path, path, new_entry.file_id, new_entry))
1812
return inventory_delta
1814
def _apply_removals(self, mover):
1815
"""Perform tree operations that remove directory/inventory names.
1817
That is, delete files that are to be deleted, and put any files that
1818
need renaming into limbo. This must be done in strict child-to-parent
1821
If inventory_delta is None, no inventory delta generation is performed.
1823
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1824
child_pb = ui.ui_factory.nested_progress_bar()
1826
for num, (path, trans_id) in enumerate(tree_paths):
1827
# do not attempt to move root into a subdirectory of itself.
1830
child_pb.update(gettext('removing file'), num, len(tree_paths))
1831
full_path = self._tree.abspath(path)
1832
if trans_id in self._removed_contents:
1833
delete_path = os.path.join(self._deletiondir, trans_id)
1834
mover.pre_delete(full_path, delete_path)
1835
elif (trans_id in self._new_name
1836
or trans_id in self._new_parent):
1838
mover.rename(full_path, self._limbo_name(trans_id))
1839
except errors.TransformRenameFailed as e:
1840
if e.errno != errno.ENOENT:
1843
self.rename_count += 1
1847
def _apply_insertions(self, mover):
1848
"""Perform tree operations that insert directory/inventory names.
1850
That is, create any files that need to be created, and restore from
1851
limbo any files that needed renaming. This must be done in strict
1852
parent-to-child order.
1854
If inventory_delta is None, no inventory delta is calculated, and
1855
no list of modified paths is returned.
1857
new_paths = self.new_paths(filesystem_only=True)
1859
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1861
child_pb = ui.ui_factory.nested_progress_bar()
1863
for num, (path, trans_id) in enumerate(new_paths):
1865
child_pb.update(gettext('adding file'), num, len(new_paths))
1866
full_path = self._tree.abspath(path)
1867
if trans_id in self._needs_rename:
1869
mover.rename(self._limbo_name(trans_id), full_path)
1870
except errors.TransformRenameFailed as e:
1871
# We may be renaming a dangling inventory id
1872
if e.errno != errno.ENOENT:
1875
self.rename_count += 1
1876
# TODO: if trans_id in self._observed_sha1s, we should
1877
# re-stat the final target, since ctime will be
1878
# updated by the change.
1879
if (trans_id in self._new_contents or
1880
self.path_changed(trans_id)):
1881
if trans_id in self._new_contents:
1882
modified_paths.append(full_path)
1883
if trans_id in self._new_executability:
1884
self._set_executability(path, trans_id)
1885
if trans_id in self._observed_sha1s:
1886
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1887
st = osutils.lstat(full_path)
1888
self._observed_sha1s[trans_id] = (o_sha1, st)
1891
for path, trans_id in new_paths:
1892
# new_paths includes stuff like workingtree conflicts. Only the
1893
# stuff in new_contents actually comes from limbo.
1894
if trans_id in self._limbo_files:
1895
del self._limbo_files[trans_id]
1896
self._new_contents.clear()
1897
return modified_paths
1899
def _apply_observed_sha1s(self):
1900
"""After we have finished renaming everything, update observed sha1s
1902
This has to be done after self._tree.apply_inventory_delta, otherwise
1903
it doesn't know anything about the files we are updating. Also, we want
1904
to do this as late as possible, so that most entries end up cached.
1906
# TODO: this doesn't update the stat information for directories. So
1907
# the first 'bzr status' will still need to rewrite
1908
# .bzr/checkout/dirstate. However, we at least don't need to
1909
# re-read all of the files.
1910
# TODO: If the operation took a while, we could do a time.sleep(3) here
1911
# to allow the clock to tick over and ensure we won't have any
1912
# problems. (we could observe start time, and finish time, and if
1913
# it is less than eg 10% overhead, add a sleep call.)
1914
paths = FinalPaths(self)
1915
for trans_id, observed in viewitems(self._observed_sha1s):
1916
path = paths.get_path(trans_id)
1917
# We could get the file_id, but dirstate prefers to use the path
1918
# anyway, and it is 'cheaper' to determine.
1919
# file_id = self._new_id[trans_id]
1920
self._tree._observed_sha1(None, path, observed)
1923
class TransformPreview(DiskTreeTransform):
1924
"""A TreeTransform for generating preview trees.
1926
Unlike TreeTransform, this version works when the input tree is a
1927
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1928
unversioned files in the input tree.
1931
def __init__(self, tree, pb=None, case_sensitive=True):
1933
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1934
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1936
def canonical_path(self, path):
1939
def tree_kind(self, trans_id):
1940
path = self._tree_id_paths.get(trans_id)
1943
kind = self._tree.path_content_summary(path)[0]
1944
if kind == 'missing':
1948
def _set_mode(self, trans_id, mode_id, typefunc):
1949
"""Set the mode of new file contents.
1950
The mode_id is the existing file to get the mode from (often the same
1951
as trans_id). The operation is only performed if there's a mode match
1952
according to typefunc.
1954
# is it ok to ignore this? probably
1957
def iter_tree_children(self, parent_id):
1958
"""Iterate through the entry's tree children, if any"""
1960
path = self._tree_id_paths[parent_id]
1963
file_id = self.tree_file_id(parent_id)
1966
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1967
children = getattr(entry, 'children', {})
1968
for child in children:
1969
childpath = joinpath(path, child)
1970
yield self.trans_id_tree_path(childpath)
1972
def new_orphan(self, trans_id, parent_id):
1973
raise NotImplementedError(self.new_orphan)
1976
class _PreviewTree(tree.InventoryTree):
1977
"""Partial implementation of Tree to support show_diff_trees"""
1979
def __init__(self, transform):
1980
self._transform = transform
1981
self._final_paths = FinalPaths(transform)
1982
self.__by_parent = None
1983
self._parent_ids = []
1984
self._all_children_cache = {}
1985
self._path2trans_id_cache = {}
1986
self._final_name_cache = {}
1987
self._iter_changes_cache = dict((c[0], c) for c in
1988
self._transform.iter_changes())
1990
def _content_change(self, file_id):
1991
"""Return True if the content of this file changed"""
1992
changes = self._iter_changes_cache.get(file_id)
1993
# changes[2] is true if the file content changed. See
1994
# InterTree.iter_changes.
1995
return (changes is not None and changes[2])
1997
def _get_repository(self):
1998
repo = getattr(self._transform._tree, '_repository', None)
2000
repo = self._transform._tree.branch.repository
2003
def _iter_parent_trees(self):
2004
for revision_id in self.get_parent_ids():
2006
yield self.revision_tree(revision_id)
2007
except errors.NoSuchRevisionInTree:
2008
yield self._get_repository().revision_tree(revision_id)
2010
def _get_file_revision(self, file_id, vf, tree_revision):
2011
parent_keys = [(file_id, t.get_file_revision(file_id)) for t in
2012
self._iter_parent_trees()]
2013
vf.add_lines((file_id, tree_revision), parent_keys,
2014
self.get_file_lines(file_id))
2015
repo = self._get_repository()
2016
base_vf = repo.texts
2017
if base_vf not in vf.fallback_versionedfiles:
2018
vf.fallback_versionedfiles.append(base_vf)
2019
return tree_revision
2021
def _stat_limbo_file(self, file_id=None, trans_id=None):
2022
if trans_id is None:
2023
trans_id = self._transform.trans_id_file_id(file_id)
2024
name = self._transform._limbo_name(trans_id)
2025
return os.lstat(name)
2028
def _by_parent(self):
2029
if self.__by_parent is None:
2030
self.__by_parent = self._transform.by_parent()
2031
return self.__by_parent
2033
def _comparison_data(self, entry, path):
2034
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2035
if kind == 'missing':
2039
file_id = self._transform.final_file_id(self._path2trans_id(path))
2040
executable = self.is_executable(file_id, path)
2041
return kind, executable, None
2043
def is_locked(self):
2046
def lock_read(self):
2047
# Perhaps in theory, this should lock the TreeTransform?
2054
def root_inventory(self):
2055
"""This Tree does not use inventory as its backing data."""
2056
raise NotImplementedError(_PreviewTree.root_inventory)
2058
def get_root_id(self):
2059
return self._transform.final_file_id(self._transform.root)
2061
def all_file_ids(self):
2062
tree_ids = set(self._transform._tree.all_file_ids())
2063
tree_ids.difference_update(self._transform.tree_file_id(t)
2064
for t in self._transform._removed_id)
2065
tree_ids.update(viewvalues(self._transform._new_id))
2069
return iter(self.all_file_ids())
2071
def _has_id(self, file_id, fallback_check):
2072
if file_id in self._transform._r_new_id:
2074
elif file_id in {self._transform.tree_file_id(trans_id) for
2075
trans_id in self._transform._removed_id}:
2078
return fallback_check(file_id)
2080
def has_id(self, file_id):
2081
return self._has_id(file_id, self._transform._tree.has_id)
2083
def has_or_had_id(self, file_id):
2084
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2086
def _path2trans_id(self, path):
2087
# We must not use None here, because that is a valid value to store.
2088
trans_id = self._path2trans_id_cache.get(path, object)
2089
if trans_id is not object:
2091
segments = splitpath(path)
2092
cur_parent = self._transform.root
2093
for cur_segment in segments:
2094
for child in self._all_children(cur_parent):
2095
final_name = self._final_name_cache.get(child)
2096
if final_name is None:
2097
final_name = self._transform.final_name(child)
2098
self._final_name_cache[child] = final_name
2099
if final_name == cur_segment:
2103
self._path2trans_id_cache[path] = None
2105
self._path2trans_id_cache[path] = cur_parent
2108
def path2id(self, path):
2109
if isinstance(path, list):
2112
path = osutils.pathjoin(*path)
2113
return self._transform.final_file_id(self._path2trans_id(path))
2115
def id2path(self, file_id):
2116
trans_id = self._transform.trans_id_file_id(file_id)
2118
return self._final_paths._determine_path(trans_id)
2120
raise errors.NoSuchId(self, file_id)
2122
def _all_children(self, trans_id):
2123
children = self._all_children_cache.get(trans_id)
2124
if children is not None:
2126
children = set(self._transform.iter_tree_children(trans_id))
2127
# children in the _new_parent set are provided by _by_parent.
2128
children.difference_update(self._transform._new_parent)
2129
children.update(self._by_parent.get(trans_id, []))
2130
self._all_children_cache[trans_id] = children
2133
def iter_children(self, file_id):
2134
trans_id = self._transform.trans_id_file_id(file_id)
2135
for child_trans_id in self._all_children(trans_id):
2136
yield self._transform.final_file_id(child_trans_id)
2139
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2140
in self._transform._tree.extras())
2141
possible_extras.update(self._transform._new_contents)
2142
possible_extras.update(self._transform._removed_id)
2143
for trans_id in possible_extras:
2144
if self._transform.final_file_id(trans_id) is None:
2145
yield self._final_paths._determine_path(trans_id)
2147
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
2148
yield_parents=False):
2149
for trans_id, parent_file_id in ordered_entries:
2150
file_id = self._transform.final_file_id(trans_id)
2153
if (specific_file_ids is not None
2154
and file_id not in specific_file_ids):
2156
kind = self._transform.final_kind(trans_id)
2158
kind = self._transform._tree.stored_kind(file_id)
2159
new_entry = inventory.make_entry(
2161
self._transform.final_name(trans_id),
2162
parent_file_id, file_id)
2163
yield new_entry, trans_id
2165
def _list_files_by_dir(self):
2166
todo = [ROOT_PARENT]
2168
while len(todo) > 0:
2170
parent_file_id = self._transform.final_file_id(parent)
2171
children = list(self._all_children(parent))
2172
paths = dict(zip(children, self._final_paths.get_paths(children)))
2173
children.sort(key=paths.get)
2174
todo.extend(reversed(children))
2175
for trans_id in children:
2176
ordered_ids.append((trans_id, parent_file_id))
2179
def iter_child_entries(self, file_id, path=None):
2180
self.id2path(file_id)
2181
trans_id = self._transform.trans_id_file_id(file_id)
2182
todo = [(child_trans_id, trans_id) for child_trans_id in
2183
self._all_children(trans_id)]
2184
for entry, trans_id in self._make_inv_entries(todo):
2187
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
2188
# This may not be a maximally efficient implementation, but it is
2189
# reasonably straightforward. An implementation that grafts the
2190
# TreeTransform changes onto the tree's iter_entries_by_dir results
2191
# might be more efficient, but requires tricky inferences about stack
2193
ordered_ids = self._list_files_by_dir()
2194
for entry, trans_id in self._make_inv_entries(ordered_ids,
2195
specific_file_ids, yield_parents=yield_parents):
2196
yield unicode(self._final_paths.get_path(trans_id)), entry
2198
def _iter_entries_for_dir(self, dir_path):
2199
"""Return path, entry for items in a directory without recursing down."""
2200
dir_file_id = self.path2id(dir_path)
2202
for file_id in self.iter_children(dir_file_id):
2203
trans_id = self._transform.trans_id_file_id(file_id)
2204
ordered_ids.append((trans_id, file_id))
2205
for entry, trans_id in self._make_inv_entries(ordered_ids):
2206
yield unicode(self._final_paths.get_path(trans_id)), entry
2208
def list_files(self, include_root=False, from_dir=None, recursive=True):
2209
"""See WorkingTree.list_files."""
2210
# XXX This should behave like WorkingTree.list_files, but is really
2211
# more like RevisionTree.list_files.
2215
prefix = from_dir + '/'
2216
entries = self.iter_entries_by_dir()
2217
for path, entry in entries:
2218
if entry.name == '' and not include_root:
2221
if not path.startswith(prefix):
2223
path = path[len(prefix):]
2224
yield path, 'V', entry.kind, entry.file_id, entry
2226
if from_dir is None and include_root is True:
2227
root_entry = inventory.make_entry('directory', '',
2228
ROOT_PARENT, self.get_root_id())
2229
yield '', 'V', 'directory', root_entry.file_id, root_entry
2230
entries = self._iter_entries_for_dir(from_dir or '')
2231
for path, entry in entries:
2232
yield path, 'V', entry.kind, entry.file_id, entry
2234
def kind(self, file_id):
2235
trans_id = self._transform.trans_id_file_id(file_id)
2236
return self._transform.final_kind(trans_id)
2238
def stored_kind(self, file_id):
2239
trans_id = self._transform.trans_id_file_id(file_id)
2241
return self._transform._new_contents[trans_id]
2243
return self._transform._tree.stored_kind(file_id)
2245
def get_file_mtime(self, file_id, path=None):
2246
"""See Tree.get_file_mtime"""
2247
if not self._content_change(file_id):
2248
return self._transform._tree.get_file_mtime(file_id)
2249
return self._stat_limbo_file(file_id).st_mtime
2251
def _file_size(self, entry, stat_value):
2252
return self.get_file_size(entry.file_id)
2254
def get_file_size(self, file_id):
2255
"""See Tree.get_file_size"""
2256
trans_id = self._transform.trans_id_file_id(file_id)
2257
kind = self._transform.final_kind(trans_id)
2260
if trans_id in self._transform._new_contents:
2261
return self._stat_limbo_file(trans_id=trans_id).st_size
2262
if self.kind(file_id) == 'file':
2263
return self._transform._tree.get_file_size(file_id)
2267
def get_file_verifier(self, file_id, path=None, stat_value=None):
2268
trans_id = self._transform.trans_id_file_id(file_id)
2269
kind = self._transform._new_contents.get(trans_id)
2271
return self._transform._tree.get_file_verifier(file_id)
2273
fileobj = self.get_file(file_id)
2275
return ("SHA1", sha_file(fileobj))
2279
def get_file_sha1(self, file_id, path=None, stat_value=None):
2280
trans_id = self._transform.trans_id_file_id(file_id)
2281
kind = self._transform._new_contents.get(trans_id)
2283
return self._transform._tree.get_file_sha1(file_id)
2285
fileobj = self.get_file(file_id)
2287
return sha_file(fileobj)
2291
def is_executable(self, file_id, path=None):
2294
trans_id = self._transform.trans_id_file_id(file_id)
2296
return self._transform._new_executability[trans_id]
2299
return self._transform._tree.is_executable(file_id, path)
2300
except OSError as e:
2301
if e.errno == errno.ENOENT:
2304
except errors.NoSuchId:
2307
def has_filename(self, path):
2308
trans_id = self._path2trans_id(path)
2309
if trans_id in self._transform._new_contents:
2311
elif trans_id in self._transform._removed_contents:
2314
return self._transform._tree.has_filename(path)
2316
def path_content_summary(self, path):
2317
trans_id = self._path2trans_id(path)
2318
tt = self._transform
2319
tree_path = tt._tree_id_paths.get(trans_id)
2320
kind = tt._new_contents.get(trans_id)
2322
if tree_path is None or trans_id in tt._removed_contents:
2323
return 'missing', None, None, None
2324
summary = tt._tree.path_content_summary(tree_path)
2325
kind, size, executable, link_or_sha1 = summary
2328
limbo_name = tt._limbo_name(trans_id)
2329
if trans_id in tt._new_reference_revision:
2330
kind = 'tree-reference'
2332
statval = os.lstat(limbo_name)
2333
size = statval.st_size
2334
if not tt._limbo_supports_executable():
2337
executable = statval.st_mode & S_IEXEC
2341
if kind == 'symlink':
2342
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2343
executable = tt._new_executability.get(trans_id, executable)
2344
return kind, size, executable, link_or_sha1
2346
def iter_changes(self, from_tree, include_unchanged=False,
2347
specific_files=None, pb=None, extra_trees=None,
2348
require_versioned=True, want_unversioned=False):
2349
"""See InterTree.iter_changes.
2351
This has a fast path that is only used when the from_tree matches
2352
the transform tree, and no fancy options are supplied.
2354
if (from_tree is not self._transform._tree or include_unchanged or
2355
specific_files or want_unversioned):
2356
return tree.InterTree(from_tree, self).iter_changes(
2357
include_unchanged=include_unchanged,
2358
specific_files=specific_files,
2360
extra_trees=extra_trees,
2361
require_versioned=require_versioned,
2362
want_unversioned=want_unversioned)
2363
if want_unversioned:
2364
raise ValueError('want_unversioned is not supported')
2365
return self._transform.iter_changes()
2367
def get_file(self, file_id, path=None):
2368
"""See Tree.get_file"""
2369
if not self._content_change(file_id):
2370
return self._transform._tree.get_file(file_id, path)
2371
trans_id = self._transform.trans_id_file_id(file_id)
2372
name = self._transform._limbo_name(trans_id)
2373
return open(name, 'rb')
2375
def get_file_with_stat(self, file_id, path=None):
2376
return self.get_file(file_id, path), None
2378
def annotate_iter(self, file_id,
2379
default_revision=_mod_revision.CURRENT_REVISION):
2380
changes = self._iter_changes_cache.get(file_id)
2384
changed_content, versioned, kind = (changes[2], changes[3],
2388
get_old = (kind[0] == 'file' and versioned[0])
2390
old_annotation = self._transform._tree.annotate_iter(file_id,
2391
default_revision=default_revision)
2395
return old_annotation
2396
if not changed_content:
2397
return old_annotation
2398
# TODO: This is doing something similar to what WT.annotate_iter is
2399
# doing, however it fails slightly because it doesn't know what
2400
# the *other* revision_id is, so it doesn't know how to give the
2401
# other as the origin for some lines, they all get
2402
# 'default_revision'
2403
# It would be nice to be able to use the new Annotator based
2404
# approach, as well.
2405
return annotate.reannotate([old_annotation],
2406
self.get_file(file_id).readlines(),
2409
def get_symlink_target(self, file_id, path=None):
2410
"""See Tree.get_symlink_target"""
2411
if not self._content_change(file_id):
2412
return self._transform._tree.get_symlink_target(file_id)
2413
trans_id = self._transform.trans_id_file_id(file_id)
2414
name = self._transform._limbo_name(trans_id)
2415
return osutils.readlink(name)
2417
def walkdirs(self, prefix=''):
2418
pending = [self._transform.root]
2419
while len(pending) > 0:
2420
parent_id = pending.pop()
2423
prefix = prefix.rstrip('/')
2424
parent_path = self._final_paths.get_path(parent_id)
2425
parent_file_id = self._transform.final_file_id(parent_id)
2426
for child_id in self._all_children(parent_id):
2427
path_from_root = self._final_paths.get_path(child_id)
2428
basename = self._transform.final_name(child_id)
2429
file_id = self._transform.final_file_id(child_id)
2430
kind = self._transform.final_kind(child_id)
2431
if kind is not None:
2432
versioned_kind = kind
2435
versioned_kind = self._transform._tree.stored_kind(file_id)
2436
if versioned_kind == 'directory':
2437
subdirs.append(child_id)
2438
children.append((path_from_root, basename, kind, None,
2439
file_id, versioned_kind))
2441
if parent_path.startswith(prefix):
2442
yield (parent_path, parent_file_id), children
2443
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2446
def get_parent_ids(self):
2447
return self._parent_ids
2449
def set_parent_ids(self, parent_ids):
2450
self._parent_ids = parent_ids
2452
def get_revision_tree(self, revision_id):
2453
return self._transform._tree.get_revision_tree(revision_id)
599
2456
def joinpath(parent, child):
600
2457
"""Join tree-relative paths, handling the tree root specially"""
601
2458
if parent is None or parent == "":