~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-03-17 07:05:37 UTC
  • mfrom: (4152.1.2 branch.stacked.streams)
  • Revision ID: pqm@pqm.ubuntu.com-20090317070537-zaud24vjs2szna87
(robertc) Add client-side streaming from stacked branches (over
        bzr:// protocols) when the sort order is compatible with doing
        that. (Robert Collins, Andrew Bennetts)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
15
15
#
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
20
"""Versioned text file storage api."""
21
21
 
30
30
import urllib
31
31
 
32
32
from bzrlib import (
33
 
    annotate,
34
33
    errors,
35
 
    graph as _mod_graph,
36
 
    groupcompress,
37
34
    index,
38
35
    knit,
39
36
    osutils,
42
39
    revision,
43
40
    ui,
44
41
    )
45
 
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
 
42
from bzrlib.graph import DictParentsProvider, Graph, _StackedParentsProvider
46
43
from bzrlib.transport.memory import MemoryTransport
47
44
""")
 
45
from bzrlib.inter import InterObject
48
46
from bzrlib.registry import Registry
 
47
from bzrlib.symbol_versioning import *
49
48
from bzrlib.textmerge import TextMerge
50
 
from bzrlib import bencode
 
49
from bzrlib.util import bencode
51
50
 
52
51
 
53
52
adapter_registry = Registry()
174
173
        self.key = key
175
174
        self.parents = None
176
175
 
177
 
    def get_bytes_as(self, storage_kind):
178
 
        raise ValueError('A request was made for key: %s, but that'
179
 
                         ' content is not available, and the calling'
180
 
                         ' code does not handle if it is missing.'
181
 
                         % (self.key,))
182
 
 
183
176
 
184
177
class AdapterFactory(ContentFactory):
185
178
    """A content factory to adapt between key prefix's."""
205
198
            yield record
206
199
 
207
200
 
208
 
class _MPDiffGenerator(object):
209
 
    """Pull out the functionality for generating mp_diffs."""
210
 
 
211
 
    def __init__(self, vf, keys):
212
 
        self.vf = vf
213
 
        # This is the order the keys were requested in
214
 
        self.ordered_keys = tuple(keys)
215
 
        # keys + their parents, what we need to compute the diffs
216
 
        self.needed_keys = ()
217
 
        # Map from key: mp_diff
218
 
        self.diffs = {}
219
 
        # Map from key: parents_needed (may have ghosts)
220
 
        self.parent_map = {}
221
 
        # Parents that aren't present
222
 
        self.ghost_parents = ()
223
 
        # Map from parent_key => number of children for this text
224
 
        self.refcounts = {}
225
 
        # Content chunks that are cached while we still need them
226
 
        self.chunks = {}
227
 
 
228
 
    def _find_needed_keys(self):
229
 
        """Find the set of keys we need to request.
230
 
 
231
 
        This includes all the original keys passed in, and the non-ghost
232
 
        parents of those keys.
233
 
 
234
 
        :return: (needed_keys, refcounts)
235
 
            needed_keys is the set of all texts we need to extract
236
 
            refcounts is a dict of {key: num_children} letting us know when we
237
 
                no longer need to cache a given parent text
238
 
        """
239
 
        # All the keys and their parents
240
 
        needed_keys = set(self.ordered_keys)
241
 
        parent_map = self.vf.get_parent_map(needed_keys)
242
 
        self.parent_map = parent_map
243
 
        # TODO: Should we be using a different construct here? I think this
244
 
        #       uses difference_update internally, and we expect the result to
245
 
        #       be tiny
246
 
        missing_keys = needed_keys.difference(parent_map)
247
 
        if missing_keys:
248
 
            raise errors.RevisionNotPresent(list(missing_keys)[0], self.vf)
249
 
        # Parents that might be missing. They are allowed to be ghosts, but we
250
 
        # should check for them
251
 
        refcounts = {}
252
 
        setdefault = refcounts.setdefault
253
 
        just_parents = set()
254
 
        for child_key, parent_keys in parent_map.iteritems():
255
 
            if not parent_keys:
256
 
                # parent_keys may be None if a given VersionedFile claims to
257
 
                # not support graph operations.
258
 
                continue
259
 
            just_parents.update(parent_keys)
260
 
            needed_keys.update(parent_keys)
261
 
            for p in parent_keys:
262
 
                refcounts[p] = setdefault(p, 0) + 1
263
 
        just_parents.difference_update(parent_map)
264
 
        # Remove any parents that are actually ghosts from the needed set
265
 
        self.present_parents = set(self.vf.get_parent_map(just_parents))
266
 
        self.ghost_parents = just_parents.difference(self.present_parents)
267
 
        needed_keys.difference_update(self.ghost_parents)
268
 
        self.needed_keys = needed_keys
269
 
        self.refcounts = refcounts
270
 
        return needed_keys, refcounts
271
 
 
272
 
    def _compute_diff(self, key, parent_lines, lines):
273
 
        """Compute a single mp_diff, and store it in self._diffs"""
274
 
        if len(parent_lines) > 0:
275
 
            # XXX: _extract_blocks is not usefully defined anywhere...
276
 
            #      It was meant to extract the left-parent diff without
277
 
            #      having to recompute it for Knit content (pack-0.92,
278
 
            #      etc). That seems to have regressed somewhere
279
 
            left_parent_blocks = self.vf._extract_blocks(key,
280
 
                parent_lines[0], lines)
281
 
        else:
282
 
            left_parent_blocks = None
283
 
        diff = multiparent.MultiParent.from_lines(lines,
284
 
                    parent_lines, left_parent_blocks)
285
 
        self.diffs[key] = diff
286
 
 
287
 
    def _process_one_record(self, key, this_chunks):
288
 
        parent_keys = None
289
 
        if key in self.parent_map:
290
 
            # This record should be ready to diff, since we requested
291
 
            # content in 'topological' order
292
 
            parent_keys = self.parent_map.pop(key)
293
 
            # If a VersionedFile claims 'no-graph' support, then it may return
294
 
            # None for any parent request, so we replace it with an empty tuple
295
 
            if parent_keys is None:
296
 
                parent_keys = ()
297
 
            parent_lines = []
298
 
            for p in parent_keys:
299
 
                # Alternatively we could check p not in self.needed_keys, but
300
 
                # ghost_parents should be tiny versus huge
301
 
                if p in self.ghost_parents:
302
 
                    continue
303
 
                refcount = self.refcounts[p]
304
 
                if refcount == 1: # Last child reference
305
 
                    self.refcounts.pop(p)
306
 
                    parent_chunks = self.chunks.pop(p)
307
 
                else:
308
 
                    self.refcounts[p] = refcount - 1
309
 
                    parent_chunks = self.chunks[p]
310
 
                p_lines = osutils.chunks_to_lines(parent_chunks)
311
 
                # TODO: Should we cache the line form? We did the
312
 
                #       computation to get it, but storing it this way will
313
 
                #       be less memory efficient...
314
 
                parent_lines.append(p_lines)
315
 
                del p_lines
316
 
            lines = osutils.chunks_to_lines(this_chunks)
317
 
            # Since we needed the lines, we'll go ahead and cache them this way
318
 
            this_chunks = lines
319
 
            self._compute_diff(key, parent_lines, lines)
320
 
            del lines
321
 
        # Is this content required for any more children?
322
 
        if key in self.refcounts:
323
 
            self.chunks[key] = this_chunks
324
 
 
325
 
    def _extract_diffs(self):
326
 
        needed_keys, refcounts = self._find_needed_keys()
327
 
        for record in self.vf.get_record_stream(needed_keys,
328
 
                                                'topological', True):
329
 
            if record.storage_kind == 'absent':
330
 
                raise errors.RevisionNotPresent(record.key, self.vf)
331
 
            self._process_one_record(record.key,
332
 
                                     record.get_bytes_as('chunked'))
333
 
        
334
 
    def compute_diffs(self):
335
 
        self._extract_diffs()
336
 
        dpop = self.diffs.pop
337
 
        return [dpop(k) for k in self.ordered_keys]
338
 
 
339
 
 
340
201
class VersionedFile(object):
341
202
    """Versioned text file storage.
342
203
 
479
340
 
480
341
    def make_mpdiffs(self, version_ids):
481
342
        """Create multiparent diffs for specified versions."""
482
 
        # XXX: Can't use _MPDiffGenerator just yet. This is because version_ids
483
 
        #      is a list of strings, not keys. And while self.get_record_stream
484
 
        #      is supported, it takes *keys*, while self.get_parent_map() takes
485
 
        #      strings... *sigh*
486
343
        knit_versions = set()
487
344
        knit_versions.update(version_ids)
488
345
        parent_map = self.get_parent_map(version_ids)
937
794
        check_content=True):
938
795
        """Add a text to the store.
939
796
 
940
 
        :param key: The key tuple of the text to add. If the last element is
941
 
            None, a CHK string will be generated during the addition.
 
797
        :param key: The key tuple of the text to add.
942
798
        :param parents: The parents key tuples of the text to add.
943
799
        :param lines: A list of lines. Each line must be a bytestring. And all
944
800
            of them except the last must be terminated with \n and contain no
971
827
        """
972
828
        raise NotImplementedError(self.add_lines)
973
829
 
974
 
    def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
975
 
        """Add a text to the store.
976
 
 
977
 
        This is a private function for use by CommitBuilder.
978
 
 
979
 
        :param key: The key tuple of the text to add. If the last element is
980
 
            None, a CHK string will be generated during the addition.
981
 
        :param parents: The parents key tuples of the text to add.
982
 
        :param text: A string containing the text to be committed.
983
 
        :param nostore_sha: Raise ExistingContent and do not add the lines to
984
 
            the versioned file if the digest of the lines matches this.
985
 
        :param random_id: If True a random id has been selected rather than
986
 
            an id determined by some deterministic process such as a converter
987
 
            from a foreign VCS. When True the backend may choose not to check
988
 
            for uniqueness of the resulting key within the versioned file, so
989
 
            this should only be done when the result is expected to be unique
990
 
            anyway.
991
 
        :param check_content: If True, the lines supplied are verified to be
992
 
            bytestrings that are correctly formed lines.
993
 
        :return: The text sha1, the number of bytes in the text, and an opaque
994
 
                 representation of the inserted version which can be provided
995
 
                 back to future _add_text calls in the parent_texts dictionary.
996
 
        """
997
 
        # The default implementation just thunks over to .add_lines(),
998
 
        # inefficient, but it works.
999
 
        return self.add_lines(key, parents, osutils.split_lines(text),
1000
 
                              nostore_sha=nostore_sha,
1001
 
                              random_id=random_id,
1002
 
                              check_content=True)
1003
 
 
1004
830
    def add_mpdiffs(self, records):
1005
831
        """Add mpdiffs to this VersionedFile.
1006
832
 
1048
874
        raise NotImplementedError(self.annotate)
1049
875
 
1050
876
    def check(self, progress_bar=None):
1051
 
        """Check this object for integrity.
1052
 
        
1053
 
        :param progress_bar: A progress bar to output as the check progresses.
1054
 
        :param keys: Specific keys within the VersionedFiles to check. When
1055
 
            this parameter is not None, check() becomes a generator as per
1056
 
            get_record_stream. The difference to get_record_stream is that
1057
 
            more or deeper checks will be performed.
1058
 
        :return: None, or if keys was supplied a generator as per
1059
 
            get_record_stream.
1060
 
        """
 
877
        """Check this object for integrity."""
1061
878
        raise NotImplementedError(self.check)
1062
879
 
1063
880
    @staticmethod
1064
881
    def check_not_reserved_id(version_id):
1065
882
        revision.check_not_reserved_id(version_id)
1066
883
 
1067
 
    def clear_cache(self):
1068
 
        """Clear whatever caches this VersionedFile holds.
1069
 
 
1070
 
        This is generally called after an operation has been performed, when we
1071
 
        don't expect to be using this versioned file again soon.
1072
 
        """
1073
 
 
1074
884
    def _check_lines_not_unicode(self, lines):
1075
885
        """Check that lines being added to a versioned file are not unicode."""
1076
886
        for line in lines:
1083
893
            if '\n' in line[:-1]:
1084
894
                raise errors.BzrBadParameterContainsNewline("lines")
1085
895
 
1086
 
    def get_known_graph_ancestry(self, keys):
1087
 
        """Get a KnownGraph instance with the ancestry of keys."""
1088
 
        # most basic implementation is a loop around get_parent_map
1089
 
        pending = set(keys)
1090
 
        parent_map = {}
1091
 
        while pending:
1092
 
            this_parent_map = self.get_parent_map(pending)
1093
 
            parent_map.update(this_parent_map)
1094
 
            pending = set()
1095
 
            map(pending.update, this_parent_map.itervalues())
1096
 
            pending = pending.difference(parent_map)
1097
 
        kg = _mod_graph.KnownGraph(parent_map)
1098
 
        return kg
1099
 
 
1100
896
    def get_parent_map(self, keys):
1101
897
        """Get a map of the parents of keys.
1102
898
 
1182
978
 
1183
979
    def make_mpdiffs(self, keys):
1184
980
        """Create multiparent diffs for specified keys."""
1185
 
        generator = _MPDiffGenerator(self, keys)
1186
 
        return generator.compute_diffs()
1187
 
 
1188
 
    def get_annotator(self):
1189
 
        return annotate.Annotator(self)
 
981
        keys_order = tuple(keys)
 
982
        keys = frozenset(keys)
 
983
        knit_keys = set(keys)
 
984
        parent_map = self.get_parent_map(keys)
 
985
        for parent_keys in parent_map.itervalues():
 
986
            if parent_keys:
 
987
                knit_keys.update(parent_keys)
 
988
        missing_keys = keys - set(parent_map)
 
989
        if missing_keys:
 
990
            raise errors.RevisionNotPresent(list(missing_keys)[0], self)
 
991
        # We need to filter out ghosts, because we can't diff against them.
 
992
        maybe_ghosts = knit_keys - keys
 
993
        ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
 
994
        knit_keys.difference_update(ghosts)
 
995
        lines = {}
 
996
        chunks_to_lines = osutils.chunks_to_lines
 
997
        for record in self.get_record_stream(knit_keys, 'topological', True):
 
998
            lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
 
999
            # line_block_dict = {}
 
1000
            # for parent, blocks in record.extract_line_blocks():
 
1001
            #   line_blocks[parent] = blocks
 
1002
            # line_blocks[record.key] = line_block_dict
 
1003
        diffs = []
 
1004
        for key in keys_order:
 
1005
            target = lines[key]
 
1006
            parents = parent_map[key] or []
 
1007
            # Note that filtering knit_keys can lead to a parent difference
 
1008
            # between the creation and the application of the mpdiff.
 
1009
            parent_lines = [lines[p] for p in parents if p in knit_keys]
 
1010
            if len(parent_lines) > 0:
 
1011
                left_parent_blocks = self._extract_blocks(key, parent_lines[0],
 
1012
                    target)
 
1013
            else:
 
1014
                left_parent_blocks = None
 
1015
            diffs.append(multiparent.MultiParent.from_lines(target,
 
1016
                parent_lines, left_parent_blocks))
 
1017
        return diffs
1190
1018
 
1191
1019
    missing_keys = index._missing_keys_from_parent_map
1192
1020
 
1262
1090
            result.append((prefix + (origin,), line))
1263
1091
        return result
1264
1092
 
1265
 
    def check(self, progress_bar=None, keys=None):
 
1093
    def check(self, progress_bar=None):
1266
1094
        """See VersionedFiles.check()."""
1267
 
        # XXX: This is over-enthusiastic but as we only thunk for Weaves today
1268
 
        # this is tolerable. Ideally we'd pass keys down to check() and 
1269
 
        # have the older VersiondFile interface updated too.
1270
1095
        for prefix, vf in self._iter_all_components():
1271
1096
            vf.check()
1272
 
        if keys is not None:
1273
 
            return self.get_record_stream(keys, 'unordered', True)
1274
1097
 
1275
1098
    def get_parent_map(self, keys):
1276
1099
        """Get a map of the parents of keys.
1508
1331
            result[revision.NULL_REVISION] = ()
1509
1332
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1510
1333
        result.update(
1511
 
            StackedParentsProvider(self._providers).get_parent_map(keys))
 
1334
            _StackedParentsProvider(self._providers).get_parent_map(keys))
