~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Martin Pool
  • Date: 2006-06-20 03:30:14 UTC
  • mfrom: (1793 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1797.
  • Revision ID: mbp@sourcefrog.net-20060620033014-e19ce470e2ce6561
[merge] bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
from binascii import hexlify
17
18
from copy import deepcopy
18
19
from cStringIO import StringIO
 
20
import re
 
21
import time
19
22
from unittest import TestSuite
20
23
 
21
24
import bzrlib.bzrdir as bzrdir
25
28
import bzrlib.gpg as gpg
26
29
from bzrlib.graph import Graph
27
30
from bzrlib.inter import InterObject
 
31
from bzrlib.inventory import Inventory
28
32
from bzrlib.knit import KnitVersionedFile, KnitPlainFactory
29
33
from bzrlib.lockable_files import LockableFiles, TransportLock
30
34
from bzrlib.lockdir import LockDir
31
 
from bzrlib.osutils import safe_unicode
32
 
from bzrlib.revision import NULL_REVISION
 
35
from bzrlib.osutils import (safe_unicode, rand_bytes, compact_date, 
 
36
                            local_time_offset)
 
37
from bzrlib.revision import NULL_REVISION, Revision
33
38
from bzrlib.store.versioned import VersionedFileStore, WeaveStore
34
39
from bzrlib.store.text import TextStore
35
40
from bzrlib.symbol_versioning import *
36
41
from bzrlib.trace import mutter, note
37
 
from bzrlib.tree import RevisionTree
 
42
from bzrlib.tree import RevisionTree, EmptyTree
38
43
from bzrlib.tsort import topo_sort
39
44
from bzrlib.testament import Testament
40
45
from bzrlib.tree import EmptyTree
 
46
from bzrlib.delta import compare_trees
41
47
import bzrlib.ui
42
48
from bzrlib.weave import WeaveFile
43
49
import bzrlib.xml5
64
70
 
65
71
        returns the sha1 of the serialized inventory.
66
72
        """
 
73
        assert inv.revision_id is None or inv.revision_id == revid, \
 
74
            "Mismatch between inventory revision" \
 
75
            " id and insertion revid (%r, %r)" % (inv.revision_id, revid)
67
76
        inv_text = bzrlib.xml5.serializer_v5.write_inventory_to_string(inv)
68
77
        inv_sha1 = bzrlib.osutils.sha_string(inv_text)
69
78
        inv_vf = self.control_weaves.get_weave('inventory',
70
79
                                               self.get_transaction())
71
 
        inv_vf.add_lines(revid, parents, bzrlib.osutils.split_lines(inv_text))
 
80
        self._inventory_add_lines(inv_vf, revid, parents, bzrlib.osutils.split_lines(inv_text))
72
81
        return inv_sha1
73
82
 
 
83
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
84
        final_parents = []
 
85
        for parent in parents:
 
86
            if parent in inv_vf:
 
87
                final_parents.append(parent)
 
88
 
 
89
        inv_vf.add_lines(revid, final_parents, lines)
 
90
 
74
91
    @needs_write_lock
75
92
    def add_revision(self, rev_id, rev, inv=None, config=None):
76
93
        """Add rev to the revision store as rev_id.
103
120
        """Return all the possible revisions that we could find."""
104
121
        return self.get_inventory_weave().versions()
105
122
 
 
123
    @deprecated_method(zero_nine)
 
124
    def all_revision_ids(self):
 
125
        """Returns a list of all the revision ids in the repository. 
 
126
 
 
127
        This is deprecated because code should generally work on the graph
 
128
        reachable from a particular revision, and ignore any other revisions
 
129
        that might be present.  There is no direct replacement method.
 
130
        """
 
131
        return self._all_revision_ids()
 
132
 
106
133
    @needs_read_lock
107
 
    def all_revision_ids(self):
 
134
    def _all_revision_ids(self):
108
135
        """Returns a list of all the revision ids in the repository. 
109
136
 
110
137
        These are in as much topological order as the underlying store can 
158
185
        self.control_files = control_files
159
186
        self._revision_store = _revision_store
160
187
        self.text_store = text_store
161
 
        # backwards compatability
 
188
        # backwards compatibility
162
189
        self.weave_store = text_store
163
190
        # not right yet - should be more semantically clear ? 
164
191
        # 
219
246
        return InterRepository.get(source, self).fetch(revision_id=revision_id,
220
247
                                                       pb=pb)
221
248
 
 
249
    def get_commit_builder(self, branch, parents, config, timestamp=None, 
 
250
                           timezone=None, committer=None, revprops=None, 
 
251
                           revision_id=None):
 
252
        """Obtain a CommitBuilder for this repository.
 
253
        
 
254
        :param branch: Branch to commit to.
 
255
        :param parents: Revision ids of the parents of the new revision.
 
256
        :param config: Configuration to use.
 
257
        :param timestamp: Optional timestamp recorded for commit.
 
258
        :param timezone: Optional timezone for timestamp.
 
259
        :param committer: Optional committer to set for commit.
 
260
        :param revprops: Optional dictionary of revision properties.
 
261
        :param revision_id: Optional revision id.
 
262
        """
 
263
        return CommitBuilder(self, parents, config, timestamp, timezone,
 
264
                             committer, revprops, revision_id)
 
265
 
222
266
    def unlock(self):
223
267
        self.control_files.unlock()
224
268
 
260
304
        """
261
305
        if not revision_id or not isinstance(revision_id, basestring):
262
306
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
263
 
        return self._revision_store.get_revision(revision_id,
264
 
                                                 self.get_transaction())
 
307
        return self._revision_store.get_revisions([revision_id],
 
308
                                                  self.get_transaction())[0]
 
309
    @needs_read_lock
 
310
    def get_revisions(self, revision_ids):
 
311
        return self._revision_store.get_revisions(revision_ids,
 
312
                                                  self.get_transaction())
265
313
 
266
314
    @needs_read_lock
267
315
    def get_revision_xml(self, revision_id):
287
335
        self._check_revision_parents(r, inv)
288
336
        return r
289
337
 
 
338
    def get_revision_delta(self, revision_id):
 
339
        """Return the delta for one revision.
 
340
 
 
341
        The delta is relative to the left-hand predecessor of the
 
342
        revision.
 
343
        """
 
344
        revision = self.get_revision(revision_id)
 
345
        new_tree = self.revision_tree(revision_id)
 
346
        if not revision.parent_ids:
 
347
            old_tree = EmptyTree()
 
348
        else:
 
349
            old_tree = self.revision_tree(revision.parent_ids[0])
 
350
        return compare_trees(old_tree, new_tree)
 
351
 
290
352
    def _check_revision_parents(self, revision, inventory):
291
353
        """Private to Repository and Fetch.
292
354
        
322
384
                                         RepositoryFormat6,
323
385
                                         RepositoryFormat7,
324
386
                                         RepositoryFormatKnit1)), \
325
 
            "fileid_involved only supported for branches which store inventory as unnested xml"
 
387
            ("fileids_altered_by_revision_ids only supported for branches " 
 
388
             "which store inventory as unnested xml, not on %r" % self)
326
389
        selected_revision_ids = set(revision_ids)
327
390
        w = self.get_inventory_weave()
328
391
        result = {}
329
392
 
330
393
        # this code needs to read every new line in every inventory for the
331
394
        # inventories [revision_ids]. Seeing a line twice is ok. Seeing a line
332
 
        # not pesent in one of those inventories is unnecessary but not 
 
395
        # not present in one of those inventories is unnecessary but not 
333
396
        # harmful because we are filtering by the revision id marker in the
334
397
        # inventory lines : we only select file ids altered in one of those  
335
 
        # revisions. We dont need to see all lines in the inventory because
 
398
        # revisions. We don't need to see all lines in the inventory because
336
399
        # only those added in an inventory in rev X can contain a revision=X
337
400
        # line.
338
401
        for line in w.iter_lines_added_or_present_in_versions(selected_revision_ids):
359
422
    @needs_read_lock
360
423
    def get_inventory(self, revision_id):
361
424
        """Get Inventory object by hash."""
362
 
        xml = self.get_inventory_xml(revision_id)
 
425
        return self.deserialise_inventory(
 
426
            revision_id, self.get_inventory_xml(revision_id))
 
427
 
 
428
    def deserialise_inventory(self, revision_id, xml):
 
429
        """Transform the xml into an inventory object. 
 
430
 
 
431
        :param revision_id: The expected revision id of the inventory.
 
432
        :param xml: A serialised inventory.
 
433
        """
363
434
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
364
435
 
365
436
    @needs_read_lock
485
556
    @needs_read_lock
486
557
    def get_ancestry(self, revision_id):
487
558
        """Return a list of revision-ids integrated by a revision.
 
559
 
 
560
        The first element of the list is always None, indicating the origin 
 
561
        revision.  This might change when we have history horizons, or 
 
562
        perhaps we should have a new API.
488
563
        
489
564
        This is topologically sorted.
490
565
        """
508
583
        # use inventory as it was in that revision
509
584
        file_id = tree.inventory.path2id(file)
510
585
        if not file_id:
511
 
            raise BzrError("%r is not present in revision %s" % (file, revno))
512
 
            try:
513
 
                revno = self.revision_id_to_revno(revision_id)
514
 
            except errors.NoSuchRevision:
515
 
                # TODO: This should not be BzrError,
516
 
                # but NoSuchFile doesn't fit either
517
 
                raise BzrError('%r is not present in revision %s' 
518
 
                                % (file, revision_id))
519
 
            else:
520
 
                raise BzrError('%r is not present in revision %s'
521
 
                                % (file, revno))
 
586
            # TODO: jam 20060427 Write a test for this code path
 
587
            #       it had a bug in it, and was raising the wrong
 
588
            #       exception.
 
589
            raise errors.BzrError("%r is not present in revision %s" % (file, revision_id))
522
590
        tree.print_file(file_id)
523
591
 
524
592
    def get_transaction(self):
560
628
        return self._revision_store.get_signature_text(revision_id,
561
629
                                                       self.get_transaction())
562
630
 
 
631
    @needs_read_lock
 
632
    def check(self, revision_ids):
 
633
        """Check consistency of all history of given revision_ids.
 
634
 
 
635
        Different repository implementations should override _check().
 
636
 
 
637
        :param revision_ids: A non-empty list of revision_ids whose ancestry
 
638
             will be checked.  Typically the last revision_id of a branch.
 
639
        """
 
640
        if not revision_ids:
 
641
            raise ValueError("revision_ids must be non-empty in %s.check" 
 
642
                    % (self,))
 
643
        return self._check(revision_ids)
 
644
 
 
645
    def _check(self, revision_ids):
 
646
        result = bzrlib.check.Check(self)
 
647
        result.check()
 
648
        return result
 
649
 
563
650
 
564
651
class AllInOneRepository(Repository):
565
652
    """Legacy support - the repository behaviour for all-in-one branches."""
635
722
                repository.get_transaction())
636
723
        if ie.revision not in w:
637
724
            text_parents = []
 
725
            # FIXME: TODO: The following loop *may* be overlapping/duplicate
 
726
            # with InventoryEntry.find_previous_heads(). if it is, then there
 
727
            # is a latent bug here where the parents may have ancestors of each
 
728
            # other. RBC, AB
638
729
            for revision, tree in parent_trees.iteritems():
639
730
                if ie.file_id not in tree:
640
731
                    continue
700
791
class KnitRepository(MetaDirRepository):
701
792
    """Knit format repository."""
702
793
 
 
794
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
795
        inv_vf.add_lines_with_ghosts(revid, parents, lines)
 
796
 
703
797
    @needs_read_lock
704
 
    def all_revision_ids(self):
 
798
    def _all_revision_ids(self):
705
799
        """See Repository.all_revision_ids()."""
 
800
        # Knits get the revision graph from the index of the revision knit, so
 
801
        # it's always possible even if they're on an unlistable transport.
706
802
        return self._revision_store.all_revision_ids(self.get_transaction())
707
803
 
708
804
    def fileid_involved_between_revs(self, from_revid, to_revid):
797
893
                    raise errors.NoSuchRevision(self, revision_id)
798
894
                # a ghost
799
895
                result.add_ghost(revision_id)
800
 
                # mark it as done so we dont try for it again.
 
896
                # mark it as done so we don't try for it again.
801
897
                done.add(revision_id)
802
898
                continue
803
899
            parent_ids = vf.get_parents_with_ghosts(revision_id)
887
983
        raise NotImplementedError(self.get_format_string)
888
984
 
889
985
    def get_format_description(self):
890
 
        """Return the short desciption for this format."""
 
986
        """Return the short description for this format."""
891
987
        raise NotImplementedError(self.get_format_description)
892
988
 
893
989
    def _get_revision_store(self, repo_transport, control_files):
1002
1098
        files = [('inventory.weave', StringIO(empty_weave)),
1003
1099
                 ]
1004
1100
        
1005
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1101
        # FIXME: RBC 20060125 don't peek under the covers
1006
1102
        # NB: no need to escape relative paths that are url safe.
1007
1103
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock',
1008
1104
                                      TransportLock)
1054
1150
     - TextStores for texts, inventories,revisions.
1055
1151
 
1056
1152
    This format is deprecated: it indexes texts using a text id which is
1057
 
    removed in format 5; initializationa and write support for this format
 
1153
    removed in format 5; initialization and write support for this format
1058
1154
    has been removed.
1059
1155
    """
1060
1156
 
1159
1255
 
1160
1256
 
1161
1257
class MetaDirRepositoryFormat(RepositoryFormat):
1162
 
    """Common base class for the new repositories using the metadir layour."""
 
1258
    """Common base class for the new repositories using the metadir layout."""
1163
1259
 
1164
1260
    def __init__(self):
1165
1261
        super(MetaDirRepositoryFormat, self).__init__()
1167
1263
 
1168
1264
    def _create_control_files(self, a_bzrdir):
1169
1265
        """Create the required files and the initial control_files object."""
1170
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1266
        # FIXME: RBC 20060125 don't peek under the covers
1171
1267
        # NB: no need to escape relative paths that are url safe.
1172
1268
        repository_transport = a_bzrdir.get_repository_transport(self)
1173
1269
        control_files = LockableFiles(repository_transport, 'lock', LockDir)
1433
1529
        # grab the basis available data
1434
1530
        if basis is not None:
1435
1531
            self.target.fetch(basis, revision_id=revision_id)
1436
 
        # but dont bother fetching if we have the needed data now.
 
1532
        # but don't bother fetching if we have the needed data now.
1437
1533
        if (revision_id not in (None, NULL_REVISION) and 
1438
1534
            self.target.has_revision(revision_id)):
1439
1535
            return
1530
1626
    def is_compatible(source, target):
1531
1627
        """Be compatible with known Weave formats.
1532
1628
        
1533
 
        We dont test for the stores being of specific types becase that
 
1629
        We don't test for the stores being of specific types because that
1534
1630
        could lead to confusing results, and there is no need to be 
1535
1631
        overly general.
1536
1632
        """
1551
1647
        if basis is not None:
1552
1648
            # copy the basis in, then fetch remaining data.
1553
1649
            basis.copy_content_into(self.target, revision_id)
1554
 
            # the basis copy_content_into could misset this.
 
1650
            # the basis copy_content_into could miss-set this.
1555
1651
            try:
1556
1652
                self.target.set_make_working_trees(self.source.make_working_trees())
1557
1653
            except NotImplementedError:
1600
1696
    def missing_revision_ids(self, revision_id=None):
1601
1697
        """See InterRepository.missing_revision_ids()."""
1602
1698
        # we want all revisions to satisfy revision_id in source.
1603
 
        # but we dont want to stat every file here and there.
 
1699
        # but we don't want to stat every file here and there.
1604
1700
        # we want then, all revisions other needs to satisfy revision_id 
1605
1701
        # checked, but not those that we have locally.
1606
1702
        # so the first thing is to get a subset of the revisions to 
1619
1715
        source_ids_set = set(source_ids)
1620
1716
        # source_ids is the worst possible case we may need to pull.
1621
1717
        # now we want to filter source_ids against what we actually
1622
 
        # have in target, but dont try to check for existence where we know
 
1718
        # have in target, but don't try to check for existence where we know
1623
1719
        # we do not have a revision as that would be pointless.
1624
1720
        target_ids = set(self.target._all_possible_ids())
1625
1721
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1648
1744
    def is_compatible(source, target):
1649
1745
        """Be compatible with known Knit formats.
1650
1746
        
1651
 
        We dont test for the stores being of specific types becase that
 
1747
        We don't test for the stores being of specific types because that
1652
1748
        could lead to confusing results, and there is no need to be 
1653
1749
        overly general.
1654
1750
        """
1682
1778
        source_ids_set = set(source_ids)
1683
1779
        # source_ids is the worst possible case we may need to pull.
1684
1780
        # now we want to filter source_ids against what we actually
1685
 
        # have in target, but dont try to check for existence where we know
 
1781
        # have in target, but don't try to check for existence where we know
1686
1782
        # we do not have a revision as that would be pointless.
1687
1783
        target_ids = set(self.target._all_possible_ids())
1688
1784
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1836
1932
        self.pb.update(message, self.count, self.total)
1837
1933
 
1838
1934
 
 
1935
class CommitBuilder(object):
 
1936
    """Provides an interface to build up a commit.
 
1937
 
 
1938
    This allows describing a tree to be committed without needing to 
 
1939
    know the internals of the format of the repository.
 
1940
    """
 
1941
    def __init__(self, repository, parents, config, timestamp=None, 
 
1942
                 timezone=None, committer=None, revprops=None, 
 
1943
                 revision_id=None):
 
1944
        """Initiate a CommitBuilder.
 
1945
 
 
1946
        :param repository: Repository to commit to.
 
1947
        :param parents: Revision ids of the parents of the new revision.
 
1948
        :param config: Configuration to use.
 
1949
        :param timestamp: Optional timestamp recorded for commit.
 
1950
        :param timezone: Optional timezone for timestamp.
 
1951
        :param committer: Optional committer to set for commit.
 
1952
        :param revprops: Optional dictionary of revision properties.
 
1953
        :param revision_id: Optional revision id.
 
1954
        """
 
1955
        self._config = config
 
1956
 
 
1957
        if committer is None:
 
1958
            self._committer = self._config.username()
 
1959
        else:
 
1960
            assert isinstance(committer, basestring), type(committer)
 
1961
            self._committer = committer
 
1962
 
 
1963
        self.new_inventory = Inventory()
 
1964
        self._new_revision_id = revision_id
 
1965
        self.parents = parents
 
1966
        self.repository = repository
 
1967
 
 
1968
        self._revprops = {}
 
1969
        if revprops is not None:
 
1970
            self._revprops.update(revprops)
 
1971
 
 
1972
        if timestamp is None:
 
1973
            self._timestamp = time.time()
 
1974
        else:
 
1975
            self._timestamp = long(timestamp)
 
1976
 
 
1977
        if timezone is None:
 
1978
            self._timezone = local_time_offset()
 
1979
        else:
 
1980
            self._timezone = int(timezone)
 
1981
 
 
1982
        self._generate_revision_if_needed()
 
1983
 
 
1984
    def commit(self, message):
 
1985
        """Make the actual commit.
 
1986
 
 
1987
        :return: The revision id of the recorded revision.
 
1988
        """
 
1989
        rev = Revision(timestamp=self._timestamp,
 
1990
                       timezone=self._timezone,
 
1991
                       committer=self._committer,
 
1992
                       message=message,
 
1993
                       inventory_sha1=self.inv_sha1,
 
1994
                       revision_id=self._new_revision_id,
 
1995
                       properties=self._revprops)
 
1996
        rev.parent_ids = self.parents
 
1997
        self.repository.add_revision(self._new_revision_id, rev, 
 
1998
            self.new_inventory, self._config)
 
1999
        return self._new_revision_id
 
2000
 
 
2001
    def finish_inventory(self):
 
2002
        """Tell the builder that the inventory is finished."""
 
2003
        self.new_inventory.revision_id = self._new_revision_id
 
2004
        self.inv_sha1 = self.repository.add_inventory(
 
2005
            self._new_revision_id,
 
2006
            self.new_inventory,
 
2007
            self.parents
 
2008
            )
 
2009
 
 
2010
    def _gen_revision_id(self):
 
2011
        """Return new revision-id."""
 
2012
        s = '%s-%s-' % (self._config.user_email(), 
 
2013
                        compact_date(self._timestamp))
 
2014
        s += hexlify(rand_bytes(8))
 
2015
        return s
 
2016
 
 
2017
    def _generate_revision_if_needed(self):
 
2018
        """Create a revision id if None was supplied.
 
2019
        
 
2020
        If the repository can not support user-specified revision ids
 
2021
        they should override this function and raise UnsupportedOperation
 
2022
        if _new_revision_id is not None.
 
2023
 
 
2024
        :raises: UnsupportedOperation
 
2025
        """
 
2026
        if self._new_revision_id is None:
 
2027
            self._new_revision_id = self._gen_revision_id()
 
2028
 
 
2029
    def record_entry_contents(self, ie, parent_invs, path, tree):
 
2030
        """Record the content of ie from tree into the commit if needed.
 
2031
 
 
2032
        :param ie: An inventory entry present in the commit.
 
2033
        :param parent_invs: The inventories of the parent revisions of the
 
2034
            commit.
 
2035
        :param path: The path the entry is at in the tree.
 
2036
        :param tree: The tree which contains this entry and should be used to 
 
2037
        obtain content.
 
2038
        """
 
2039
        self.new_inventory.add(ie)
 
2040
 
 
2041
        # ie.revision is always None if the InventoryEntry is considered
 
2042
        # for committing. ie.snapshot will record the correct revision 
 
2043
        # which may be the sole parent if it is untouched.
 
2044
        if ie.revision is not None:
 
2045
            return
 
2046
        previous_entries = ie.find_previous_heads(
 
2047
            parent_invs,
 
2048
            self.repository.weave_store,
 
2049
            self.repository.get_transaction())
 
2050
        # we are creating a new revision for ie in the history store
 
2051
        # and inventory.
 
2052
        ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
 
2053
 
 
2054
    def modified_directory(self, file_id, file_parents):
 
2055
        """Record the presence of a symbolic link.
 
2056
 
 
2057
        :param file_id: The file_id of the link to record.
 
2058
        :param file_parents: The per-file parent revision ids.
 
2059
        """
 
2060
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2061
    
 
2062
    def modified_file_text(self, file_id, file_parents,
 
2063
                           get_content_byte_lines, text_sha1=None,
 
2064
                           text_size=None):
 
2065
        """Record the text of file file_id
 
2066
 
 
2067
        :param file_id: The file_id of the file to record the text of.
 
2068
        :param file_parents: The per-file parent revision ids.
 
2069
        :param get_content_byte_lines: A callable which will return the byte
 
2070
            lines for the file.
 
2071
        :param text_sha1: Optional SHA1 of the file contents.
 
2072
        :param text_size: Optional size of the file contents.
 
2073
        """
 
2074
        mutter('storing text of file {%s} in revision {%s} into %r',
 
2075
               file_id, self._new_revision_id, self.repository.weave_store)
 
2076
        # special case to avoid diffing on renames or 
 
2077
        # reparenting
 
2078
        if (len(file_parents) == 1
 
2079
            and text_sha1 == file_parents.values()[0].text_sha1
 
2080
            and text_size == file_parents.values()[0].text_size):
 
2081
            previous_ie = file_parents.values()[0]
 
2082
            versionedfile = self.repository.weave_store.get_weave(file_id, 
 
2083
                self.repository.get_transaction())
 
2084
            versionedfile.clone_text(self._new_revision_id, 
 
2085
                previous_ie.revision, file_parents.keys())
 
2086
            return text_sha1, text_size
 
2087
        else:
 
2088
            new_lines = get_content_byte_lines()
 
2089
            # TODO: Rather than invoking sha_strings here, _add_text_to_weave
 
2090
            # should return the SHA1 and size
 
2091
            self._add_text_to_weave(file_id, new_lines, file_parents.keys())
 
2092
            return bzrlib.osutils.sha_strings(new_lines), \
 
2093
                sum(map(len, new_lines))
 
2094
 
 
2095
    def modified_link(self, file_id, file_parents, link_target):
 
2096
        """Record the presence of a symbolic link.
 
2097
 
 
2098
        :param file_id: The file_id of the link to record.
 
2099
        :param file_parents: The per-file parent revision ids.
 
2100
        :param link_target: Target location of this link.
 
2101
        """
 
2102
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2103
 
 
2104
    def _add_text_to_weave(self, file_id, new_lines, parents):
 
2105
        versionedfile = self.repository.weave_store.get_weave_or_empty(
 
2106
            file_id, self.repository.get_transaction())
 
2107
        versionedfile.add_lines(self._new_revision_id, parents, new_lines)
 
2108
        versionedfile.clear_cache()
 
2109
 
 
2110
 
1839
2111
# Copied from xml.sax.saxutils
1840
2112
def _unescape_xml(data):
1841
2113
    """Unescape &, <, and > in a string of data.