605
536
trans_ids.update(self._new_parent)
608
def _get_file_id_maps(self):
609
"""Return mapping of file_ids to trans_ids in the to and from states"""
610
trans_ids = self._affected_ids()
613
# Build up two dicts: trans_ids associated with file ids in the
614
# FROM state, vs the TO state.
615
for trans_id in trans_ids:
616
from_file_id = self.tree_file_id(trans_id)
617
if from_file_id is not None:
618
from_trans_ids[from_file_id] = trans_id
619
to_file_id = self.final_file_id(trans_id)
620
if to_file_id is not None:
621
to_trans_ids[to_file_id] = trans_id
622
return from_trans_ids, to_trans_ids
624
def _from_file_data(self, from_trans_id, from_versioned, from_path):
625
"""Get data about a file in the from (tree) state
627
Return a (name, parent, kind, executable) tuple
629
from_path = self._tree_id_paths.get(from_trans_id)
631
# get data from working tree if versioned
632
from_entry = next(self._tree.iter_entries_by_dir(
633
specific_files=[from_path]))[1]
634
from_name = from_entry.name
635
from_parent = from_entry.parent_id
638
if from_path is None:
639
# File does not exist in FROM state
643
# File exists, but is not versioned. Have to use path-
645
from_name = os.path.basename(from_path)
646
tree_parent = self.get_tree_parent(from_trans_id)
647
from_parent = self.tree_file_id(tree_parent)
648
if from_path is not None:
649
from_kind, from_executable, from_stats = \
650
self._tree._comparison_data(from_entry, from_path)
653
from_executable = False
654
return from_name, from_parent, from_kind, from_executable
656
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
657
"""Get data about a file in the to (target) state
659
Return a (name, parent, kind, executable) tuple
661
to_name = self.final_name(to_trans_id)
662
to_kind = self.final_kind(to_trans_id)
663
to_parent = self.final_file_id(self.final_parent(to_trans_id))
664
if to_trans_id in self._new_executability:
665
to_executable = self._new_executability[to_trans_id]
666
elif to_trans_id == from_trans_id:
667
to_executable = from_executable
669
to_executable = False
670
return to_name, to_parent, to_kind, to_executable
672
def iter_changes(self):
539
def iter_changes(self, want_unversioned=False):
673
540
"""Produce output in the same format as Tree.iter_changes.
675
542
Will produce nonsensical results if invoked while inventory/filesystem
676
543
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
678
This reads the Transform, but only reproduces changes involving a
679
file_id. Files that are not versioned in either of the FROM or TO
680
states are not reflected.
682
545
final_paths = FinalPaths(self)
683
from_trans_ids, to_trans_ids = self._get_file_id_maps()
546
trans_ids = self._affected_ids()
685
# Now iterate through all active file_ids
686
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)
688
from_trans_id = from_trans_ids.get(file_id)
689
552
# find file ids, and determine versioning state
690
if from_trans_id is None:
553
if from_path is None:
691
554
from_versioned = False
692
from_trans_id = to_trans_ids[file_id]
694
from_versioned = True
695
to_trans_id = to_trans_ids.get(file_id)
696
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)
697
561
to_versioned = False
698
to_trans_id = from_trans_id
702
if not from_versioned:
705
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:
709
to_path = final_paths.get_path(to_trans_id)
711
from_name, from_parent, from_kind, from_executable = \
712
self._from_file_data(from_trans_id, from_versioned, from_path)
714
to_name, to_parent, to_kind, to_executable = \
715
self._to_file_data(to_trans_id, from_trans_id, from_executable)
717
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:
719
597
elif to_kind in ('file', 'symlink') and (
720
to_trans_id != from_trans_id
721
or to_trans_id in self._new_contents):
598
trans_id in self._new_contents):
723
600
if (not modified and from_versioned == to_versioned
724
and from_parent == to_parent and from_name == to_name
601
and from_path == to_path
602
and from_name == to_name
725
603
and from_executable == to_executable):
605
if (from_path, to_path) == (None, None):
729
file_id, (from_path, to_path), modified,
609
(from_path, to_path), modified,
730
610
(from_versioned, to_versioned),
731
(from_parent, to_parent),
732
611
(from_name, to_name),
733
612
(from_kind, to_kind),
734
613
(from_executable, to_executable)))
828
707
return (self._tree.get_file_lines(path),)
830
def serialize(self, serializer):
831
"""Serialize this TreeTransform.
833
:param serializer: A Serialiser like pack.ContainerSerializer.
835
from .. import bencode
836
new_name = {k.encode('utf-8'): v.encode('utf-8')
837
for k, v in self._new_name.items()}
838
new_parent = {k.encode('utf-8'): v.encode('utf-8')
839
for k, v in self._new_parent.items()}
840
new_id = {k.encode('utf-8'): v
841
for k, v in self._new_id.items()}
842
new_executability = {k.encode('utf-8'): int(v)
843
for k, v in self._new_executability.items()}
844
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
845
for k, v in self._tree_path_ids.items()}
846
non_present_ids = {k: v.encode('utf-8')
847
for k, v in self._non_present_ids.items()}
848
removed_contents = [trans_id.encode('utf-8')
849
for trans_id in self._removed_contents]
850
removed_id = [trans_id.encode('utf-8')
851
for trans_id in self._removed_id]
853
b'_id_number': self._id_number,
854
b'_new_name': new_name,
855
b'_new_parent': new_parent,
856
b'_new_executability': new_executability,
858
b'_tree_path_ids': tree_path_ids,
859
b'_removed_id': removed_id,
860
b'_removed_contents': removed_contents,
861
b'_non_present_ids': non_present_ids,
863
yield serializer.bytes_record(bencode.bencode(attribs),
865
for trans_id, kind in sorted(self._new_contents.items()):
867
with open(self._limbo_name(trans_id), 'rb') as cur_file:
868
lines = cur_file.readlines()
869
parents = self._get_parents_lines(trans_id)
870
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
871
content = b''.join(mpdiff.to_patch())
872
if kind == 'directory':
874
if kind == 'symlink':
875
content = self._read_symlink_target(trans_id)
876
if not isinstance(content, bytes):
877
content = content.encode('utf-8')
878
yield serializer.bytes_record(
879
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
881
def deserialize(self, records):
882
"""Deserialize a stored TreeTransform.
884
:param records: An iterable of (names, content) tuples, as per
885
pack.ContainerPushParser.
887
from .. import bencode
888
names, content = next(records)
889
attribs = bencode.bdecode(content)
890
self._id_number = attribs[b'_id_number']
891
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
892
for k, v in attribs[b'_new_name'].items()}
893
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
894
for k, v in attribs[b'_new_parent'].items()}
895
self._new_executability = {
896
k.decode('utf-8'): bool(v)
897
for k, v in attribs[b'_new_executability'].items()}
898
self._new_id = {k.decode('utf-8'): v
899
for k, v in attribs[b'_new_id'].items()}
900
self._r_new_id = {v: k for k, v in self._new_id.items()}
901
self._tree_path_ids = {}
902
self._tree_id_paths = {}
903
for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
904
path = bytepath.decode('utf-8')
905
trans_id = trans_id.decode('utf-8')
906
self._tree_path_ids[path] = trans_id
907
self._tree_id_paths[trans_id] = path
908
self._removed_id = {trans_id.decode('utf-8')
909
for trans_id in attribs[b'_removed_id']}
910
self._removed_contents = set(
911
trans_id.decode('utf-8')
912
for trans_id in attribs[b'_removed_contents'])
913
self._non_present_ids = {
915
for k, v in attribs[b'_non_present_ids'].items()}
916
for ((trans_id, kind),), content in records:
917
trans_id = trans_id.decode('utf-8')
918
kind = kind.decode('ascii')
920
mpdiff = multiparent.MultiParent.from_patch(content)
921
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
922
self.create_file(lines, trans_id)
923
if kind == 'directory':
924
self.create_directory(trans_id)
925
if kind == 'symlink':
926
self.create_symlink(content.decode('utf-8'), trans_id)
928
709
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
929
710
"""Schedule creation of a new file.
1593
1449
self._new_contents.clear()
1594
1450
return modified_paths
1596
def _inventory_altered(self):
1597
"""Determine which trans_ids need new Inventory entries.
1599
An new entry is needed when anything that would be reflected by an
1600
inventory entry changes, including file name, file_id, parent file_id,
1601
file kind, and the execute bit.
1603
Some care is taken to return entries with real changes, not cases
1604
where the value is deleted and then restored to its original value,
1605
but some actually unchanged values may be returned.
1607
:returns: A list of (path, trans_id) for all items requiring an
1608
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)
1610
1457
changed_ids = set()
1611
# Find entries whose file_ids are new (or changed).
1612
new_file_id = set(t for t in self._new_id
1613
if self._new_id[t] != self.tree_file_id(t))
1614
for id_set in [self._new_name, self._new_parent, new_file_id,
1458
for id_set in [self._new_name, self._new_parent,
1615
1459
self._new_executability]:
1616
1460
changed_ids.update(id_set)
1617
# removing implies a kind change
1618
changed_kind = set(self._removed_contents)
1461
for id_set in [self._new_name, self._new_parent]:
1462
removed_id.update(id_set)
1619
1463
# so does adding
1620
changed_kind.intersection_update(self._new_contents)
1464
changed_kind = set(self._new_contents)
1621
1465
# Ignore entries that are already known to have changed.
1622
1466
changed_kind.difference_update(changed_ids)
1623
1467
# to keep only the truly changed ones
1624
1468
changed_kind = (t for t in changed_kind
1625
1469
if self.tree_kind(t) != self.final_kind(t))
1626
# all kind changes will alter the inventory
1627
1470
changed_ids.update(changed_kind)
1628
# To find entries with changed parent_ids, find parents which existed,
1629
# but changed file_id.
1630
# Now add all their children to the set.
1631
for parent_trans_id in new_file_id:
1632
changed_ids.update(self.iter_tree_children(parent_trans_id))
1633
return sorted(FinalPaths(self).get_paths(changed_ids))
1635
def _generate_transform_changes(self):
1636
"""Generate an inventory delta for the current transform."""
1638
new_paths = self._inventory_altered()
1639
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)
1640
1477
with ui.ui_factory.nested_progress_bar() as child_pb:
1641
for num, trans_id in enumerate(self._removed_id):
1478
for num, trans_id in enumerate(removed_id):
1642
1479
if (num % 10) == 0:
1643
1480
child_pb.update(gettext('removing file'),
1644
1481
num, total_entries)
1645
if trans_id == self._new_root:
1646
file_id = self._tree.path2id('')
1648
file_id = self.tree_file_id(trans_id)
1649
# File-id isn't really being deleted, just moved
1650
if file_id in self._r_new_id:
1483
path = self._tree_id_paths[trans_id]
1652
path = self._tree_id_paths[trans_id]
1653
changes.append((path, None, None))
1654
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1486
changes[path] = (None, None, None, None)
1656
1487
for num, (path, trans_id) in enumerate(new_paths):
1657
1488
if (num % 10) == 0:
1658
1489
child_pb.update(gettext('adding file'),
1659
num + len(self._removed_id), total_entries)
1660
file_id = new_path_file_ids[trans_id]
1490
num + len(removed_id), total_entries)
1663
1492
kind = self.final_kind(trans_id)
1664
1493
if kind is None:
1665
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1666
parent_trans_id = self.final_parent(trans_id)
1667
parent_file_id = new_path_file_ids.get(parent_trans_id)
1668
if parent_file_id is None:
1669
parent_file_id = self.final_file_id(parent_trans_id)
1670
if trans_id in self._new_reference_revision:
1671
new_entry = inventory.TreeReference(
1673
self._new_name[trans_id],
1674
self.final_file_id(self._new_parent[trans_id]),
1675
None, self._new_reference_revision[trans_id])
1677
new_entry = inventory.make_entry(kind,
1678
self.final_name(trans_id),
1679
parent_file_id, file_id)
1681
old_path = self._tree.id2path(new_entry.file_id)
1682
except errors.NoSuchId:
1684
new_executability = self._new_executability.get(trans_id)
1685
if new_executability is not None:
1686
new_entry.executable = new_executability
1688
(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: