~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Mark Hammond
  • Date: 2008-12-28 05:21:23 UTC
  • mfrom: (3920 +trunk)
  • mto: (3932.1.1 prepare-1.11)
  • mto: This revision was merged to the branch mainline in revision 3937.
  • Revision ID: mhammond@skippinet.com.au-20081228052123-f78xs5sbdkotshwf
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,
67
68
class CommitBuilder(object):
68
69
    """Provides an interface to build up a commit.
69
70
 
70
 
    This allows describing a tree to be committed without needing to 
 
71
    This allows describing a tree to be committed without needing to
71
72
    know the internals of the format of the repository.
72
73
    """
73
 
    
 
74
 
74
75
    # all clients should supply tree roots.
75
76
    record_root_entry = True
76
77
    # the default CommitBuilder does not manage trees whose root is versioned.
119
120
 
120
121
        self._generate_revision_if_needed()
121
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
122
129
 
123
130
    def _validate_unicode_text(self, text, context):
124
131
        """Verify things like commit messages don't have bogus characters."""
230
237
        """Get a delta against the basis inventory for ie."""
231
238
        if ie.file_id not in basis_inv:
232
239
            # add
233
 
            return (None, path, ie.file_id, ie)
 
240
            result = (None, path, ie.file_id, ie)
 
241
            self._basis_delta.append(result)
 
242
            return result
234
243
        elif ie != basis_inv[ie.file_id]:
235
244
            # common but altered
236
245
            # TODO: avoid tis id2path call.
237
 
            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
238
249
        else:
239
250
            # common, unaltered
240
251
            return None
241
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
 
242
294
    def record_entry_contents(self, ie, parent_invs, path, tree,
243
295
        content_summary):
244
296
        """Record the content of ie from tree into the commit if needed.
296
348
        if ie.revision is not None:
297
349
            if not self._versioned_root and path == '':
298
350
                # repositories that do not version the root set the root's
299
 
                # revision to the new commit even when no change occurs, and
300
 
                # this masks when a change may have occurred against the basis,
301
 
                # 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.
302
357
                if ie.file_id in basis_inv:
303
358
                    delta = (basis_inv.id2path(ie.file_id), path,
304
359
                        ie.file_id, ie)
305
360
                else:
306
361
                    # add
307
362
                    delta = (None, path, ie.file_id, ie)
 
363
                self._basis_delta.append(delta)
308
364
                return delta, False, None
309
365
            else:
310
366
                # we don't need to commit this, because the caller already
615
671
        return self._inventory_add_lines(revision_id, parents,
616
672
            inv_lines, check_content=False)
617
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
 
618
713
    def _inventory_add_lines(self, revision_id, parents, lines,
619
714
        check_content=True):
620
715
        """Store lines in inv_vf and return the sha1 of the inventory."""
748
843
        # Should fetch trigger a reconcile after the fetch? Only needed for
749
844
        # some repository formats that can suffer internal inconsistencies.
750
845
        self._fetch_reconcile = False
 
846
        # An InventoryEntry cache, used during deserialization
 
847
        self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
751
848
 
752
849
    def __repr__(self):
753
850
        return '%s(%r)' % (self.__class__.__name__,
1051
1148
                raise errors.BzrError(
1052
1149
                    'Must end write groups before releasing write locks.')
1053
1150
        self.control_files.unlock()
 
1151
        if self.control_files._lock_count == 0:
 
1152
            self._inventory_entry_cache.clear()
1054
1153
        for repo in self._fallback_repositories:
1055
1154
            repo.unlock()
1056
1155
 
1397
1496
        :param desired_files: a list of (file_id, revision_id, identifier)
1398
1497
            triples
1399
1498
        """
1400
 
        transaction = self.get_transaction()
1401
1499
        text_keys = {}
1402
1500
        for file_id, revision_id, callable_data in desired_files:
1403
1501
            text_keys[(file_id, revision_id)] = callable_data
1586
1684
    def _iter_inventory_xmls(self, revision_ids):