1512
1335
        for key, parents in result.iteritems():
1513
1336
            if parents == ():
1514
1337
                result[key] = (revision.NULL_REVISION,)
1525
1348
    def __init__(self, plan, a_marker=TextMerge.A_MARKER,
1526
1349
                 b_marker=TextMerge.B_MARKER):
1527
1350
        TextMerge.__init__(self, a_marker, b_marker)
1528
 
        self.plan = list(plan)
 
1351
        self.plan = plan
1529
1352
 
1530
1353
    def _merge_struct(self):
1531
1354
        lines_a = []
1578
1401
            elif state == 'conflicted-b':
1579
1402
                ch_b = ch_a = True
1580
1403
                lines_b.append(line)
1581
 
            elif state == 'killed-both':
1582
 
                # This counts as a change, even though there is no associated
1583
 
                # line
1584
 
                ch_b = ch_a = True
1585
1404
            else:
1586
1405
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1587
 
                        'killed-base'):
 
1406
                        'killed-base', 'killed-both'):
1588
1407
                    raise AssertionError(state)
1589
1408
        for struct in outstanding_struct():
1590
1409
            yield struct
1591
1410
 
1592
 
    def base_from_plan(self):
1593
 
        """Construct a BASE file from the plan text."""
1594
 
        base_lines = []
