/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Mark Hammond
  • Date: 2008-12-21 07:42:20 UTC
  • mfrom: (3915 +trunk)
  • mto: (3932.3.1 cicp-1.11)
  • mto: This revision was merged to the branch mainline in revision 3937.
  • Revision ID: mhammond@skippinet.com.au-20081221074220-7dr5oydglxyyvic3
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
    check,
26
26
    debug,
27
27
    errors,
 
28
    fifo_cache,
28
29
    generate_ids,
29
30
    gpg,
30
31
    graph,
38
39
    symbol_versioning,
39
40
    tsort,
40
41
    ui,
 
42
    versionedfile,
41
43
    )
42
44
from bzrlib.bundle import serializer
43
45
from bzrlib.revisiontree import RevisionTree
66
68
class CommitBuilder(object):
67
69
    """Provides an interface to build up a commit.
68
70
 
69
 
    This allows describing a tree to be committed without needing to 
 
71
    This allows describing a tree to be committed without needing to
70
72
    know the internals of the format of the repository.
71
73
    """
72
 
    
 
74
 
73
75
    # all clients should supply tree roots.
74
76
    record_root_entry = True
75
77
    # the default CommitBuilder does not manage trees whose root is versioned.
103
105
 
104
106
        self._revprops = {}
105
107
        if revprops is not None:
 
108
            self._validate_revprops(revprops)
106
109
            self._revprops.update(revprops)
107
110
 
108
111
        if timestamp is None:
117
120
 
118
121
        self._generate_revision_if_needed()
119
122
        self.__heads = graph.HeadsCache(repository.get_graph()).heads
 
123
        self._basis_delta = []
 
124
        # API compatibility, older code that used CommitBuilder did not call
 
125
        # .record_delete(), which means the delta that is computed would not be
 
126
        # valid. Callers that will call record_delete() should call
 
127
        # .will_record_deletes() to indicate that.
 
128
        self._recording_deletes = False
 
129
 
 
130
    def _validate_unicode_text(self, text, context):
 
131
        """Verify things like commit messages don't have bogus characters."""
 
132
        if '\r' in text:
 
133
            raise ValueError('Invalid value for %s: %r' % (context, text))
 
134
 
 
135
    def _validate_revprops(self, revprops):
 
136
        for key, value in revprops.iteritems():
 
137
            # We know that the XML serializers do not round trip '\r'
 
138
            # correctly, so refuse to accept them
 
139
            if not isinstance(value, basestring):
 
140
                raise ValueError('revision property (%s) is not a valid'
 
141
                                 ' (unicode) string: %r' % (key, value))
 
142
            self._validate_unicode_text(value,
 
143
                                        'revision property (%s)' % (key,))
120
144
 
121
145
    def commit(self, message):
122
146
        """Make the actual commit.
123
147
 
124
148
        :return: The revision id of the recorded revision.
125
149
        """
 
150
        self._validate_unicode_text(message, 'commit message')