1587
1685
        keys = [(revision_id,) for revision_id in revision_ids]
1588
1686
        stream = self.inventories.get_record_stream(keys, 'unordered', True)
1589
 
        texts = {}
 
1687
        text_chunks = {}
1590
1688
        for record in stream:
1591
1689
            if record.storage_kind != 'absent':
1592
 
                texts[record.key] = record.get_bytes_as('fulltext')
 
1690
                text_chunks[record.key] = record.get_bytes_as('chunked')
1593
1691
            else:
1594
1692
                raise errors.NoSuchRevision(self, record.key)
1595
1693
        for key in keys:
1596
 
            yield texts[key], key[-1]
 
1694
            chunks = text_chunks.pop(key)
 
1695
            yield ''.join(chunks), key[-1]
1597
1696
 
1598
1697
    def deserialise_inventory(self, revision_id, xml):
1599
1698
        """Transform the xml into an inventory object. 
1601
1700
        :param revision_id: The expected revision id of the inventory.
1602
1701
        :param xml: A serialised inventory.
1603
1702
        """
1604
 
        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)
1605
1705
        if result.revision_id != revision_id:
1606
1706
            raise AssertionError('revision id mismatch %s != %s' % (
1607
1707
                result.revision_id, revision_id))
2259
2359
    rich_root_data = False
2260
2360
    supports_tree_reference = False
2261
2361
    supports_external_lookups = False
2262
 
    _matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
2362
 
 
2363
    @property
 
2364
    def _matchingbzrdir(self):
 
2365
        matching = bzrdir.BzrDirMetaFormat1()
 
2366
        matching.repository_format = self
 
2367
        return matching
2263
2368
 
2264
2369
    def __init__(self):
2265
2370
        super(MetaDirRepositoryFormat, self).__init__()
3077
3182
            return False
3078
3183
        return True
3079
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
 
3080
3268
    @needs_write_lock
3081
3269
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
3082
3270
        """See InterRepository.fetch()."""
3083
3271
        revision_ids = self.target.search_missing_revision_ids(self.source,
3084
3272
            revision_id, find_ghosts=find_ghosts).get_keys()
 
3273
        if not revision_ids:
 
3274
            return 0, 0
3085
3275
        revision_ids = tsort.topo_sort(
3086
3276
            self.source.get_graph().get_parent_map(revision_ids))
3087
 
        def revisions_iterator():
3088
 
            rev_ids = list(revision_ids)
3089
 
            for offset in xrange(0, len(rev_ids), 100):
3090
 
                current_revids = rev_ids[offset:offset+100]
3091
 
                revisions = self.source.get_revisions(current_revids)
3092
 
                trees = self.source.revision_trees(current_revids)
3093
 
                keys = [(r,) for r in current_revids]
3094
 
                sig_stream = self.source.signatures.get_record_stream(
3095
 
                    keys, 'unordered', True)
3096
 
                sigs = {}
3097
 
                for record in versionedfile.filter_absent(sig_stream):
3098
 
                    sigs[record.key[0]] = record.get_bytes_as('fulltext')
3099
 
                for rev, tree in zip(revisions, trees):
3100
 
                    yield rev, tree, sigs.get(rev.revision_id, None)
3101
3277
        if pb is None:
3102
3278
            my_pb = ui.ui_factory.nested_progress_bar()
3103
3279
            pb = my_pb
3104
3280
        else:
3105
3281
            my_pb = None
3106
3282
        try:
3107
 
            install_revisions(self.target, revisions_iterator(),
3108
 
                              len(revision_ids), pb)
 
3283
            self._fetch_all_revisions(revision_ids, pb)
3109
3284
        finally:
3110
3285
            if my_pb is not None:
3111
3286
                my_pb.finished()
3112
3287
        return len(revision_ids), 0
3113
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
 
3114
3311
 
3115
3312
class InterOtherToRemote(InterRepository):
3116
3313
    """An InterRepository that simply delegates to the 'real' InterRepository