1595
 
        for state, line in self.plan:
1596
 
            if state in ('killed-a', 'killed-b', 'killed-both', 'unchanged'):
1597
 
                # If unchanged, then this line is straight from base. If a or b
1598
 
                # or both killed the line, then it *used* to be in base.
1599
 
                base_lines.append(line)
1600
 
            else:
1601
 
                if state not in ('killed-base', 'irrelevant',
1602
 
                                 'ghost-a', 'ghost-b',
1603
 
                                 'new-a', 'new-b',
1604
 
                                 'conflicted-a', 'conflicted-b'):
1605
 
                    # killed-base, irrelevant means it doesn't apply
1606
 
                    # ghost-a/ghost-b are harder to say for sure, but they
1607
 
                    # aren't in the 'inc_c' which means they aren't in the
1608
 
                    # shared base of a & b. So we don't include them.  And
1609
 
                    # obviously if the line is newly inserted, it isn't in base
1610
 
 
1611
 
                    # If 'conflicted-a' or b, then it is new vs one base, but
1612
 
                    # old versus another base. However, if we make it present
1613
 
                    # in the base, it will be deleted from the target, and it
1614
 
                    # seems better to get a line doubled in the merge result,
1615
 
                    # rather than have it deleted entirely.
1616
 
                    # Example, each node is the 'text' at that point:
