619
536
trans_ids.update(self._new_parent)
622
def _get_file_id_maps(self):
623
"""Return mapping of file_ids to trans_ids in the to and from states"""
624
trans_ids = self._affected_ids()
627
# Build up two dicts: trans_ids associated with file ids in the
628
# FROM state, vs the TO state.
629
for trans_id in trans_ids:
630
from_file_id = self.tree_file_id(trans_id)
631
if from_file_id is not None:
632
from_trans_ids[from_file_id] = trans_id
633
to_file_id = self.final_file_id(trans_id)
634
if to_file_id is not None:
635
to_trans_ids[to_file_id] = trans_id
636
return from_trans_ids, to_trans_ids
638
def _from_file_data(self, from_trans_id, from_versioned, from_path):
639
"""Get data about a file in the from (tree) state
641
Return a (name, parent, kind, executable) tuple
643
from_path = self._tree_id_paths.get(from_trans_id)
645
# get data from working tree if versioned
646
from_entry = next(self._tree.iter_entries_by_dir(
647
specific_files=[from_path]))[1]
648
from_name = from_entry.name
649
from_parent = from_entry.parent_id
652
if from_path is None:
653
# File does not exist in FROM state
657
# File exists, but is not versioned. Have to use path-
659
from_name = os.path.basename(from_path)
660
tree_parent = self.get_tree_parent(from_trans_id)
661
from_parent = self.tree_file_id(tree_parent)
662
if from_path is not None:
663
from_kind, from_executable, from_stats = \
664
self._tree._comparison_data(from_entry, from_path)
667
from_executable = False
668
return from_name, from_parent, from_kind, from_executable
670
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
671
"""Get data about a file in the to (target) state
673
Return a (name, parent, kind, executable) tuple
675
to_name = self.final_name(to_trans_id)
676
to_kind = self.final_kind(to_trans_id)
677
to_parent = self.final_file_id(self.final_parent(to_trans_id))
678
if to_trans_id in self._new_executability:
679
to_executable = self._new_executability[to_trans_id]
680
elif to_trans_id == from_trans_id:
681
to_executable = from_executable
683
to_executable = False
684
return to_name, to_parent, to_kind, to_executable
686
def iter_changes(self):
539
def iter_changes(self, want_unversioned=False):
687
540
"""Produce output in the same format as Tree.iter_changes.
689
542
Will produce nonsensical results if invoked while inventory/filesystem
690
conflicts (as reported by TreeTransform.find_conflicts()) are present.
692
This reads the Transform, but only reproduces changes involving a
693
file_id. Files that are not versioned in either of the FROM or TO
694
states are not reflected.
543
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
696
545
final_paths = FinalPaths(self)
697
from_trans_ids, to_trans_ids = self._get_file_id_maps()
546
trans_ids = self._affected_ids()
699
# Now iterate through all active file_ids
700
for file_id in set(from_trans_ids).union(to_trans_ids):
548
# Now iterate through all active paths
549
for trans_id in trans_ids:
550
from_path = self.tree_path(trans_id)
702
from_trans_id = from_trans_ids.get(file_id)
703
552
# find file ids, and determine versioning state
704
if from_trans_id is None:
553
if from_path is None:
705
554
from_versioned = False
706
from_trans_id = to_trans_ids[file_id]
708
from_versioned = True
709
to_trans_id = to_trans_ids.get(file_id)
710
if to_trans_id is None:
556
from_versioned = self._tree.is_versioned(from_path)
557
if not want_unversioned and not from_versioned:
559
to_path = final_paths.get_path(trans_id)
711
561
to_versioned = False
712
to_trans_id = from_trans_id
716
if not from_versioned:
719
from_path = self._tree_id_paths.get(from_trans_id)
563
to_versioned = self.final_is_versioned(trans_id)
564
if not want_unversioned and not to_versioned:
723
to_path = final_paths.get_path(to_trans_id)
725
from_name, from_parent, from_kind, from_executable = \
726
self._from_file_data(from_trans_id, from_versioned, from_path)
728
to_name, to_parent, to_kind, to_executable = \
729
self._to_file_data(to_trans_id, from_trans_id, from_executable)
731
if from_kind != to_kind:
568
# get data from working tree if versioned
569
from_entry = next(self._tree.iter_entries_by_dir(
570
specific_files=[from_path]))[1]
571
from_name = from_entry.name
574
if from_path is None:
575
# File does not exist in FROM state
578
# File exists, but is not versioned. Have to use path-
580
from_name = os.path.basename(from_path)
581
if from_path is not None:
582
from_kind, from_executable, from_stats = \
583
self._tree._comparison_data(from_entry, from_path)
586
from_executable = False
588
to_name = self.final_name(trans_id)
589
to_kind = self.final_kind(trans_id)
590
if trans_id in self._new_executability:
591
to_executable = self._new_executability[trans_id]
593
to_executable = from_executable
595
if from_versioned and from_kind != to_kind:
733
597
elif to_kind in ('file', 'symlink') and (
734
to_trans_id != from_trans_id
735
or to_trans_id in self._new_contents):
598
trans_id in self._new_contents):
737
600
if (not modified and from_versioned == to_versioned
738
and from_parent == to_parent and from_name == to_name
601
and from_path == to_path
602
and from_name == to_name
739
603
and from_executable == to_executable):
605
if (from_path, to_path) == (None, None):
743
file_id, (from_path, to_path), modified,
609
(from_path, to_path), modified,
744
610
(from_versioned, to_versioned),
745
(from_parent, to_parent),
746
611
(from_name, to_name),
747
612
(from_kind, to_kind),
748
613
(from_executable, to_executable)))
842
707
return (self._tree.get_file_lines(path),)
844
def serialize(self, serializer):
845
"""Serialize this TreeTransform.
847
:param serializer: A Serialiser like pack.ContainerSerializer.
849
from .. import bencode
850
new_name = {k.encode('utf-8'): v.encode('utf-8')
851
for k, v in self._new_name.items()}
852
new_parent = {k.encode('utf-8'): v.encode('utf-8')
853
for k, v in self._new_parent.items()}
854
new_id = {k.encode('utf-8'): v
855
for k, v in self._new_id.items()}
856
new_executability = {k.encode('utf-8'): int(v)
857
for k, v in self._new_executability.items()}
858
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
859
for k, v in self._tree_path_ids.items()}
860
non_present_ids = {k: v.encode('utf-8')
861
for k, v in self._non_present_ids.items()}
862
removed_contents = [trans_id.encode('utf-8')
863
for trans_id in self._removed_contents]
864
removed_id = [trans_id.encode('utf-8')
865
for trans_id in self._removed_id]
867
b'_id_number': self._id_number,
868
b'_new_name': new_name,
869
b'_new_parent': new_parent,
870
b'_new_executability': new_executability,
872
b'_tree_path_ids': tree_path_ids,
873
b'_removed_id': removed_id,
874
b'_removed_contents': removed_contents,
875
b'_non_present_ids': non_present_ids,
877
yield serializer.bytes_record(bencode.bencode(attribs),
879
for trans_id, kind in sorted(self._new_contents.items()):
881
with open(self._limbo_name(trans_id), 'rb') as cur_file:
882
lines = cur_file.readlines()
883
parents = self._get_parents_lines(trans_id)
884
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
885
content = b''.join(mpdiff.to_patch())
886
if kind == 'directory':
888
if kind == 'symlink':
889
content = self._read_symlink_target(trans_id)
890
if not isinstance(content, bytes):
891
content = content.encode('utf-8')
892
yield serializer.bytes_record(
893
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
895
def deserialize(self, records):
896
"""Deserialize a stored TreeTransform.
898
:param records: An iterable of (names, content) tuples, as per
899
pack.ContainerPushParser.
901
from .. import bencode
902
names, content = next(records)
903
attribs = bencode.bdecode(content)
904
self._id_number = attribs[b'_id_number']
905
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
906
for k, v in attribs[b'_new_name'].items()}
907
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
908
for k, v in attribs[b'_new_parent'].items()}
909
self._new_executability = {
910
k.decode('utf-8'): bool(v)
911
for k, v in attribs[b'_new_executability'].items()}
912
self._new_id = {k.decode('utf-8'): v
913
for k, v in attribs[b'_new_id'].items()}
914
self._r_new_id = {v: k for k, v in self._new_id.items()}
915
self._tree_path_ids = {}
916
self._tree_id_paths = {}
917
for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
918
path = bytepath.decode('utf-8')
919
trans_id = trans_id.decode('utf-8')
920
self._tree_path_ids[path] = trans_id
921
self._tree_id_paths[trans_id] = path
922
self._removed_id = {trans_id.decode('utf-8')
923
for trans_id in attribs[b'_removed_id']}
924
self._removed_contents = set(
925
trans_id.decode('utf-8')
926
for trans_id in attribs[b'_removed_contents'])
927
self._non_present_ids = {
929
for k, v in attribs[b'_non_present_ids'].items()}
930
for ((trans_id, kind),), content in records:
931
trans_id = trans_id.decode('utf-8')
932
kind = kind.decode('ascii')
934
mpdiff = multiparent.MultiParent.from_patch(content)
935
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
936
self.create_file(lines, trans_id)
937
if kind == 'directory':
938
self.create_directory(trans_id)
939
if kind == 'symlink':
940
self.create_symlink(content.decode('utf-8'), trans_id)
942
709
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
943
710
"""Schedule creation of a new file.
1588
1449
self._new_contents.clear()
1589
1450
return modified_paths
1591
def _inventory_altered(self):
1592
"""Determine which trans_ids need new Inventory entries.
1594
An new entry is needed when anything that would be reflected by an
1595
inventory entry changes, including file name, file_id, parent file_id,
1596
file kind, and the execute bit.
1598
Some care is taken to return entries with real changes, not cases
1599
where the value is deleted and then restored to its original value,
1600
but some actually unchanged values may be returned.
1602
:returns: A list of (path, trans_id) for all items requiring an
1603
inventory change. Ordered by path.
1452
def _generate_index_changes(self):
1453
"""Generate an inventory delta for the current transform."""
1454
removed_id = set(self._removed_id)
1455
removed_id.update(self._removed_contents)
1605
1457
changed_ids = set()
1606
# Find entries whose file_ids are new (or changed).
1607
new_file_id = set(t for t in self._new_id
1608
if self._new_id[t] != self.tree_file_id(t))
1609
for id_set in [self._new_name, self._new_parent, new_file_id,
1458
for id_set in [self._new_name, self._new_parent,
1610
1459
self._new_executability]:
1611
1460
changed_ids.update(id_set)
1612
# removing implies a kind change
1613
changed_kind = set(self._removed_contents)
1461
for id_set in [self._new_name, self._new_parent]:
1462
removed_id.update(id_set)
1614
1463
# so does adding
1615
changed_kind.intersection_update(self._new_contents)
1464
changed_kind = set(self._new_contents)
1616
1465
# Ignore entries that are already known to have changed.
1617
1466
changed_kind.difference_update(changed_ids)
1618
1467
# to keep only the truly changed ones
1619
1468
changed_kind = (t for t in changed_kind
1620
1469
if self.tree_kind(t) != self.final_kind(t))
1621
# all kind changes will alter the inventory
1622
1470
changed_ids.update(changed_kind)
1623
# To find entries with changed parent_ids, find parents which existed,
1624
# but changed file_id.
1625
# Now add all their children to the set.
1626
for parent_trans_id in new_file_id:
1627
changed_ids.update(self.iter_tree_children(parent_trans_id))
1628
return sorted(FinalPaths(self).get_paths(changed_ids))
1630
def _generate_transform_changes(self):
1631
"""Generate an inventory delta for the current transform."""
1633
new_paths = self._inventory_altered()
1634
total_entries = len(new_paths) + len(self._removed_id)
1471
for t in changed_kind:
1472
if self.final_kind(t) == 'directory':
1474
changed_ids.remove(t)
1475
new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1476
total_entries = len(new_paths) + len(removed_id)
1635
1477
with ui.ui_factory.nested_progress_bar() as child_pb:
1636
for num, trans_id in enumerate(self._removed_id):
1478
for num, trans_id in enumerate(removed_id):
1637
1479
if (num % 10) == 0:
1638
1480
child_pb.update(gettext('removing file'),
1639
1481
num, total_entries)
1640
if trans_id == self._new_root:
1641
file_id = self._tree.path2id('')
1643
file_id = self.tree_file_id(trans_id)
1644
# File-id isn't really being deleted, just moved
1645
if file_id in self._r_new_id:
1483
path = self._tree_id_paths[trans_id]
1647
path = self._tree_id_paths[trans_id]
1648
changes.append((path, None, None))
1649
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1486
changes[path] = (None, None, None, None)
1651
1487
for num, (path, trans_id) in enumerate(new_paths):
1652
1488
if (num % 10) == 0:
1653
1489
child_pb.update(gettext('adding file'),
1654
num + len(self._removed_id), total_entries)
1655
file_id = new_path_file_ids[trans_id]
1490
num + len(removed_id), total_entries)
1658
1492
kind = self.final_kind(trans_id)
1659
1493
if kind is None:
1660
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1661
parent_trans_id = self.final_parent(trans_id)
1662
parent_file_id = new_path_file_ids.get(parent_trans_id)
1663
if parent_file_id is None:
1664
parent_file_id = self.final_file_id(parent_trans_id)
1665
if trans_id in self._new_reference_revision:
1666
new_entry = inventory.TreeReference(
1668
self._new_name[trans_id],
1669
self.final_file_id(self._new_parent[trans_id]),
1670
None, self._new_reference_revision[trans_id])
1672
new_entry = inventory.make_entry(kind,
1673
self.final_name(trans_id),
1674
parent_file_id, file_id)
1676
old_path = self._tree.id2path(new_entry.file_id)
1677
except errors.NoSuchId:
1679
new_executability = self._new_executability.get(trans_id)
1680
if new_executability is not None:
1681
new_entry.executable = new_executability
1683
(old_path, path, new_entry))
1495
versioned = self.final_is_versioned(trans_id)
1498
executability = self._new_executability.get(trans_id)
1499
reference_revision = self._new_reference_revision.get(trans_id)
1500
symlink_target = self._symlink_target.get(trans_id)
1502
kind, executability, reference_revision, symlink_target)
1503
return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1506
class GitTransformPreview(GitTreeTransform):
1507
"""A TreeTransform for generating preview trees.
1509
Unlike TreeTransform, this version works when the input tree is a
1510
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1511
unversioned files in the input tree.
1514
def __init__(self, tree, pb=None, case_sensitive=True):
1516
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1517
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1519
def canonical_path(self, path):
1522
def tree_kind(self, trans_id):
1523
path = self.tree_path(trans_id)
1526
kind = self._tree.path_content_summary(path)[0]
1527
if kind == 'missing':
1531
def _set_mode(self, trans_id, mode_id, typefunc):
1532
"""Set the mode of new file contents.
1533
The mode_id is the existing file to get the mode from (often the same
1534
as trans_id). The operation is only performed if there's a mode match
1535
according to typefunc.
1537
# is it ok to ignore this? probably
1540
def iter_tree_children(self, parent_id):
1541
"""Iterate through the entry's tree children, if any"""
1543
path = self._tree_id_paths[parent_id]
1547
for child in self._tree.iter_child_entries(path):
1548
childpath = joinpath(path, child.name)
1549
yield self.trans_id_tree_path(childpath)
1550
except errors.NoSuchFile:
1553
def new_orphan(self, trans_id, parent_id):
1554
raise NotImplementedError(self.new_orphan)
1557
class GitPreviewTree(PreviewTree, GitTree):
1558
"""Partial implementation of Tree to support show_diff_trees"""
1560
def __init__(self, transform):
1561
PreviewTree.__init__(self, transform)
1562
self.store = transform._tree.store
1563
self.mapping = transform._tree.mapping
1564
self._final_paths = FinalPaths(transform)
1566
def supports_setting_file_ids(self):
1569
def _supports_executable(self):
1570
return self._transform._limbo_supports_executable()
1572
def walkdirs(self, prefix=''):
1573
pending = [self._transform.root]
1574
while len(pending) > 0:
1575
parent_id = pending.pop()
1578
prefix = prefix.rstrip('/')
1579
parent_path = self._final_paths.get_path(parent_id)
1580
for child_id in self._all_children(parent_id):
1581
path_from_root = self._final_paths.get_path(child_id)
1582
basename = self._transform.final_name(child_id)
1583
kind = self._transform.final_kind(child_id)
1584
if kind is not None:
1585
versioned_kind = kind
1588
versioned_kind = self._transform._tree.stored_kind(
1590
if versioned_kind == 'directory':
1591
subdirs.append(child_id)
1592
children.append((path_from_root, basename, kind, None,
1595
if parent_path.startswith(prefix):
1596
yield parent_path, children
1597
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1600
def iter_changes(self, from_tree, include_unchanged=False,
1601
specific_files=None, pb=None, extra_trees=None,
1602
require_versioned=True, want_unversioned=False):
1603
"""See InterTree.iter_changes.
1605
This has a fast path that is only used when the from_tree matches
1606
the transform tree, and no fancy options are supplied.
1608
return InterTree.get(from_tree, self).iter_changes(
1609
include_unchanged=include_unchanged,
1610
specific_files=specific_files,
1612
extra_trees=extra_trees,
1613
require_versioned=require_versioned,
1614
want_unversioned=want_unversioned)
1616
def get_file(self, path):
1617
"""See Tree.get_file"""
1618
trans_id = self._path2trans_id(path)
1619
if trans_id is None:
1620
raise errors.NoSuchFile(path)
1621
if trans_id in self._transform._new_contents:
1622
name = self._transform._limbo_name(trans_id)
1623
return open(name, 'rb')
1624
if trans_id in self._transform._removed_contents:
1625
raise errors.NoSuchFile(path)
1626
orig_path = self._transform.tree_path(trans_id)
1627
return self._transform._tree.get_file(orig_path)
1629
def get_symlink_target(self, path):
1630
"""See Tree.get_symlink_target"""
1631
trans_id = self._path2trans_id(path)
1632
if trans_id is None:
1633
raise errors.NoSuchFile(path)
1634
if trans_id not in self._transform._new_contents:
1635
orig_path = self._transform.tree_path(trans_id)
1636
return self._transform._tree.get_symlink_target(orig_path)
1637
name = self._transform._limbo_name(trans_id)
1638
return osutils.readlink(name)
1640
def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1641
trans_id = self._path2trans_id(path)
1642
if trans_id is None:
1644
orig_path = self._transform.tree_path(trans_id)
1645
if orig_path is not None:
1646
old_annotation = self._transform._tree.annotate_iter(
1647
orig_path, default_revision=default_revision)
1651
lines = self.get_file_lines(path)
1652
except errors.NoSuchFile:
1654
return annotate.reannotate([old_annotation], lines, default_revision)
1656
def get_file_text(self, path):
1657
"""Return the byte content of a file.
1659
:param path: The path of the file.
1661
:returns: A single byte string for the whole file.
1663
with self.get_file(path) as my_file:
1664
return my_file.read()
1666
def get_file_lines(self, path):
1667
"""Return the content of a file, as lines.
1669
:param path: The path of the file.
1671
return osutils.split_lines(self.get_file_text(path))
1674
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1675
in self._transform._tree.extras())
1676
possible_extras.update(self._transform._new_contents)
1677
possible_extras.update(self._transform._removed_id)
1678
for trans_id in possible_extras:
1679
if not self._transform.final_is_versioned(trans_id):
1680
yield self._final_paths._determine_path(trans_id)
1682
def path_content_summary(self, path):
1683
trans_id = self._path2trans_id(path)
1684
tt = self._transform
1685
tree_path = tt.tree_path(trans_id)
1686
kind = tt._new_contents.get(trans_id)
1688
if tree_path is None or trans_id in tt._removed_contents:
1689
return 'missing', None, None, None
1690
summary = tt._tree.path_content_summary(tree_path)
1691
kind, size, executable, link_or_sha1 = summary
1694
limbo_name = tt._limbo_name(trans_id)
1695
if trans_id in tt._new_reference_revision:
1696
kind = 'tree-reference'
1698
statval = os.lstat(limbo_name)
1699
size = statval.st_size
1700
if not tt._limbo_supports_executable():
1703
executable = statval.st_mode & S_IEXEC
1707
if kind == 'symlink':
1708
link_or_sha1 = os.readlink(limbo_name)
1709
if not isinstance(link_or_sha1, str):
1710
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1711
executable = tt._new_executability.get(trans_id, executable)
1712
return kind, size, executable, link_or_sha1
1714
def get_file_mtime(self, path):
1715
"""See Tree.get_file_mtime"""
1716
trans_id = self._path2trans_id(path)
1717
if trans_id is None:
1718
raise errors.NoSuchFile(path)
1719
if trans_id not in self._transform._new_contents:
1720
return self._transform._tree.get_file_mtime(
1721
self._transform.tree_path(trans_id))
1722
name = self._transform._limbo_name(trans_id)
1723
statval = os.lstat(name)
1724
return statval.st_mtime
1726
def is_versioned(self, path):
1727
trans_id = self._path2trans_id(path)
1728
if trans_id is None:
1729
# It doesn't exist, so it's not versioned.
1731
if trans_id in self._transform._versioned:
1733
if trans_id in self._transform._removed_id:
1735
orig_path = self._transform.tree_path(trans_id)
1736
return self._transform._tree.is_versioned(orig_path)
1738
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1740
raise NotImplementedError(
1741
'follow tree references not yet supported')
1743
# This may not be a maximally efficient implementation, but it is
1744
# reasonably straightforward. An implementation that grafts the
1745
# TreeTransform changes onto the tree's iter_entries_by_dir results
1746
# might be more efficient, but requires tricky inferences about stack
1748
for trans_id, path in self._list_files_by_dir():
1749
entry, is_versioned = self._transform.final_entry(trans_id)
1752
if not is_versioned and entry.kind != 'directory':
1754
if specific_files is not None and path not in specific_files:
1756
if entry is not None:
1759
def _list_files_by_dir(self):
1760
todo = [ROOT_PARENT]
1761
while len(todo) > 0:
1763
children = list(self._all_children(parent))
1764
paths = dict(zip(children, self._final_paths.get_paths(children)))
1765
children.sort(key=paths.get)
1766
todo.extend(reversed(children))
1767
for trans_id in children:
1768
yield trans_id, paths[trans_id][0]
1770
def revision_tree(self, revision_id):
1771
return self._transform._tree.revision_tree(revision_id)
1773
def _stat_limbo_file(self, trans_id):
1774
name = self._transform._limbo_name(trans_id)
1775
return os.lstat(name)
1777
def git_snapshot(self, want_unversioned=False):
1780
for trans_id, path in self._list_files_by_dir():
1781
if not self._transform.final_is_versioned(trans_id):
1782
if not want_unversioned:
1785
o, mode = self._transform.final_git_entry(trans_id)
1787
self.store.add_object(o)
1788
os.append((encode_git_path(path), o.id, mode))
1791
return commit_tree(self.store, os), extra
1793
def iter_child_entries(self, path):
1794
trans_id = self._path2trans_id(path)
1795
if trans_id is None:
1796
raise errors.NoSuchFile(path)
1797
for child_trans_id in self._all_children(trans_id):
1798
entry, is_versioned = self._transform.final_entry(trans_id)
1799
if not is_versioned:
1801
if entry is not None: