/breezy/trunk

To get this branch, use:
bzr branch https://code.breezy-vcs.org/breezy/trunk

« back to all changes in this revision

Viewing changes to breezy/bzr/vf_repository.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

Show diffs side-by-side

added added

removed removed

Lines of Context:
112
112
    """Commit builder implementation for versioned files based repositories.
113
113
    """
114
114
 
115
 
    # this commit builder supports the record_entry_contents interface
116
 
    supports_record_entry_contents = True
117
 
 
118
115
    # the default CommitBuilder does not manage trees whose root is versioned.
119
116
    _versioned_root = False
120
117
 
129
126
        except IndexError:
130
127
            basis_id = _mod_revision.NULL_REVISION
131
128
        self.basis_delta_revision = basis_id
132
 
        self.new_inventory = Inventory(None)
 
129
        self._new_inventory = None
133
130
        self._basis_delta = []
134
131
        self.__heads = graph.HeadsCache(repository.get_graph()).heads
135
132
        # memo'd check for no-op commits.
136
133
        self._any_changes = False
137
 
        # API compatibility, older code that used CommitBuilder did not call
138
 
        # .record_delete(), which means the delta that is computed would not be
139
 
        # valid. Callers that will call record_delete() should call
140
 
        # .will_record_deletes() to indicate that.
141
 
        self._recording_deletes = False
142
 
 
143
 
    def will_record_deletes(self):
144
 
        """Tell the commit builder that deletes are being notified.
145
 
 
146
 
        This enables the accumulation of an inventory delta; for the resulting
147
 
        commit to be valid, deletes against the basis MUST be recorded via
148
 
        builder.record_delete().
149
 
        """
150
 
        self._recording_deletes = True
151
134
 
152
135
    def any_changes(self):
153
136
        """Return True if any entries were changed.
234
217
        require deserializing the inventory, while we already have a copy in
235
218
        memory.
236
219
        """
237
 
        if self.new_inventory is None:
238
 
            self.new_inventory = self.repository.get_inventory(
 
220
        if self._new_inventory is None:
 
221
            self._new_inventory = self.repository.get_inventory(
239
222
                self._new_revision_id)
240
223
        return inventorytree.InventoryRevisionTree(self.repository,
241
 
            self.new_inventory, self._new_revision_id)
 
224
            self._new_inventory, self._new_revision_id)
242
225
 
243
226
    def finish_inventory(self):
244
227
        """Tell the builder that the inventory is finished.
246
229
        :return: The inventory id in the repository, which can be used with
247
230
            repository.get_inventory.
248
231
        """
249
 
        if self.new_inventory is None:
250
 
            # an inventory delta was accumulated without creating a new
251
 
            # inventory.
252
 
            basis_id = self.basis_delta_revision
253
 
            # We ignore the 'inventory' returned by add_inventory_by_delta
254
 
            # because self.new_inventory is used to hint to the rest of the
255
 
            # system what code path was taken
256
 
            self.inv_sha1, _ = self.repository.add_inventory_by_delta(
257
 
                basis_id, self._basis_delta, self._new_revision_id,
258
 
                self.parents)
259
 
        else:
260
 
            if self.new_inventory.root is None:
261
 
                raise AssertionError('Root entry should be supplied to'
262
 
                    ' record_entry_contents, as of bzr 0.10.')
263
 
                self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
264
 
            self.new_inventory.revision_id = self._new_revision_id
265
 
            self.inv_sha1 = self.repository.add_inventory(
266
 
                self._new_revision_id,
267
 
                self.new_inventory,
268
 
                self.parents
269
 
                )
 
232
        # an inventory delta was accumulated without creating a new
 
233
        # inventory.
 
234
        basis_id = self.basis_delta_revision
 
235
        self.inv_sha1, self._new_inventory = self.repository.add_inventory_by_delta(
 
236
            basis_id, self._basis_delta, self._new_revision_id,
 
237
            self.parents)
270
238
        return self._new_revision_id
271
239
 
272
 
    def _check_root(self, ie, parent_invs, tree):
273
 
        """Helper for record_entry_contents.
274
 
 
275
 
        :param ie: An entry being added.
276
 
        :param parent_invs: The inventories of the parent revisions of the
277
 
            commit.
278
 
        :param tree: The tree that is being committed.
279
 
        """
