~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Jelmer Vernooij
  • Date: 2006-06-13 13:24:40 UTC
  • mfrom: (1767 +trunk)
  • mto: (1769.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 1770.
  • Revision ID: jelmer@samba.org-20060613132440-24e222a86f948f60
[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 *
64
69
 
65
70
        returns the sha1 of the serialized inventory.
66
71
        """
 
72
        assert inv.revision_id is None or inv.revision_id == revid, \
 
73
            "Mismatch between inventory revision" \
 
74
            " id and insertion revid (%r, %r)" % (inv.revision_id, revid)
67
75
        inv_text = bzrlib.xml5.serializer_v5.write_inventory_to_string(inv)
68
76
        inv_sha1 = bzrlib.osutils.sha_string(inv_text)
69
77
        inv_vf = self.control_weaves.get_weave('inventory',
70
78
                                               self.get_transaction())
71
 
        inv_vf.add_lines(revid, parents, bzrlib.osutils.split_lines(inv_text))
 
79
        self._inventory_add_lines(inv_vf, revid, parents, bzrlib.osutils.split_lines(inv_text))
72
80
        return inv_sha1
73
81
 
 
82
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
83
        final_parents = []
 
84
        for parent in parents:
 
85
            if parent in inv_vf:
 
86
                final_parents.append(parent)
 
87
 
 
88
        inv_vf.add_lines(revid, final_parents, lines)
 
89
 
74
90
    @needs_write_lock
75
91
    def add_revision(self, rev_id, rev, inv=None, config=None):
76
92
        """Add rev to the revision store as rev_id.
103
119
        """Return all the possible revisions that we could find."""
104
120
        return self.get_inventory_weave().versions()
105
121
 
 
122
    @deprecated_method(zero_nine)
 
123
    def all_revision_ids(self):
 
124
        """Returns a list of all the revision ids in the repository. 
 
125
 
 
126
        This is deprecated because code should generally work on the graph
 
127
        reachable from a particular revision, and ignore any other revisions
 
128
        that might be present.  There is no direct replacement method.
 
129
        """
 
130
        return self._all_revision_ids()
 
131
 
106
132
    @needs_read_lock
107
 
    def all_revision_ids(self):
 
133
    def _all_revision_ids(self):
108
134
        """Returns a list of all the revision ids in the repository. 
109
135
 
110
136
        These are in as much topological order as the underlying store can 
158
184
        self.control_files = control_files
159
185
        self._revision_store = _revision_store
160
186
        self.text_store = text_store
161
 
        # backwards compatability
 
187
        # backwards compatibility
162
188
        self.weave_store = text_store
163
189
        # not right yet - should be more semantically clear ? 
164
190
        # 
219
245
        return InterRepository.get(source, self).fetch(revision_id=revision_id,
220
246
                                                       pb=pb)
221
247
 
 
248
    def get_commit_builder(self, branch, parents, config, timestamp=None, 
 
249
                           timezone=None, committer=None, revprops=None, 
 
250
                           revision_id=None):
 
251
        """Obtain a CommitBuilder for this repository.
 
252
        
 
253
        :param branch: Branch to commit to.
 
254
        :param parents: Revision ids of the parents of the new revision.
 
255
        :param config: Configuration to use.
 
256
        :param timestamp: Optional timestamp recorded for commit.
 
257
        :param timezone: Optional timezone for timestamp.
 
258
        :param committer: Optional committer to set for commit.
 
259
        :param revprops: Optional dictionary of revision properties.
 
260
        :param revision_id: Optional revision id.
 
261
        """
 
262
        return CommitBuilder(self, parents, config, timestamp, timezone,
 
263
                             committer, revprops, revision_id)
 
264
 
222
265
    def unlock(self):
223
266
        self.control_files.unlock()
224
267
 
322
365
                                         RepositoryFormat6,
323
366
                                         RepositoryFormat7,
324
367
                                         RepositoryFormatKnit1)), \
325
 
            "fileid_involved only supported for branches which store inventory as unnested xml"
 
368
            ("fileids_altered_by_revision_ids only supported for branches " 
 
369
             "which store inventory as unnested xml, not on %r" % self)
326
370
        selected_revision_ids = set(revision_ids)
327
371
        w = self.get_inventory_weave()
328
372
        result = {}
329
373
 
330
374
        # this code needs to read every new line in every inventory for the
331
375
        # inventories [revision_ids]. Seeing a line twice is ok. Seeing a line
332
 
        # not pesent in one of those inventories is unnecessary but not 
 
376
        # not present in one of those inventories is unnecessary but not 
333
377
        # harmful because we are filtering by the revision id marker in the
334
378
        # 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
 
379
        # revisions. We don't need to see all lines in the inventory because
336
380
        # only those added in an inventory in rev X can contain a revision=X
337
381
        # line.
338
382
        for line in w.iter_lines_added_or_present_in_versions(selected_revision_ids):
359
403
    @needs_read_lock
360
404
    def get_inventory(self, revision_id):
361
405
        """Get Inventory object by hash."""
362
 
        xml = self.get_inventory_xml(revision_id)
 
406
        return self.deserialise_inventory(
 
407
            revision_id, self.get_inventory_xml(revision_id))
 
408
 
 
409
    def deserialise_inventory(self, revision_id, xml):
 
410
        """Transform the xml into an inventory object. 
 
411
 
 
412
        :param revision_id: The expected revision id of the inventory.
 
413
        :param xml: A serialised inventory.
 
414
        """
363
415
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
364
416
 
365
417
    @needs_read_lock
485
537
    @needs_read_lock
486
538
    def get_ancestry(self, revision_id):
487
539
        """Return a list of revision-ids integrated by a revision.
 
540
 
 
541
        The first element of the list is always None, indicating the origin 
 
542
        revision.  This might change when we have history horizons, or 
 
543
        perhaps we should have a new API.
488
544
        
489
545
        This is topologically sorted.
490
546
        """
508
564
        # use inventory as it was in that revision
509
565
        file_id = tree.inventory.path2id(file)
510
566
        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))
 
567
            # TODO: jam 20060427 Write a test for this code path
 
568
            #       it had a bug in it, and was raising the wrong
 
569
            #       exception.
 
570
            raise errors.BzrError("%r is not present in revision %s" % (file, revision_id))
522
571
        tree.print_file(file_id)
523
572
 
524
573
    def get_transaction(self):
560
609
        return self._revision_store.get_signature_text(revision_id,
561
610
                                                       self.get_transaction())
562
611
 
 
612
    @needs_read_lock
 
613
    def check(self, revision_ids):
 
614
        """Check consistency of all history of given revision_ids.
 
615
 
 
616
        Different repository implementations should override _check().
 
617
 
 
618
        :param revision_ids: A non-empty list of revision_ids whose ancestry
 
619
             will be checked.  Typically the last revision_id of a branch.
 
620
        """
 
621
        if not revision_ids:
 
622
            raise ValueError("revision_ids must be non-empty in %s.check" 
 
623
                    % (self,))
 
624
        return self._check(revision_ids)
 
625
 
 
626
    def _check(self, revision_ids):
 
627
        result = bzrlib.check.Check(self)
 
628
        result.check()
 
629
        return result
 
630
 
563
631
 
564
632
class AllInOneRepository(Repository):
565
633
    """Legacy support - the repository behaviour for all-in-one branches."""
616
684
        return True
617
685
 
618
686
 
 
687
def install_revision(repository, rev, revision_tree):
 
688
    """Install all revision data into a repository."""
 
689
    present_parents = []
 
690
    parent_trees = {}
 
691
    for p_id in rev.parent_ids:
 
692
        if repository.has_revision(p_id):
 
693
            present_parents.append(p_id)
 
694
            parent_trees[p_id] = repository.revision_tree(p_id)
 
695
        else:
 
696
            parent_trees[p_id] = EmptyTree()
 
697
 
 
698
    inv = revision_tree.inventory
 
699
    
 
700
    # Add the texts that are not already present
 
701
    for path, ie in inv.iter_entries():
 
702
        w = repository.weave_store.get_weave_or_empty(ie.file_id,
 
703
                repository.get_transaction())
 
704
        if ie.revision not in w:
 
705
            text_parents = []
 
706
            # FIXME: TODO: The following loop *may* be overlapping/duplicate
 
707
            # with InventoryEntry.find_previous_heads(). if it is, then there
 
708
            # is a latent bug here where the parents may have ancestors of each
 
709
            # other. RBC, AB
 
710
            for revision, tree in parent_trees.iteritems():
 
711
                if ie.file_id not in tree:
 
712
                    continue
 
713
                parent_id = tree.inventory[ie.file_id].revision
 
714
                if parent_id in text_parents:
 
715
                    continue
 
716
                text_parents.append(parent_id)
 
717
                    
 
718
            vfile = repository.weave_store.get_weave_or_empty(ie.file_id, 
 
719
                repository.get_transaction())
 
720
            lines = revision_tree.get_file(ie.file_id).readlines()
 
721
            vfile.add_lines(rev.revision_id, text_parents, lines)
 
722
    try:
 
723
        # install the inventory
 
724
        repository.add_inventory(rev.revision_id, inv, present_parents)
 
725
    except errors.RevisionAlreadyPresent:
 
726
        pass
 
727
    repository.add_revision(rev.revision_id, rev, inv)
 
728
 
 
729
 
619
730
class MetaDirRepository(Repository):
620
731
    """Repositories in the new meta-dir layout."""
621
732
 
661
772
class KnitRepository(MetaDirRepository):
662
773
    """Knit format repository."""
663
774
 
 
775
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
776
        inv_vf.add_lines_with_ghosts(revid, parents, lines)
 
777
 
664
778
    @needs_read_lock
665
 
    def all_revision_ids(self):
 
779
    def _all_revision_ids(self):
666
780
        """See Repository.all_revision_ids()."""
 
781
        # Knits get the revision graph from the index of the revision knit, so
 
782
        # it's always possible even if they're on an unlistable transport.
667
783
        return self._revision_store.all_revision_ids(self.get_transaction())
668
784
 
669
785
    def fileid_involved_between_revs(self, from_revid, to_revid):
758
874
                    raise errors.NoSuchRevision(self, revision_id)
759
875
                # a ghost
760
876
                result.add_ghost(revision_id)
761
 
                # mark it as done so we dont try for it again.
 
877
                # mark it as done so we don't try for it again.
762
878
                done.add(revision_id)
763
879
                continue
764
880
            parent_ids = vf.get_parents_with_ghosts(revision_id)
848
964
        raise NotImplementedError(self.get_format_string)
849
965
 
850
966
    def get_format_description(self):
851
 
        """Return the short desciption for this format."""
 
967
        """Return the short description for this format."""
852
968
        raise NotImplementedError(self.get_format_description)
853
969
 
854
970
    def _get_revision_store(self, repo_transport, control_files):
963
1079
        files = [('inventory.weave', StringIO(empty_weave)),
964
1080
                 ]
965
1081
        
966
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1082
        # FIXME: RBC 20060125 don't peek under the covers
967
1083
        # NB: no need to escape relative paths that are url safe.
968
1084
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock',
969
1085
                                      TransportLock)
1015
1131
     - TextStores for texts, inventories,revisions.
1016
1132
 
1017
1133
    This format is deprecated: it indexes texts using a text id which is
1018
 
    removed in format 5; initializationa and write support for this format
 
1134
    removed in format 5; initialization and write support for this format
1019
1135
    has been removed.
1020
1136
    """
1021
1137
 
1120
1236
 
1121
1237
 
1122
1238
class MetaDirRepositoryFormat(RepositoryFormat):
1123
 
    """Common base class for the new repositories using the metadir layour."""
 
1239
    """Common base class for the new repositories using the metadir layout."""
1124
1240
 
1125
1241
    def __init__(self):
1126
1242
        super(MetaDirRepositoryFormat, self).__init__()
1128
1244
 
1129
1245
    def _create_control_files(self, a_bzrdir):
1130
1246
        """Create the required files and the initial control_files object."""
1131
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1247
        # FIXME: RBC 20060125 don't peek under the covers
1132
1248
        # NB: no need to escape relative paths that are url safe.
1133
1249
        repository_transport = a_bzrdir.get_repository_transport(self)
1134
1250
        control_files = LockableFiles(repository_transport, 'lock', LockDir)
1394
1510
        # grab the basis available data
1395
1511
        if basis is not None:
1396
1512
            self.target.fetch(basis, revision_id=revision_id)
1397
 
        # but dont bother fetching if we have the needed data now.
 
1513
        # but don't bother fetching if we have the needed data now.
1398
1514
        if (revision_id not in (None, NULL_REVISION) and 
1399
1515
            self.target.has_revision(revision_id)):
1400
1516
            return
1491
1607
    def is_compatible(source, target):
1492
1608
        """Be compatible with known Weave formats.
1493
1609
        
1494
 
        We dont test for the stores being of specific types becase that
 
1610
        We don't test for the stores being of specific types because that
1495
1611
        could lead to confusing results, and there is no need to be 
1496
1612
        overly general.
1497
1613
        """
1512
1628
        if basis is not None:
1513
1629
            # copy the basis in, then fetch remaining data.
1514
1630
            basis.copy_content_into(self.target, revision_id)
1515
 
            # the basis copy_content_into could misset this.
 
1631
            # the basis copy_content_into could miss-set this.
1516
1632
            try:
1517
1633
                self.target.set_make_working_trees(self.source.make_working_trees())
1518
1634
            except NotImplementedError:
1561
1677
    def missing_revision_ids(self, revision_id=None):
1562
1678
        """See InterRepository.missing_revision_ids()."""
1563
1679
        # we want all revisions to satisfy revision_id in source.
1564
 
        # but we dont want to stat every file here and there.
 
1680
        # but we don't want to stat every file here and there.
1565
1681
        # we want then, all revisions other needs to satisfy revision_id 
1566
1682
        # checked, but not those that we have locally.
1567
1683
        # so the first thing is to get a subset of the revisions to 
1580
1696
        source_ids_set = set(source_ids)
1581
1697
        # source_ids is the worst possible case we may need to pull.
1582
1698
        # now we want to filter source_ids against what we actually
1583
 
        # have in target, but dont try to check for existence where we know
 
1699
        # have in target, but don't try to check for existence where we know
1584
1700
        # we do not have a revision as that would be pointless.
1585
1701
        target_ids = set(self.target._all_possible_ids())
1586
1702
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1609
1725
    def is_compatible(source, target):
1610
1726
        """Be compatible with known Knit formats.
1611
1727
        
1612
 
        We dont test for the stores being of specific types becase that
 
1728
        We don't test for the stores being of specific types because that
1613
1729
        could lead to confusing results, and there is no need to be 
1614
1730
        overly general.
1615
1731
        """
1643
1759
        source_ids_set = set(source_ids)
1644
1760
        # source_ids is the worst possible case we may need to pull.
1645
1761
        # now we want to filter source_ids against what we actually
1646
 
        # have in target, but dont try to check for existence where we know
 
1762
        # have in target, but don't try to check for existence where we know
1647
1763
        # we do not have a revision as that would be pointless.
1648
1764
        target_ids = set(self.target._all_possible_ids())
1649
1765
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1797
1913
        self.pb.update(message, self.count, self.total)
1798
1914
 
1799
1915
 
 
1916
class CommitBuilder(object):
 
1917
    """Provides an interface to build up a commit.
 
1918
 
 
1919
    This allows describing a tree to be committed without needing to 
 
1920
    know the internals of the format of the repository.
 
1921
    """
 
1922
    def __init__(self, repository, parents, config, timestamp=None, 
 
1923
                 timezone=None, committer=None, revprops=None, 
 
1924
                 revision_id=None):
 
1925
        """Initiate a CommitBuilder.
 
1926
 
 
1927
        :param repository: Repository to commit to.
 
1928
        :param parents: Revision ids of the parents of the new revision.
 
1929
        :param config: Configuration to use.
 
1930
        :param timestamp: Optional timestamp recorded for commit.
 
1931
        :param timezone: Optional timezone for timestamp.
 
1932
        :param committer: Optional committer to set for commit.
 
1933
        :param revprops: Optional dictionary of revision properties.
 
1934
        :param revision_id: Optional revision id.
 
1935
        """
 
1936
        self._config = config
 
1937
 
 
1938
        if committer is None:
 
1939
            self._committer = self._config.username()
 
1940
        else:
 
1941
            assert isinstance(committer, basestring), type(committer)
 
1942
            self._committer = committer
 
1943
 
 
1944
        self.new_inventory = Inventory()
 
1945
        self._new_revision_id = revision_id
 
1946
        self.parents = parents
 
1947
        self.repository = repository
 
1948
 
 
1949
        self._revprops = {}
 
1950
        if revprops is not None:
 
1951
            self._revprops.update(revprops)
 
1952
 
 
1953
        if timestamp is None:
 
1954
            self._timestamp = time.time()
 
1955
        else:
 
1956
            self._timestamp = long(timestamp)
 
1957
 
 
1958
        if timezone is None:
 
1959
            self._timezone = local_time_offset()
 
1960
        else:
 
1961
            self._timezone = int(timezone)
 
1962
 
 
1963
        self._generate_revision_if_needed()
 
1964
 
 
1965
    def commit(self, message):
 
1966
        """Make the actual commit.
 
1967
 
 
1968
        :return: The revision id of the recorded revision.
 
1969
        """
 
1970
        rev = Revision(timestamp=self._timestamp,
 
1971
                       timezone=self._timezone,
 
1972
                       committer=self._committer,
 
1973
                       message=message,
 
1974
                       inventory_sha1=self.inv_sha1,
 
1975
                       revision_id=self._new_revision_id,
 
1976
                       properties=self._revprops)
 
1977
        rev.parent_ids = self.parents
 
1978
        self.repository.add_revision(self._new_revision_id, rev, 
 
1979
            self.new_inventory, self._config)
 
1980
        return self._new_revision_id
 
1981
 
 
1982
    def finish_inventory(self):
 
1983
        """Tell the builder that the inventory is finished."""
 
1984
        self.new_inventory.revision_id = self._new_revision_id
 
1985
        self.inv_sha1 = self.repository.add_inventory(
 
1986
            self._new_revision_id,
 
1987
            self.new_inventory,
 
1988
            self.parents
 
1989
            )
 
1990
 
 
1991
    def _gen_revision_id(self):
 
1992
        """Return new revision-id."""
 
1993
        s = '%s-%s-' % (self._config.user_email(), 
 
1994
                        compact_date(self._timestamp))
 
1995
        s += hexlify(rand_bytes(8))
 
1996
        return s
 
1997
 
 
1998
    def _generate_revision_if_needed(self):
 
1999
        """Create a revision id if None was supplied.
 
2000
        
 
2001
        If the repository can not support user-specified revision ids
 
2002
        they should override this function and raise UnsupportedOperation
 
2003
        if _new_revision_id is not None.
 
2004
 
 
2005
        :raises: UnsupportedOperation
 
2006
        """
 
2007
        if self._new_revision_id is None:
 
2008
            self._new_revision_id = self._gen_revision_id()
 
2009
 
 
2010
    def record_entry_contents(self, ie, parent_invs, path, tree):
 
2011
        """Record the content of ie from tree into the commit if needed.
 
2012
 
 
2013
        :param ie: An inventory entry present in the commit.
 
2014
        :param parent_invs: The inventories of the parent revisions of the
 
2015
            commit.
 
2016
        :param path: The path the entry is at in the tree.
 
2017
        :param tree: The tree which contains this entry and should be used to 
 
2018
        obtain content.
 
2019
        """
 
2020
        self.new_inventory.add(ie)
 
2021
 
 
2022
        # ie.revision is always None if the InventoryEntry is considered
 
2023
        # for committing. ie.snapshot will record the correct revision 
 
2024
        # which may be the sole parent if it is untouched.
 
2025
        if ie.revision is not None:
 
2026
            return
 
2027
        previous_entries = ie.find_previous_heads(
 
2028
            parent_invs,
 
2029
            self.repository.weave_store,
 
2030
            self.repository.get_transaction())
 
2031
        # we are creating a new revision for ie in the history store
 
2032
        # and inventory.
 
2033
        ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
 
2034
 
 
2035
    def modified_directory(self, file_id, file_parents):
 
2036
        """Record the presence of a symbolic link.
 
2037
 
 
2038
        :param file_id: The file_id of the link to record.
 
2039
        :param file_parents: The per-file parent revision ids.
 
2040
        """
 
2041
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2042
    
 
2043
    def modified_file_text(self, file_id, file_parents,
 
2044
                           get_content_byte_lines, text_sha1=None,
 
2045
                           text_size=None):
 
2046
        """Record the text of file file_id
 
2047
 
 
2048
        :param file_id: The file_id of the file to record the text of.
 
2049
        :param file_parents: The per-file parent revision ids.
 
2050
        :param get_content_byte_lines: A callable which will return the byte
 
2051
            lines for the file.
 
2052
        :param text_sha1: Optional SHA1 of the file contents.
 
2053
        :param text_size: Optional size of the file contents.
 
2054
        """
 
2055
        mutter('storing text of file {%s} in revision {%s} into %r',
 
2056
               file_id, self._new_revision_id, self.repository.weave_store)
 
2057
        # special case to avoid diffing on renames or 
 
2058
        # reparenting
 
2059
        if (len(file_parents) == 1
 
2060
            and text_sha1 == file_parents.values()[0].text_sha1
 
2061
            and text_size == file_parents.values()[0].text_size):
 
2062
            previous_ie = file_parents.values()[0]
 
2063
            versionedfile = self.repository.weave_store.get_weave(file_id, 
 
2064
                self.repository.get_transaction())
 
2065
            versionedfile.clone_text(self._new_revision_id, 
 
2066
                previous_ie.revision, file_parents.keys())
 
2067
            return text_sha1, text_size
 
2068
        else:
 
2069
            new_lines = get_content_byte_lines()
 
2070
            # TODO: Rather than invoking sha_strings here, _add_text_to_weave
 
2071
            # should return the SHA1 and size
 
2072
            self._add_text_to_weave(file_id, new_lines, file_parents.keys())
 
2073
            return bzrlib.osutils.sha_strings(new_lines), \
 
2074
                sum(map(len, new_lines))
 
2075
 
 
2076
    def modified_link(self, file_id, file_parents, link_target):
 
2077
        """Record the presence of a symbolic link.
 
2078
 
 
2079
        :param file_id: The file_id of the link to record.
 
2080
        :param file_parents: The per-file parent revision ids.
 
2081
        :param link_target: Target location of this link.
 
2082
        """
 
2083
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2084
 
 
2085
    def _add_text_to_weave(self, file_id, new_lines, parents):
 
2086
        versionedfile = self.repository.weave_store.get_weave_or_empty(
 
2087
            file_id, self.repository.get_transaction())
 
2088
        versionedfile.add_lines(self._new_revision_id, parents, new_lines)
 
2089
        versionedfile.clear_cache()
 
2090
 
 
2091
 
1800
2092
# Copied from xml.sax.saxutils
1801
2093
def _unescape_xml(data):
1802
2094
    """Unescape &, <, and > in a string of data.