~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: John Arbash Meinel
  • Date: 2008-12-10 17:22:13 UTC
  • mfrom: (3890 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3912.
  • Revision ID: john@arbash-meinel.com-20081210172213-h2b0auuil3qaz28u
Merge bzr.dev 3890, bring in the FIFOCache.

Show diffs side-by-side

added added

removed removed

Lines of Context:
67
67
class CommitBuilder(object):
68
68
    """Provides an interface to build up a commit.
69
69
 
70
 
    This allows describing a tree to be committed without needing to 
 
70
    This allows describing a tree to be committed without needing to
71
71
    know the internals of the format of the repository.
72
72
    """
73
 
    
 
73
 
74
74
    # all clients should supply tree roots.
75
75
    record_root_entry = True
76
76
    # the default CommitBuilder does not manage trees whose root is versioned.
119
119
 
120
120
        self._generate_revision_if_needed()
121
121
        self.__heads = graph.HeadsCache(repository.get_graph()).heads
 
122
        self._basis_delta = []
 
123
        # API compatibility, older code that used CommitBuilder did not call
 
124
        # .record_delete(), which means the delta that is computed would not be
 
125
        # valid. Callers that will call record_delete() should call
 
126
        # .will_record_deletes() to indicate that.
 
127
        self._recording_deletes = False
122
128
 
123
129
    def _validate_unicode_text(self, text, context):
124
130
        """Verify things like commit messages don't have bogus characters."""
230
236
        """Get a delta against the basis inventory for ie."""
231
237
        if ie.file_id not in basis_inv:
232
238
            # add
233
 
            return (None, path, ie.file_id, ie)
 
239
            result = (None, path, ie.file_id, ie)
 
240
            self._basis_delta.append(result)
 
241
            return result
234
242
        elif ie != basis_inv[ie.file_id]:
235
243
            # common but altered
236
244
            # TODO: avoid tis id2path call.
237
 
            return (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
245
            result = (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
246
            self._basis_delta.append(result)
 
247
            return result
238
248
        else:
239
249
            # common, unaltered
240
250
            return None
241
251
 
 
252
    def get_basis_delta(self):
 
253
        """Return the complete inventory delta versus the basis inventory.
 
254
 
 
255
        This has been built up with the calls to record_delete and
 
256
        record_entry_contents. The client must have already called
 
257
        will_record_deletes() to indicate that they will be generating a
 
258
        complete delta.
 
259
 
 
260
        :return: An inventory delta, suitable for use with apply_delta, or
 
261
            Repository.add_inventory_by_delta, etc.
 
262
        """
 
263
        if not self._recording_deletes:
 
264
            raise AssertionError("recording deletes not activated.")
 
265
        return self._basis_delta
 
266
 
 
267
    def record_delete(self, path, file_id):
 
268
        """Record that a delete occured against a basis tree.
 
269
 
 
270
        This is an optional API - when used it adds items to the basis_delta
 
271
        being accumulated by the commit builder. It cannot be called unless the
 
272
        method will_record_deletes() has been called to inform the builder that
 
273
        a delta is being supplied.
 
274
 
 
275
        :param path: The path of the thing deleted.
 
276
        :param file_id: The file id that was deleted.
 
277
        """
 
278
        if not self._recording_deletes:
 
279
            raise AssertionError("recording deletes not activated.")
 
280
        delta = (path, None, file_id, None)
 
281
        self._basis_delta.append(delta)
 
282
        return delta
 
283
 
 
284
    def will_record_deletes(self):
 
285
        """Tell the commit builder that deletes are being notified.
 
286
 
 
287
        This enables the accumulation of an inventory delta; for the resulting
 
288
        commit to be valid, deletes against the basis MUST be recorded via
 
289
        builder.record_delete().
 
290
        """
 
291
        self._recording_deletes = True
 
292
 
242
293
    def record_entry_contents(self, ie, parent_invs, path, tree,
243
294
        content_summary):
244
295
        """Record the content of ie from tree into the commit if needed.
296
347
        if ie.revision is not None:
297
348
            if not self._versioned_root and path == '':
298
349
                # 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.
 
350
                # revision to the new commit even when no change occurs (more
 
351
                # specifically, they do not record a revision on the root; and
 
352
                # the rev id is assigned to the root during deserialisation -
 
353
                # this masks when a change may have occurred against the basis.
 
354
                # To match this we always issue a delta, because the revision
 
355
                # of the root will always be changing.
302
356
                if ie.file_id in basis_inv:
303
357
                    delta = (basis_inv.id2path(ie.file_id), path,
304
358
                        ie.file_id, ie)
305
359
                else:
306
360
                    # add
307
361
                    delta = (None, path, ie.file_id, ie)
 
362
                self._basis_delta.append(delta)
308
363
                return delta, False, None
309
364
            else:
310
365
                # we don't need to commit this, because the caller already
615
670
        return self._inventory_add_lines(revision_id, parents,
616
671
            inv_lines, check_content=False)
617
672
 
 
673
    def add_inventory_by_delta(self, basis_revision_id, delta, new_revision_id,
 
674
                               parents):
 
675
        """Add a new inventory expressed as a delta against another revision.
 
676
 
 
677
        :param basis_revision_id: The inventory id the delta was created
 
678
            against. (This does not have to be a direct parent.)
 
679
        :param delta: The inventory delta (see Inventory.apply_delta for
 
680
            details).
 
681
        :param new_revision_id: The revision id that the inventory is being
 
682
            added for.
 
683
        :param parents: The revision ids of the parents that revision_id is
 
684
            known to have and are in the repository already. These are supplied
 
685
            for repositories that depend on the inventory graph for revision
 
686
            graph access, as well as for those that pun ancestry with delta
 
687
            compression.
 
688
 
 
689
        :returns: (validator, new_inv)
 
690
            The validator(which is a sha1 digest, though what is sha'd is
 
691
            repository format specific) of the serialized inventory, and the
 
692
            resulting inventory.
 
693
        """
 
694
        if not self.is_in_write_group():
 
695
            raise AssertionError("%r not in write group" % (self,))
 
696
        _mod_revision.check_not_reserved_id(new_revision_id)
 
697
        basis_tree = self.revision_tree(basis_revision_id)
 
698
        basis_tree.lock_read()
 
699
        try:
 
700
            # Note that this mutates the inventory of basis_tree, which not all
 
701
            # inventory implementations may support: A better idiom would be to
 
702
            # return a new inventory, but as there is no revision tree cache in
 
703
            # repository this is safe for now - RBC 20081013
 
704
            basis_inv = basis_tree.inventory
 
705
            basis_inv.apply_delta(delta)
 
706
            basis_inv.revision_id = new_revision_id
 
707
            return (self.add_inventory(new_revision_id, basis_inv, parents),
 
708
                    basis_inv)
 
709
        finally:
 
710
            basis_tree.unlock()
 
711
 
618
712
    def _inventory_add_lines(self, revision_id, parents, lines,
619
713
        check_content=True):
620
714
        """Store lines in inv_vf and return the sha1 of the inventory."""
3082
3176
            return False
3083
3177
        return True
3084
3178
 
 
3179
    def _fetch_batch(self, revision_ids, basis_id, basis_tree):
 
3180
        """Fetch across a few revisions.
 
3181
 
 
3182
        :param revision_ids: The revisions to copy
 
3183
        :param basis_id: The revision_id of basis_tree
 
3184
        :param basis_tree: A tree that is not in revision_ids which should
 
3185
            already exist in the target.
 
3186
        :return: (basis_id, basis_tree) A new basis to use now that these trees
 
3187
            have been copied.
 
3188
        """
 
3189
        # Walk though all revisions; get inventory deltas, copy referenced
 
3190
        # texts that delta references, insert the delta, revision and
 
3191
        # signature.
 
3192
        text_keys = set()
 
3193
        pending_deltas = []
 
3194
        pending_revisions = []
 
3195
        for tree in self.source.revision_trees(revision_ids):
 
3196
            current_revision_id = tree.get_revision_id()
 
3197
            delta = tree.inventory._make_delta(basis_tree.inventory)
 
3198
            for old_path, new_path, file_id, entry in delta:
 
3199
                if new_path is not None:
 
3200
                    if not (new_path or self.target.supports_rich_root()):
 
3201
                        # We leave the inventory delta in, because that
 
3202
                        # will have the deserialised inventory root
 
3203
                        # pointer.
 
3204
                        continue
 
3205
                    # TODO: Do we need:
 
3206
                    #       "if entry.revision == current_revision_id" ?
 
3207
                    if entry.revision == current_revision_id:
 
3208
                        text_keys.add((file_id, entry.revision))
 
3209
            revision = self.source.get_revision(current_revision_id)
 
3210
            pending_deltas.append((basis_id, delta,
 
3211
                current_revision_id, revision.parent_ids))
 
3212
            pending_revisions.append(revision)
 
3213
            basis_id = current_revision_id
 
3214
            basis_tree = tree
 
3215
        # Copy file texts
 
3216
        from_texts = self.source.texts
 
3217
        to_texts = self.target.texts
 
3218
        to_texts.insert_record_stream(from_texts.get_record_stream(
 
3219
            text_keys, self.target._fetch_order,
 
3220
            not self.target._fetch_uses_deltas))
 
3221
        # insert deltas
 
3222
        for delta in pending_deltas:
 
3223
            self.target.add_inventory_by_delta(*delta)
 
3224
        # insert signatures and revisions
 
3225
        for revision in pending_revisions:
 
3226
            try:
 
3227
                signature = self.source.get_signature_text(
 
3228
                    revision.revision_id)
 
3229
                self.target.add_signature_text(revision.revision_id,
 
3230
                    signature)
 
3231
            except errors.NoSuchRevision:
 
3232
                pass
 
3233
            self.target.add_revision(revision.revision_id, revision)
 
3234
        return basis_id, basis_tree
 
3235
 
 
3236
    def _fetch_all_revisions(self, revision_ids, pb):
 
3237
        """Fetch everything for the list of revisions.
 
3238
 
 
3239
        :param revision_ids: The list of revisions to fetch. Must be in
 
3240
            topological order.
 
3241
        :param pb: A ProgressBar
 
3242
        :return: None
 
3243
        """
 
3244
        basis_id, basis_tree = self._get_basis(revision_ids[0])
 
3245
        batch_size = 100
 
3246
        for offset in range(0, len(revision_ids), batch_size):
 
3247
            self.target.start_write_group()
 
3248
            try:
 
3249
                pb.update('Transferring revisions', offset,
 
3250
                          len(revision_ids))
 
3251
                batch = revision_ids[offset:offset+batch_size]
 
3252
                basis_id, basis_tree = self._fetch_batch(batch,
 
3253
                    basis_id, basis_tree)
 
3254
            except:
 
3255
                self.target.abort_write_group()
 
3256
                raise
 
3257
            else:
 
3258
                self.target.commit_write_group()
 
3259
        pb.update('Transferring revisions', len(revision_ids),
 
3260
                  len(revision_ids))
 
3261
 
3085
3262
    @needs_write_lock
3086
3263
    def fetch(self, revision_id=None, pb=None, find_ghosts=False):
3087
3264
        """See InterRepository.fetch()."""
3088
3265
        revision_ids = self.target.search_missing_revision_ids(self.source,
3089
3266
            revision_id, find_ghosts=find_ghosts).get_keys()
 
3267
        if not revision_ids:
 
3268
            return 0, 0
3090
3269
        revision_ids = tsort.topo_sort(
3091
3270
            self.source.get_graph().get_parent_map(revision_ids))
3092
 
        def revisions_iterator():
3093
 
            rev_ids = list(revision_ids)
3094
 
            for offset in xrange(0, len(rev_ids), 100):
3095
 
                current_revids = rev_ids[offset:offset+100]
3096
 
                revisions = self.source.get_revisions(current_revids)
3097
 
                trees = self.source.revision_trees(current_revids)
3098
 
                keys = [(r,) for r in current_revids]
3099
 
                sig_stream = self.source.signatures.get_record_stream(
3100
 
                    keys, 'unordered', True)
3101
 
                sigs = {}
3102
 
                for record in versionedfile.filter_absent(sig_stream):
3103
 
                    sigs[record.key[0]] = record.get_bytes_as('fulltext')
3104
 
                for rev, tree in zip(revisions, trees):
3105
 
                    yield rev, tree, sigs.get(rev.revision_id, None)
3106
3271
        if pb is None:
3107
3272
            my_pb = ui.ui_factory.nested_progress_bar()
3108
3273
            pb = my_pb
3109
3274
        else:
3110
3275
            my_pb = None
3111
3276
        try:
3112
 
            install_revisions(self.target, revisions_iterator(),
3113
 
                              len(revision_ids), pb)
 
3277
            self._fetch_all_revisions(revision_ids, pb)
3114
3278
        finally:
3115
3279
            if my_pb is not None:
3116
3280
                my_pb.finished()
3117
3281
        return len(revision_ids), 0
3118
3282
 
 
3283
    def _get_basis(self, first_revision_id):
 
3284
        """Get a revision and tree which exists in the target.
 
3285
 
 
3286
        This assumes that first_revision_id is selected for transmission
 
3287
        because all other ancestors are already present. If we can't find an
 
3288
        ancestor we fall back to NULL_REVISION since we know that is safe.
 
3289
 
 
3290
        :return: (basis_id, basis_tree)
 
3291
        """
 
3292
        first_rev = self.source.get_revision(first_revision_id)
 
3293
        try:
 
3294
            basis_id = first_rev.parent_ids[0]
 
3295
            # only valid as a basis if the target has it
 
3296
            self.target.get_revision(basis_id)
 
3297
            # Try to get a basis tree - if its a ghost it will hit the
 
3298
            # NoSuchRevision case.
 
3299
            basis_tree = self.source.revision_tree(basis_id)
 
3300
        except (IndexError, errors.NoSuchRevision):
 
3301
            basis_id = _mod_revision.NULL_REVISION
 
3302
            basis_tree = self.source.revision_tree(basis_id)
 
3303
        return basis_id, basis_tree
 
3304
 
3119
3305
 
3120
3306
class InterOtherToRemote(InterRepository):
3121
3307
    """An InterRepository that simply delegates to the 'real' InterRepository