280
 
        # In this revision format, root entries have no knit or weave When
281
 
        # serializing out to disk and back in root.revision is always
282
 
        # _new_revision_id
283
 
        ie.revision = self._new_revision_id
284
 
 
285
240
    def _require_root_change(self, tree):
286
241
        """Enforce an appropriate root object change.
287
242
 
325
280
    def get_basis_delta(self):
326
281
        """Return the complete inventory delta versus the basis inventory.
327
282
 
328
 
        This has been built up with the calls to record_delete and
329
 
        record_entry_contents. The client must have already called
330
 
        will_record_deletes() to indicate that they will be generating a
331
 
        complete delta.
332
 
 
333
283
        :return: An inventory delta, suitable for use with apply_delta, or
334
284
            Repository.add_inventory_by_delta, etc.
335
285
        """
336
 
        if not self._recording_deletes:
337
 
            raise AssertionError("recording deletes not activated.")
338
286
        return self._basis_delta
339
287
 
340
 
    def record_delete(self, path, file_id):
341
 
        """Record that a delete occured against a basis tree.
342
 
 
343
 
        This is an optional API - when used it adds items to the basis_delta
344
 
        being accumulated by the commit builder. It cannot be called unless the
345
 
        method will_record_deletes() has been called to inform the builder that
346
 
        a delta is being supplied.
347
 
 
348
 
        :param path: The path of the thing deleted.
349
 
        :param file_id: The file id that was deleted.
350
 
        """
351
 
        if not self._recording_deletes:
352
 
            raise AssertionError("recording deletes not activated.")
353
 
        delta = (path, None, file_id, None)
354
 
        self._basis_delta.append(delta)
355
 
        self._any_changes = True
356
 
        return delta
357
 
 
358
 
    def record_entry_contents(self, ie, parent_invs, path, tree,
359
 
        content_summary):
360
 
        """Record the content of ie from tree into the commit if needed.
361
 
 
362
 
        Side effect: sets ie.revision when unchanged
363
 
 
364
 
        :param ie: An inventory entry present in the commit.
365
 
        :param parent_invs: The inventories of the parent revisions of the
366
 
            commit.
367
 
        :param path: The path the entry is at in the tree.
368
 
        :param tree: The tree which contains this entry and should be used to
369
 
            obtain content.
370
 
        :param content_summary: Summary data from the tree about the paths
371
 
            content - stat, length, exec, sha/link target. This is only
372
 
            accessed when the entry has a revision of None - that is when it is
373
 
            a candidate to commit.
374
 
        :return: A tuple (change_delta, version_recorded, fs_hash).
375
 
            change_delta is an inventory_delta change for this entry against
376
 
            the basis tree of the commit, or None if no change occured against
377
 
            the basis tree.
378
 
            version_recorded is True if a new version of the entry has been
379
 
            recorded. For instance, committing a merge where a file was only
380
 
            changed on the other side will return (delta, False).
381
 
            fs_hash is either None, or the hash details for the path (currently
382
 
            a tuple of the contents sha1 and the statvalue returned by
383
 
            tree.get_file_with_stat()).