126
151
        rev = _mod_revision.Revision(
127
152
                       timestamp=self._timestamp,
128
153
                       timezone=self._timezone,
212
237
        """Get a delta against the basis inventory for ie."""
213
238
        if ie.file_id not in basis_inv:
214
239
            # add
215
 
            return (None, path, ie.file_id, ie)
 
240
            result = (None, path, ie.file_id, ie)
 
241
            self._basis_delta.append(result)
 
242
            return result
216
243
        elif ie != basis_inv[ie.file_id]:
217
244
            # common but altered
218
245
            # TODO: avoid tis id2path call.
219
 
            return (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
246
            result = (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
247
            self._basis_delta.append(result)
 
248
            return result
220
249
        else:
221
250
            # common, unaltered
222
251
            return None
223
252
 
 
253
    def get_basis_delta(self):
 
254
        """Return the complete inventory delta versus the basis inventory.
 
255
 
 
256
        This has been built up with the calls to record_delete and
 
257
        record_entry_contents. The client must have already called
 
258
        will_record_deletes() to indicate that they will be generating a
 
259
        complete delta.
 
260
 
 
261
        :return: An inventory delta, suitable for use with apply_delta, or
 
262
            Repository.add_inventory_by_delta, etc.
 
263
        """
 
264
        if not self._recording_deletes:
 
265
            raise AssertionError("recording deletes not activated.")
 
266
        return self._basis_delta
 
267
 
 
268
    def record_delete(self, path, file_id):
 
269
        """Record that a delete occured against a basis tree.
 
270
 
 
271
        This is an optional API - when used it adds items to the basis_delta
 
272
        being accumulated by the commit builder. It cannot be called unless the
 
273
        method will_record_deletes() has been called to inform the builder that
 
274
        a delta is being supplied.
 
275
 
 
276
        :param path: The path of the thing deleted.
 
277
        :param file_id: The file id that was deleted.
 
278
        """
 
279
        if not self._recording_deletes:
 
280
            raise AssertionError("recording deletes not activated.")
 
281
        delta = (path, None, file_id, None)
 
282
        self._basis_delta.append(delta)
 
283
        return delta
 
284
 
 
285
    def will_record_deletes(self):
 
286
        """Tell the commit builder that deletes are being notified.
 
287
 
 
288
        This enables the accumulation of an inventory delta; for the resulting
 
289
        commit to be valid, deletes against the basis MUST be recorded via
 
290
        builder.record_delete().
 
291
        """
 
292
        self._recording_deletes = True
 
293
 
224
294
    def record_entry_contents(self, ie, parent_invs, path, tree,
225
295
        content_summary):
226
296
        """Record the content of ie from tree into the commit if needed.
278
348
        if ie.revision is not None:
279
349
            if not self._versioned_root and path == '':
280
350
                # repositories that do not version the root set the root's
281
 
                # revision to the new commit even when no change occurs, and
282
 
                # this masks when a change may have occurred against the basis,
283
 
                # so calculate if one happened.
 
351
                # revision to the new commit even when no change occurs (more
 
352
                # specifically, they do not record a revision on the root; and
 
353
                # the rev id is assigned to the root during deserialisation -
 
354
                # this masks when a change may have occurred against the basis.
 
355
                # To match this we always issue a delta, because the revision
 
356
                # of the root will always be changing.
284
357
                if ie.file_id in basis_inv:
285
358
                    delta = (basis_inv.id2path(ie.file_id), path,
286
359
                        ie.file_id, ie)
287
360
                else:
288
361
                    # add
289
362
                    delta = (None, path, ie.file_id, ie)
 
363
                self._basis_delta.append(delta)
290
364
                return delta, False, None
291
365
            else:
292
366
                # we don't need to commit this, because the caller already
563
637
        self.inventories.add_fallback_versioned_files(repository.inventories)
564
638
        self.revisions.add_fallback_versioned_files(repository.revisions)
565
639
        self.signatures.add_fallback_versioned_files(repository.signatures)
 
640
        self._fetch_order = 'topological'
566
641
 
567
642
    def _check_fallback_repository(self, repository):
568
643
        """Check that this repository can fallback to repository safely.
596
671
        return self._inventory_add_lines(revision_id, parents,
597
672
            inv_lines, check_content=False)
598
673
 
 
674
    def add_inventory_by_delta(self, basis_revision_id, delta, new_revision_id,
 
675
                               parents):
 
676
        """Add a new inventory expressed as a delta against another revision.
 
677
 
 
678
        :param basis_revision_id: The inventory id the delta was created
 
679
            against. (This does not have to be a direct parent.)
 
680
        :param delta: The inventory delta (see Inventory.apply_delta for
 
681
            details).
 
682
        :param new_revision_id: The revision id that the inventory is being
 
683
            added for.
 
684
        :param parents: The revision ids of the parents that revision_id is
 
685
            known to have and are in the repository already. These are supplied
 
686
            for repositories that depend on the inventory graph for revision
 
687
            graph access, as well as for those that pun ancestry with delta
 
688
            compression.
 
689
 
 
690
        :returns: (validator, new_inv)
 
691
            The validator(which is a sha1 digest, though what is sha'd is
 
692
            repository format specific) of the serialized inventory, and the
 
693
            resulting inventory.
 
694
        """
 
695
        if not self.is_in_write_group():
 
696
            raise AssertionError("%r not in write group" % (self,))
 
697
        _mod_revision.check_not_reserved_id(new_revision_id)
 
698
        basis_tree = self.revision_tree(basis_revision_id)
 
699
        basis_tree.lock_read()
 
700
        try:
 
701
            # Note that this mutates the inventory of basis_tree, which not all
 
702
            # inventory implementations may support: A better idiom would be to
 
703
            # return a new inventory, but as there is no revision tree cache in
 
704
            # repository this is safe for now - RBC 20081013
 
705
            basis_inv = basis_tree.inventory
 
706
            basis_inv.apply_delta(delta)
 
707
            basis_inv.revision_id = new_revision_id
 
708
            return (self.add_inventory(new_revision_id, basis_inv, parents),
 
709
                    basis_inv)
 
710
        finally:
 
711
            basis_tree.unlock()
 
712
 
599
713
    def _inventory_add_lines(self, revision_id, parents, lines,
600
714
        check_content=True):
601
715
        """Store lines in inv_vf and return the sha1 of the inventory."""
729
843
        # Should fetch trigger a reconcile after the fetch? Only needed for
730
844
        # some repository formats that can suffer internal inconsistencies.
731
845
        self._fetch_reconcile = False
 
846
        # An InventoryEntry cache, used during deserialization
 
847
        self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
732
848
 
733
849
    def __repr__(self):
734
850
        return '%s(%r)' % (self.__class__.__name__,
1032
1148
                raise errors.BzrError(
1033
1149
                    'Must end write groups before releasing write locks.')
1034
1150
        self.control_files.unlock()
 
1151
        if self.control_files._lock_count == 0:
 
1152
            self._inventory_entry_cache.clear()
1035
1153
        for repo in self._fallback_repositories:
1036
1154
            repo.unlock()
1037
1155
 
1378
1496
        :param desired_files: a list of (file_id, revision_id, identifier)
1379
1497
            triples
1380
1498
        """
1381
 
        transaction = self.get_transaction()
1382
1499
        text_keys = {}
1383
1500
        for file_id, revision_id, callable_data in desired_files:
1384
1501
            text_keys[(file_id, revision_id)] = callable_data
1527
1644
        yield ("inventory", None, revision_ids)
1528
1645
 
1529
1646
        # signatures
1530
 
        revisions_with_signatures = set()
1531
 
        for rev_id in revision_ids:
1532
 
            try:
1533
 
                self.get_signature_text(rev_id)
1534
 
            except errors.NoSuchRevision:
1535
 
                # not signed.
1536
 
                pass
1537
 
            else:
1538
 
                revisions_with_signatures.add(rev_id)
 
1647
        # XXX: Note ATM no callers actually pay attention to this return
 
1648
        #      instead they just use the list of revision ids and ignore
 
1649
        #      missing sigs. Consider removing this work entirely
 
1650
        revisions_with_signatures = set(self.signatures.get_parent_map(
 
1651
            [(r,) for r in revision_ids]))
 
1652
        revisions_with_signatures = set(
 
1653
            [r for (r,) in revisions_with_signatures])
 
1654
        revisions_with_signatures.intersection_update(revision_ids)
1539
1655
        yield ("signatures", None, revisions_with_signatures)
1540
1656
 
1541
1657
        # revisions
1568
1684
    def _iter_inventory_xmls(self, revision_ids):
1569
1685
        keys = [(revision_id,) for revision_id in revision_ids]
1570
1686
        stream = self.inventories.get_record_stream(keys, 'unordered', True)
1571
 
        texts = {}
 
1687
        text_chunks = {}
1572
1688
        for record in stream:
1573
1689
            if record.storage_kind != 'absent':
1574
 
                texts[record.key] = record.get_bytes_as('fulltext')
 
1690
                text_chunks[record.key] = record.get_bytes_as('chunked')
1575
1691
            else:
1576
1692
                raise errors.NoSuchRevision(self, record.key)
1577
1693
        for key in keys:
1578
 
            yield texts[key], key[-1]
 
1694
            chunks = text_chunks.pop(key)
 
1695
            yield ''.join(chunks), key[-1]
1579
1696
 
1580
1697
    def deserialise_inventory(self, revision_id, xml):
1581
1698
        """Transform the xml into an inventory object. 
1583
1700
        :param revision_id: The expected revision id of the inventory.
1584
1701
        :param xml: A serialised inventory.
1585
1702
        """
1586
 
        result = self._serializer.read_inventory_from_string(xml, revision_id)
 
1703
        result = self._serializer.read_inventory_from_string(xml, revision_id,
 
1704
                    entry_cache=self._inventory_entry_cache)
1587
1705
        if result.revision_id != revision_id:
1588
1706
            raise AssertionError('revision id mismatch %s != %s' % (
1589
1707
                result.revision_id, revision_id))
2241
2359
    rich_root_data = False
2242
2360
    supports_tree_reference = False
2243
2361
    supports_external_lookups = False
2244
 
    _matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
2362
 
 
2363
    @property
 
2364
    def _matchingbzrdir(self):
 
2365
        matching = bzrdir.BzrDirMetaFormat1()
 
2366
        matching.repository_format = self
 
2367
        return matching
2245
2368
 
2246
2369
    def __init__(self):
2247
2370
        super(MetaDirRepositoryFormat, self).__init__()
3059
3182
            return False
3060
3183
        return True
3061
3184
 
 
3185
    def _fetch_batch(self, revision_ids, basis_id, basis_tree):
 
3186
        """Fetch across a few revisions.
 
3187
 
 
3188
        :param revision_ids: The revisions to copy
 
3189
        :param basis_id: The revision_id of basis_tree
 
3190
        :param basis_tree: A tree that is not in revision_ids which should
 
3191
            already exist in the target.
 
3192
        :return: (basis_id, basis_tree) A new basis to use now that these trees
 
3193
            have been copied.
 
3194
        """
 
3195
        # Walk though all revisions; get inventory deltas, copy referenced
 
3196
        # texts that delta references, insert the delta, revision and
 
3197
        # signature.
 
3198
        text_keys = set()
 
3199
        pending_deltas = []
 
3200
        pending_revisions = []
 
3201
        for tree in self.source.revision_trees(revision_ids):
 
3202
            current_revision_id = tree.get_revision_id()
 
3203
            delta = tree.inventory._make_delta(basis_tree.inventory)
 
3204
            for old_path, new_path, file_id, entry in delta:
 
3205
                if new_path is not None:
 
3206
                    if not (new_path or self.target.supports_rich_root()):
 
3207
                        # We leave the inventory delta in, because that
 
3208
                        # will have the deserialised inventory root
 
3209
                        # pointer.
 
3210
                        continue
 
3211
                    # TODO: Do we need:
 
3212
                    #       "if entry.revision == current_revision_id" ?
 
3213
                    if entry.revision == current_revision_id:
 
3214
                        text_keys.add((file_id, entry.revision))
 
3215
            revision = self.source.get_revision(current_revision_id)
 
3216
            pending_deltas.append((basis_id, delta,
 
3217
                current_revision_id, revision.parent_ids))
 
3218
            pending_revisions.append(revision)
 
3219
            basis_id = current_revision_id
 
3220
            basis_tree = tree
 
3221
        # Copy file texts
 
3222
        from_texts = self.source.texts
 
3223
        to_texts = self.target.texts
 
3224
        to_texts.insert_record_stream(from_texts.get_record_stream(
 
3225
            text_keys, self.target._fetch_order,
 
3226
            not self.target._fetch_uses_deltas))
 
3227
        # insert deltas
 
3228
        for delta in pending_deltas:
 
3229
            self.target.add_inventory_by_delta(*delta)
 
3230
        # insert signatures and revisions
 
3231
        for revision in pending_revisions:
 
3232
            try:
 
3233
                signature = self.source.get_signature_text(
 
3234
                    revision.revision_id)
 
3235
                self.target.add_signature_text(revision.revision_id,
 
3236
                    signature)
 
3237
            except errors.NoSuchRevision:
 
3238
                pass
 
3239
            self.target.add_revision(revision.revision_id, revision)
 
3240
        return basis_id, basis_tree
 
3241
 
 
3242
    def _fetch_all_revisions(self, revision_ids, pb):
 
3243
        """Fetch everything for the list of revisions.
 
3244
 
 
3245
        :param revision_ids: The list of revisions to fetch. Must be in
 
3246
            topological order.
 
3247
        :param pb: A ProgressBar
 
3248
        :return: None
 
3249
        """
 
3250
        basis_id, basis_tree = self._get_basis(revision_ids[0])
 
3251
        batch_size = 100
 
3252
        for offset in range(0, len(revision_ids), batch_size):
 
3253
            self.target.start_write_group()
 
3254
            try:
 
3255
                pb.update('Transferring revisions', offset,
 
3256
                          len(revision_ids))
 
3257
                batch = revision_ids[offset:offset+batch_size]
 
3258
                basis_id, basis_tree = self._fetch_batch(batch,
 
3259
                    basis_id, basis_tree)
 
3260
            except:
 
3261
                self.target.abort_write_group()
 
3262
                raise
 
3263
            else:
 
3264
                self.target.commit_write_group()
 
3265
        pb.update('Transferring revisions', len(revision_ids),
 
3266
                  len(revision_ids))
 
3267
 
3062
3268
    @needs_write_lock
3063
3269
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
3064
3270
        """See InterRepository.fetch()."""
3065
3271
        revision_ids = self.target.search_missing_revision_ids(self.source,
3066
3272
            revision_id, find_ghosts=find_ghosts).get_keys()
 
3273
        if not revision_ids:
 
3274
            return 0, 0
3067
3275
        revision_ids = tsort.topo_sort(
3068
3276
            self.source.get_graph().get_parent_map(revision_ids))
3069
 
        def revisions_iterator():
3070
 
            for current_revision_id in revision_ids:
3071
 
                revision = self.source.get_revision(current_revision_id)
3072
 
                tree = self.source.revision_tree(current_revision_id)
3073
 
                try:
3074
 
                    signature = self.source.get_signature_text(
3075
 
                        current_revision_id)
3076
 
                except errors.NoSuchRevision:
3077
 
                    signature = None
3078
 
                yield revision, tree, signature
3079
3277
        if pb is None:
3080
3278
            my_pb = ui.ui_factory.nested_progress_bar()
3081
3279
            pb = my_pb
3082
3280
        else:
3083
3281
            my_pb = None
3084
3282
        try:
3085
 
            install_revisions(self.target, revisions_iterator(),
3086
 
                              len(revision_ids), pb)
 
3283
            self._fetch_all_revisions(revision_ids, pb)
3087
3284
        finally:
3088
3285
            if my_pb is not None:
3089
3286
                my_pb.finished()
3090
3287
        return len(revision_ids), 0
3091
3288
 
 
3289
    def _get_basis(self, first_revision_id):
 
3290
        """Get a revision and tree which exists in the target.
 
3291
 
 
3292
        This assumes that first_revision_id is selected for transmission
 
3293
        because all other ancestors are already present. If we can't find an
 
3294
        ancestor we fall back to NULL_REVISION since we know that is safe.
 
3295
 
 
3296
        :return: (basis_id, basis_tree)
 
3297
        """
 
3298
        first_rev = self.source.get_revision(first_revision_id)
 
3299
        try:
 
3300
            basis_id = first_rev.parent_ids[0]
 
3301
            # only valid as a basis if the target has it
 
3302
            self.target.get_revision(basis_id)
 
3303
            # Try to get a basis tree - if its a ghost it will hit the
 
3304
            # NoSuchRevision case.
 
3305
            basis_tree = self.source.revision_tree(basis_id)
 
3306
        except (IndexError, errors.NoSuchRevision):
 
3307
            basis_id = _mod_revision.NULL_REVISION
 
3308
            basis_tree = self.source.revision_tree(basis_id)
 
3309
        return basis_id, basis_tree
 
3310
 
3092
3311
 
3093
3312
class InterOtherToRemote(InterRepository):
3094
3313
    """An InterRepository that simply delegates to the 'real' InterRepository