1617
 
                    #           MN
1618
 
                    #          /   \
1619
 
                    #        MaN   MbN
1620
 
                    #         |  X  |
1621
 
                    #        MabN MbaN
1622
 
                    #          \   /
1623
 
                    #           ???
1624
 
                    # There was a criss-cross conflict merge. Both sides
1625
 
                    # include the other, but put themselves first.
1626
 
                    # Weave marks this as a 'clean' merge, picking OTHER over
1627
 
                    # THIS. (Though the details depend on order inserted into
1628
 
                    # weave, etc.)
1629
 
                    # LCA generates a plan:
1630
 
                    # [('unchanged', M),
1631
 
                    #  ('conflicted-b', b),
1632
 
                    #  ('unchanged', a),
1633
 
                    #  ('conflicted-a', b),
1634
 
                    #  ('unchanged', N)]
1635
 
                    # If you mark 'conflicted-*' as part of BASE, then a 3-way
1636
 
                    # merge tool will cleanly generate "MaN" (as BASE vs THIS
1637
 
                    # removes one 'b', and BASE vs OTHER removes the other)
1638
 
                    # If you include neither, 3-way creates a clean "MbabN" as
1639
 
                    # THIS adds one 'b', and OTHER does too.
1640
 
                    # It seems that having the line 2 times is better than