384
 
        """
385
 
        if self.new_inventory.root is None:
386
 
            if ie.parent_id is not None:
387
 
                raise errors.RootMissing()
388
 
            self._check_root(ie, parent_invs, tree)
389
 
        if ie.revision is None:
390
 
            kind = content_summary[0]
391
 
        else:
392
 
            # ie is carried over from a prior commit
393
 
            kind = ie.kind
394
 
        # XXX: repository specific check for nested tree support goes here - if
395
 
        # the repo doesn't want nested trees we skip it ?
396
 
        if (kind == 'tree-reference' and
397
 
            not self.repository._format.supports_tree_reference):
398
 
            # mismatch between commit builder logic and repository:
399
 
            # this needs the entry creation pushed down into the builder.
400
 
            raise NotImplementedError('Missing repository subtree support.')
401
 
        self.new_inventory.add(ie)
402
 
 
403
 
        # TODO: slow, take it out of the inner loop.
404
 
        try:
405
 
            basis_inv = parent_invs[0]
406
 
        except IndexError:
407
 
            basis_inv = Inventory(root_id=None)
408
 
 
409
 
        # ie.revision is always None if the InventoryEntry is considered
410
 
        # for committing. We may record the previous parents revision if the
411
 
        # content is actually unchanged against a sole head.
412
 
        if ie.revision is not None:
413
 
            if not self._versioned_root and path == '':
414
 
                # repositories that do not version the root set the root's
415
 
                # revision to the new commit even when no change occurs (more
416
 
                # specifically, they do not record a revision on the root; and
417
 
                # the rev id is assigned to the root during deserialisation -
418
 
                # this masks when a change may have occurred against the basis.
419
 
                # To match this we always issue a delta, because the revision
420
 
                # of the root will always be changing.
421
 
                if basis_inv.has_id(ie.file_id):
422
 
                    delta = (basis_inv.id2path(ie.file_id), path,
423
 
                        ie.file_id, ie)
424
 
                else:
425
 
                    # add
426
 
                    delta = (None, path, ie.file_id, ie)
427
 
                self._basis_delta.append(delta)
428
 
                return delta, False, None
429
 
            else:
430
 
                # we don't need to commit this, because the caller already
431
 
                # determined that an existing revision of this file is
432
 
                # appropriate. If it's not being considered for committing then
433
 
                # it and all its parents to the root must be unaltered so
434
 
                # no-change against the basis.
435
 
                if ie.revision == self._new_revision_id:
436
 
                    raise AssertionError("Impossible situation, a skipped "
437
 
                        "inventory entry (%r) claims to be modified in this "
438
 
                        "commit (%r).", (ie, self._new_revision_id))
439
 
                return None, False, None
440
 
        # XXX: Friction: parent_candidates should return a list not a dict
441
 
        #      so that we don't have to walk the inventories again.
442
 
        parent_candidate_entries = ie.parent_candidates(parent_invs)
443
 
        head_set = self._heads(ie.file_id, parent_candidate_entries)
444
 
        heads = []
445
 
        for inv in parent_invs:
446
 
            if inv.has_id(ie.file_id):
447
 
                old_rev = inv[ie.file_id].revision
448
 
                if old_rev in head_set:
449
 
                    heads.append(inv[ie.file_id].revision)
450
 
                    head_set.remove(inv[ie.file_id].revision)
451
 
 
452
 
        store = False
453
 
        # now we check to see if we need to write a new record to the
454
 
        # file-graph.
455
 
        # We write a new entry unless there is one head to the ancestors, and
456
 
        # the kind-derived content is unchanged.
457
 
 
458
 
        # Cheapest check first: no ancestors, or more the one head in the
459
 
        # ancestors, we write a new node.
460
 
        if len(heads) != 1:
461
 
            store = True
462
 
        if not store:
463
 
            # There is a single head, look it up for comparison
464
 
            parent_entry = parent_candidate_entries[heads[0]]
465
 
            # if the non-content specific data has changed, we'll be writing a
466
 
            # node:
467
 
            if (parent_entry.parent_id != ie.parent_id or
468
 
                parent_entry.name != ie.name):
469
 
                store = True
470
 
        # now we need to do content specific checks:
471
 
        if not store:
472
 
            # if the kind changed the content obviously has
473
 
            if kind != parent_entry.kind:
474
 
                store = True
475
 
        # Stat cache fingerprint feedback for the caller - None as we usually
476
 
        # don't generate one.
477
 
        fingerprint = None
478
 
        if kind == 'file':
479
 
            if content_summary[2] is None:
480
 
                raise ValueError("Files must not have executable = None")
481
 
            if not store:
482
 
                # We can't trust a check of the file length because of content
483
 
                # filtering...
484
 
                if (# if the exec bit has changed we have to store:
485
 
                    parent_entry.executable != content_summary[2]):
486
 
                    store = True
487
 
                elif parent_entry.text_sha1 == content_summary[3]:
488
 
                    # all meta and content is unchanged (using a hash cache
489
 
                    # hit to check the sha)
490
 
                    ie.revision = parent_entry.revision
491
 
                    ie.text_size = parent_entry.text_size
492
 
                    ie.text_sha1 = parent_entry.text_sha1
493
 
                    ie.executable = parent_entry.executable
494
 
                    return self._get_delta(ie, basis_inv, path), False, None
495
 
                else:
496
 
                    # Either there is only a hash change(no hash cache entry,
497
 
                    # or same size content change), or there is no change on
498
 
                    # this file at all.
499
 
                    # Provide the parent's hash to the store layer, so that the
500
 
                    # content is unchanged we will not store a new node.
501
 
                    nostore_sha = parent_entry.text_sha1
502
 
            if store:
503
 
                # We want to record a new node regardless of the presence or
504
 
                # absence of a content change in the file.
505
 
                nostore_sha = None
506
 
            ie.executable = content_summary[2]
507
 
            file_obj, stat_value = tree.get_file_with_stat(ie.file_id, path)
508
 
            try:
509
 
                text = file_obj.read()
510
 
            finally:
511
 
                file_obj.close()
512
 
            try:
513
 
                ie.text_sha1, ie.text_size = self._add_text_to_weave(
514
 
                    ie.file_id, text, heads, nostore_sha)
515
 
                # Let the caller know we generated a stat fingerprint.
516
 
                fingerprint = (ie.text_sha1, stat_value)
517
 
            except errors.ExistingContent:
518
 
                # Turns out that the file content was unchanged, and we were
519
 
                # only going to store a new node if it was changed. Carry over
520
 
                # the entry.
521
 
                ie.revision = parent_entry.revision
522
 
                ie.text_size = parent_entry.text_size
523
 
                ie.text_sha1 = parent_entry.text_sha1
524
 
                ie.executable = parent_entry.executable
525
 
                return self._get_delta(ie, basis_inv, path), False, None
526
 
        elif kind == 'directory':
527
 
            if not store:
528
 
                # all data is meta here, nothing specific to directory, so
529
 
                # carry over:
530
 
                ie.revision = parent_entry.revision
531
 
                return self._get_delta(ie, basis_inv, path), False, None
532
 
            self._add_text_to_weave(ie.file_id, '', heads, None)
533
 
        elif kind == 'symlink':
534
 
            current_link_target = content_summary[3]
535
 
            if not store:
536
 
                # symlink target is not generic metadata, check if it has
537
 
                # changed.
538
 
                if current_link_target != parent_entry.symlink_target:
539
 
                    store = True
540
 
            if not store:
541
 
                # unchanged, carry over.
542
 
                ie.revision = parent_entry.revision
543
 
                ie.symlink_target = parent_entry.symlink_target
544
 
                return self._get_delta(ie, basis_inv, path), False, None
545
 
            ie.symlink_target = current_link_target
546
 
            self._add_text_to_weave(ie.file_id, '', heads, None)
547
 
        elif kind == 'tree-reference':
548
 
            if not store:
549
 
                if content_summary[3] != parent_entry.reference_revision:
550
 
                    store = True
551
 
            if not store:
552
 
                # unchanged, carry over.
553
 
                ie.reference_revision = parent_entry.reference_revision
554
 
                ie.revision = parent_entry.revision
555
 
                return self._get_delta(ie, basis_inv, path), False, None
556
 
            ie.reference_revision = content_summary[3]
557
 
            if ie.reference_revision is None:
558
 
                raise AssertionError("invalid content_summary for nested tree: %r"
559
 
                    % (content_summary,))
560
 
            self._add_text_to_weave(ie.file_id, '', heads, None)
561
 
        else:
562
 
            raise NotImplementedError('unknown kind')
563
 
        ie.revision = self._new_revision_id
564
 
        # The initial commit adds a root directory, but this in itself is not
565
 
        # a worthwhile commit.
566
 
        if (self.basis_delta_revision != _mod_revision.NULL_REVISION or
567
 
            path != ""):
568
 
            self._any_changes = True
569
 
        return self._get_delta(ie, basis_inv, path), True, fingerprint
570
 
 
571
288
    def record_iter_changes(self, tree, basis_revision_id, iter_changes,
572
289
        _entry_factory=entry_factory):
573
290
        """Record a new tree via iter_changes.
827
544
            inv_delta.append((change[1][0], new_path, change[0], entry))
828
545
            if new_path == '':
829
546
                seen_root = True
830
 
        self.new_inventory = None
831
547
        # The initial commit adds a root directory, but this in itself is not
832
548
        # a worthwhile commit.
833
549
        if ((len(inv_delta) > 0 and basis_revision_id != _mod_revision.NULL_REVISION) or
854
570
    # the root entry gets versioned properly by this builder.
855
571
    _versioned_root = True
856
572
 
857
 
    def _check_root(self, ie, parent_invs, tree):
858
 
        """Helper for record_entry_contents.
859
 
 
860
 
        :param ie: An entry being added.
861
 
        :param parent_invs: The inventories of the parent revisions of the
862
 
            commit.
863
 
        :param tree: The tree that is being committed.
864
 
        """
865
 
 
866
573
    def _require_root_change(self, tree):
867
574
        """Enforce an appropriate root object change.
868
575
 
1395
1102
        be used by reconcile, or reconcile-alike commands that are correcting
1396
1103
        or testing the revision graph.
1397
1104
        """
1398
 
        return self._get_revisions([revision_id])[0]
1399
 
 
1400
 
    @needs_read_lock
1401
 
    def get_revisions(self, revision_ids):
1402
 
        """Get many revisions at once.
1403
 
        
1404
 
        Repositories that need to check data on every revision read should 
1405
 
        subclass this method.
1406
 
        """
1407
 
        return self._get_revisions(revision_ids)
1408
 
 
1409
 
    @needs_read_lock
1410
 
    def _get_revisions(self, revision_ids):
1411
 
        """Core work logic to get many revisions without sanity checks."""
1412
 
        revs = {}
1413
 
        for revid, rev in self._iter_revisions(revision_ids):
1414
 
            if rev is None:
1415
 
                raise errors.NoSuchRevision(self, revid)
1416
 
            revs[revid] = rev
1417
 
        return [revs[revid] for revid in revision_ids]
1418
 
 
1419
 
    def _iter_revisions(self, revision_ids):
 
1105
        return self.get_revisions([revision_id])[0]
 
1106
 
 
1107
    def iter_revisions(self, revision_ids):
1420
1108
        """Iterate over revision objects.
1421
1109
 
1422
1110
        :param revision_ids: An iterable of revisions to examine. None may be
1426
1114
        :return: An iterator of (revid, revision) tuples. Absent revisions (
1427
1115
            those asked for but not available) are returned as (revid, None).
1428
1116
        """
1429
 
        if revision_ids is None:
1430
 
            revision_ids = self.all_revision_ids()
1431
 
        else:
 
1117
        self.lock_read()
 
1118
        try:
1432
1119
            for rev_id in revision_ids:
1433
 
                if not rev_id or not isinstance(rev_id, basestring):
 
1120
                if not rev_id or not isinstance(rev_id, bytes):
1434
1121
                    raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
1435
 
        keys = [(key,) for key in revision_ids]
1436
 
        stream = self.revisions.get_record_stream(keys, 'unordered', True)
1437
 
        for record in stream:
1438
 
            revid = record.key[0]
1439
 
            if record.storage_kind == 'absent':
1440
 
                yield (revid, None)
1441
 
            else:
1442
 
                text = record.get_bytes_as('fulltext')
1443
 
                rev = self._serializer.read_revision_from_string(text)
1444
 
                yield (revid, rev)
 
1122
            keys = [(key,) for key in revision_ids]
 
1123
            stream = self.revisions.get_record_stream(keys, 'unordered', True)
 
1124
            for record in stream:
 
1125
                revid = record.key[0]
 
1126
                if record.storage_kind == 'absent':
 
1127
                    yield (revid, None)
 
1128
                else:
 
1129
                    text = record.get_bytes_as('fulltext')
 
1130
                    rev = self._serializer.read_revision_from_string(text)
 
1131
                    yield (revid, rev)
 
1132
        finally:
 
1133
            self.unlock()
1445
1134
 
1446
1135
    @needs_write_lock
1447
1136
    def add_signature_text(self, revision_id, signature):
1973
1662
            raise AssertionError()
1974
1663
        vf = self.revisions
1975
1664
        if revisions_iterator is None:
1976
 
            revisions_iterator = self._iter_revisions(None)
 
1665
            revisions_iterator = self.iter_revisions(self.all_revision_ids())
1977
1666
        for revid, revision in revisions_iterator:
1978
1667
            if revision is None:
1979
1668
                pass