1641
 
                    # having it omitted. (Easier to manually delete than notice
1642
 
                    # it needs to be added.)
1643
 
                    raise AssertionError('Unknown state: %s' % (state,))
1644
 
        return base_lines
1645
 
 
1646
1411
 
1647
1412
class WeaveMerge(PlanWeaveMerge):
1648
1413
    """Weave merge that takes a VersionedFile and two versions as its input."""
1724
1489
                yield (l, key)
1725
1490
 
1726
1491
 
1727
 
class NoDupeAddLinesDecorator(object):
1728
 
    """Decorator for a VersionedFiles that skips doing an add_lines if the key
1729
 
    is already present.
1730
 
    """
1731
 
 
1732
 
    def __init__(self, store):
1733
 
        self._store = store
1734
 
 
1735
 
    def add_lines(self, key, parents, lines, parent_texts=None,
1736
 
            left_matching_blocks=None, nostore_sha=None, random_id=False,
1737
 
            check_content=True):
1738
 
        """See VersionedFiles.add_lines.
1739
 
        
1740
 
        This implementation may return None as the third element of the return
1741
 
        value when the original store wouldn't.
1742
 
        """
1743
 
        if nostore_sha:
1744
 
            raise NotImplementedError(
1745
 
                "NoDupeAddLinesDecorator.add_lines does not implement the "
1746
 
                "nostore_sha behaviour.")
1747
 
        if key[-1] is None:
1748
 
            sha1 = osutils.sha_strings(lines)
1749
 
            key = ("sha1:" + sha1,)
1750
 
        else:
1751
 
            sha1 = None
1752
 
        if key in self._store.get_parent_map([key]):
1753
 
            # This key has already been inserted, so don't do it again.
1754
 
            if sha1 is None:
1755
 
                sha1 = osutils.sha_strings(lines)
1756
 
            return sha1, sum(map(len, lines)), None
1757
 
        return self._store.add_lines(key, parents, lines,
1758
 
                parent_texts=parent_texts,
1759
 
                left_matching_blocks=left_matching_blocks,
1760
 
                nostore_sha=nostore_sha, random_id=random_id,
1761
 
                check_content=check_content)
1762
 
 
1763
 
    def __getattr__(self, name):
1764
 
        return getattr(self._store, name)
1765
 
 
1766
 
 
1767
1492
def network_bytes_to_kind_and_offset(network_bytes):
1768
1493
    """Strip of a record kind from the front of network_bytes.
1769
1494
 
1786
1511
            record.get_bytes_as(record.storage_kind) call.
1787
1512
        """
1788
1513
        self._bytes_iterator = bytes_iterator
1789
 
        self._kind_factory = {
1790
 
            'fulltext': fulltext_network_to_record,
1791
 
            'groupcompress-block': groupcompress.network_block_to_records,
1792
 
            'knit-ft-gz': knit.knit_network_to_record,
1793
 
            'knit-delta-gz': knit.knit_network_to_record,
1794
 
            'knit-annotated-ft-gz': knit.knit_network_to_record,
1795
 
            'knit-annotated-delta-gz': knit.knit_network_to_record,
1796
 
            'knit-delta-closure': knit.knit_delta_closure_to_records,
 
1514
        self._kind_factory = {'knit-ft-gz':knit.knit_network_to_record,
 
1515
            'knit-delta-gz':knit.knit_network_to_record,
 
1516
            'knit-annotated-ft-gz':knit.knit_network_to_record,
 
1517
            'knit-annotated-delta-gz':knit.knit_network_to_record,
 
1518
            'knit-delta-closure':knit.knit_delta_closure_to_records,
 
1519
            'fulltext':fulltext_network_to_record,
1797
1520
            }
1798
1521
 
1799
1522
    def read(self):