/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/vf_repository.py

  • Committer: Jelmer Vernooij
  • Date: 2011-05-02 17:07:16 UTC
  • mto: This revision was merged to the branch mainline in revision 5844.
  • Revision ID: jelmer@samba.org-20110502170716-mp5l2k4l4m5b3cw6
split out versionedfile-specific stuff from commitbuilder.

Show diffs side-by-side

added added

removed removed

Lines of Context:
51
51
    )
52
52
from bzrlib.inventory import (
53
53
    Inventory,
 
54
    InventoryDirectory,
 
55
    ROOT_ID,
 
56
    entry_factory,
54
57
    )
55
58
 
56
59
from bzrlib.repository import (
 
60
    CommitBuilder,
57
61
    InterRepository,
58
62
    MetaDirRepository,
59
63
    Repository,
64
68
    )
65
69
 
66
70
 
 
71
class VersionedFileCommitBuilder(CommitBuilder):
 
72
    """Commit builder implementation for versioned files based repositories.
 
73
    """
 
74
 
 
75
    # this commit builder supports the record_entry_contents interface
 
76
    supports_record_entry_contents = True
 
77
 
 
78
    # the default CommitBuilder does not manage trees whose root is versioned.
 
79
    _versioned_root = False
 
80
 
 
81
    def __init__(self, repository, parents, config, timestamp=None,
 
82
                 timezone=None, committer=None, revprops=None,
 
83
                 revision_id=None, lossy=False):
 
84
        super(VersionedFileCommitBuilder, self).__init__(repository,
 
85
            parents, config, timestamp, timezone, committer, revprops,
 
86
            revision_id, lossy)
 
87
        self.new_inventory = Inventory(None)
 
88
        self._basis_delta = []
 
89
        self.__heads = graph.HeadsCache(repository.get_graph()).heads
 
90
        # memo'd check for no-op commits.
 
91
        self._any_changes = False
 
92
        # API compatibility, older code that used CommitBuilder did not call
 
93
        # .record_delete(), which means the delta that is computed would not be
 
94
        # valid. Callers that will call record_delete() should call
 
95
        # .will_record_deletes() to indicate that.
 
96
        self._recording_deletes = False
 
97
 
 
98
    def will_record_deletes(self):
 
99
        """Tell the commit builder that deletes are being notified.
 
100
 
 
101
        This enables the accumulation of an inventory delta; for the resulting
 
102
        commit to be valid, deletes against the basis MUST be recorded via
 
103
        builder.record_delete().
 
104
        """
 
105
        self._recording_deletes = True
 
106
        try:
 
107
            basis_id = self.parents[0]
 
108
        except IndexError:
 
109
            basis_id = _mod_revision.NULL_REVISION
 
110
        self.basis_delta_revision = basis_id
 
111
 
 
112
    def any_changes(self):
 
113
        """Return True if any entries were changed.
 
114
 
 
115
        This includes merge-only changes. It is the core for the --unchanged
 
116
        detection in commit.
 
117
 
 
118
        :return: True if any changes have occured.
 
119
        """
 
120
        return self._any_changes
 
121
 
 
122
    def _ensure_fallback_inventories(self):
 
123
        """Ensure that appropriate inventories are available.
 
124
 
 
125
        This only applies to repositories that are stacked, and is about
 
126
        enusring the stacking invariants. Namely, that for any revision that is
 
127
        present, we either have all of the file content, or we have the parent
 
128
        inventory and the delta file content.
 
129
        """
 
130
        if not self.repository._fallback_repositories:
 
131
            return
 
132
        if not self.repository._format.supports_chks:
 
133
            raise errors.BzrError("Cannot commit directly to a stacked branch"
 
134
                " in pre-2a formats. See "
 
135
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
 
136
        # This is a stacked repo, we need to make sure we have the parent
 
137
        # inventories for the parents.
 
138
        parent_keys = [(p,) for p in self.parents]
 
139
        parent_map = self.repository.inventories._index.get_parent_map(parent_keys)
 
140
        missing_parent_keys = set([pk for pk in parent_keys
 
141
                                       if pk not in parent_map])
 
142
        fallback_repos = list(reversed(self.repository._fallback_repositories))
 
143
        missing_keys = [('inventories', pk[0])
 
144
                        for pk in missing_parent_keys]
 
145
        resume_tokens = []
 
146
        while missing_keys and fallback_repos:
 
147
            fallback_repo = fallback_repos.pop()
 
148
            source = fallback_repo._get_source(self.repository._format)
 
149
            sink = self.repository._get_sink()
 
150
            stream = source.get_stream_for_missing_keys(missing_keys)
 
151
            missing_keys = sink.insert_stream_without_locking(stream,
 
152
                self.repository._format)
 
153
        if missing_keys:
 
154
            raise errors.BzrError('Unable to fill in parent inventories for a'
 
155
                                  ' stacked branch')
 
156
 
 
157
    def commit(self, message):
 
158
        """Make the actual commit.
 
159
 
 
160
        :return: The revision id of the recorded revision.
 
161
        """
 
162
        self._validate_unicode_text(message, 'commit message')
 
163
        rev = _mod_revision.Revision(
 
164
                       timestamp=self._timestamp,
 
165
                       timezone=self._timezone,
 
166
                       committer=self._committer,
 
167
                       message=message,
 
168
                       inventory_sha1=self.inv_sha1,
 
169
                       revision_id=self._new_revision_id,
 
170
                       properties=self._revprops)
 
171
        rev.parent_ids = self.parents
 
172
        self.repository.add_revision(self._new_revision_id, rev,
 
173
            self.new_inventory, self._config)
 
174
        self._ensure_fallback_inventories()
 
175
        self.repository.commit_write_group()
 
176
        return self._new_revision_id
 
177
 
 
178
    def abort(self):
 
179
        """Abort the commit that is being built.
 
180
        """
 
181
        self.repository.abort_write_group()
 
182
 
 
183
    def revision_tree(self):
 
184
        """Return the tree that was just committed.
 
185
 
 
186
        After calling commit() this can be called to get a
 
187
        RevisionTree representing the newly committed tree. This is
 
188
        preferred to calling Repository.revision_tree() because that may
 
189
        require deserializing the inventory, while we already have a copy in
 
190
        memory.
 
191
        """
 
192
        if self.new_inventory is None:
 
193
            self.new_inventory = self.repository.get_inventory(
 
194
                self._new_revision_id)
 
195
        return InventoryRevisionTree(self.repository, self.new_inventory,
 
196
            self._new_revision_id)
 
197
 
 
198
    def finish_inventory(self):
 
199
        """Tell the builder that the inventory is finished.
 
200
 
 
201
        :return: The inventory id in the repository, which can be used with
 
202
            repository.get_inventory.
 
203
        """
 
204
        if self.new_inventory is None:
 
205
            # an inventory delta was accumulated without creating a new
 
206
            # inventory.
 
207
            basis_id = self.basis_delta_revision
 
208
            # We ignore the 'inventory' returned by add_inventory_by_delta
 
209
            # because self.new_inventory is used to hint to the rest of the
 
210
            # system what code path was taken
 
211
            self.inv_sha1, _ = self.repository.add_inventory_by_delta(
 
212
                basis_id, self._basis_delta, self._new_revision_id,
 
213
                self.parents)
 
214
        else:
 
215
            if self.new_inventory.root is None:
 
216
                raise AssertionError('Root entry should be supplied to'
 
217
                    ' record_entry_contents, as of bzr 0.10.')
 
218
                self.new_inventory.add(InventoryDirectory(ROOT_ID, '', None))
 
219
            self.new_inventory.revision_id = self._new_revision_id
 
220
            self.inv_sha1 = self.repository.add_inventory(
 
221
                self._new_revision_id,
 
222
                self.new_inventory,
 
223
                self.parents
 
224
                )
 
225
        return self._new_revision_id
 
226
 
 
227
    def _check_root(self, ie, parent_invs, tree):
 
228
        """Helper for record_entry_contents.
 
229
 
 
230
        :param ie: An entry being added.
 
231
        :param parent_invs: The inventories of the parent revisions of the
 
232
            commit.
 
233
        :param tree: The tree that is being committed.
 
234
        """
 
235
        # In this revision format, root entries have no knit or weave When
 
236
        # serializing out to disk and back in root.revision is always
 
237
        # _new_revision_id
 
238
        ie.revision = self._new_revision_id
 
239
 
 
240
    def _require_root_change(self, tree):
 
241
        """Enforce an appropriate root object change.
 
242
 
 
243
        This is called once when record_iter_changes is called, if and only if
 
244
        the root was not in the delta calculated by record_iter_changes.
 
245
 
 
246
        :param tree: The tree which is being committed.
 
247
        """
 
248
        if len(self.parents) == 0:
 
249
            raise errors.RootMissing()
 
250
        entry = entry_factory['directory'](tree.path2id(''), '',
 
251
            None)
 
252
        entry.revision = self._new_revision_id
 
253
        self._basis_delta.append(('', '', entry.file_id, entry))
 
254
 
 
255
    def _get_delta(self, ie, basis_inv, path):
 
256
        """Get a delta against the basis inventory for ie."""
 
257
        if ie.file_id not in basis_inv:
 
258
            # add
 
259
            result = (None, path, ie.file_id, ie)
 
260
            self._basis_delta.append(result)
 
261
            return result
 
262
        elif ie != basis_inv[ie.file_id]:
 
263
            # common but altered
 
264
            # TODO: avoid tis id2path call.
 
265
            result = (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
266
            self._basis_delta.append(result)
 
267
            return result
 
268
        else:
 
269
            # common, unaltered
 
270
            return None
 
271
 
 
272
    def _heads(self, file_id, revision_ids):
 
273
        """Calculate the graph heads for revision_ids in the graph of file_id.
 
274
 
 
275
        This can use either a per-file graph or a global revision graph as we
 
276
        have an identity relationship between the two graphs.
 
277
        """
 
278
        return self.__heads(revision_ids)
 
279
 
 
280
    def get_basis_delta(self):
 
281
        """Return the complete inventory delta versus the basis inventory.
 
282
 
 
283
        This has been built up with the calls to record_delete and
 
284
        record_entry_contents. The client must have already called
 
285
        will_record_deletes() to indicate that they will be generating a
 
286
        complete delta.
 
287
 
 
288
        :return: An inventory delta, suitable for use with apply_delta, or
 
289
            Repository.add_inventory_by_delta, etc.
 
290
        """
 
291
        if not self._recording_deletes:
 
292
            raise AssertionError("recording deletes not activated.")
 
293
        return self._basis_delta
 
294
 
 
295
    def record_delete(self, path, file_id):
 
296
        """Record that a delete occured against a basis tree.
 
297
 
 
298
        This is an optional API - when used it adds items to the basis_delta
 
299
        being accumulated by the commit builder. It cannot be called unless the
 
300
        method will_record_deletes() has been called to inform the builder that
 
301
        a delta is being supplied.
 
302
 
 
303
        :param path: The path of the thing deleted.
 
304
        :param file_id: The file id that was deleted.
 
305
        """
 
306
        if not self._recording_deletes:
 
307
            raise AssertionError("recording deletes not activated.")
 
308
        delta = (path, None, file_id, None)
 
309
        self._basis_delta.append(delta)
 
310
        self._any_changes = True
 
311
        return delta
 
312
 
 
313
    def record_entry_contents(self, ie, parent_invs, path, tree,
 
314
        content_summary):
 
315
        """Record the content of ie from tree into the commit if needed.
 
316
 
 
317
        Side effect: sets ie.revision when unchanged
 
318
 
 
319
        :param ie: An inventory entry present in the commit.
 
320
        :param parent_invs: The inventories of the parent revisions of the
 
321
            commit.
 
322
        :param path: The path the entry is at in the tree.
 
323
        :param tree: The tree which contains this entry and should be used to
 
324
            obtain content.
 
325
        :param content_summary: Summary data from the tree about the paths
 
326
            content - stat, length, exec, sha/link target. This is only
 
327
            accessed when the entry has a revision of None - that is when it is
 
328
            a candidate to commit.
 
329
        :return: A tuple (change_delta, version_recorded, fs_hash).
 
330
            change_delta is an inventory_delta change for this entry against
 
331
            the basis tree of the commit, or None if no change occured against
 
332
            the basis tree.
 
333
            version_recorded is True if a new version of the entry has been
 
334
            recorded. For instance, committing a merge where a file was only
 
335
            changed on the other side will return (delta, False).
 
336
            fs_hash is either None, or the hash details for the path (currently
 
337
            a tuple of the contents sha1 and the statvalue returned by
 
338
            tree.get_file_with_stat()).
 
339
        """
 
340
        if self.new_inventory.root is None:
 
341
            if ie.parent_id is not None:
 
342
                raise errors.RootMissing()
 
343
            self._check_root(ie, parent_invs, tree)
 
344
        if ie.revision is None:
 
345
            kind = content_summary[0]
 
346
        else:
 
347
            # ie is carried over from a prior commit
 
348
            kind = ie.kind
 
349
        # XXX: repository specific check for nested tree support goes here - if
 
350
        # the repo doesn't want nested trees we skip it ?
 
351
        if (kind == 'tree-reference' and
 
352
            not self.repository._format.supports_tree_reference):
 
353
            # mismatch between commit builder logic and repository:
 
354
            # this needs the entry creation pushed down into the builder.
 
355
            raise NotImplementedError('Missing repository subtree support.')
 
356
        self.new_inventory.add(ie)
 
357
 
 
358
        # TODO: slow, take it out of the inner loop.
 
359
        try:
 
360
            basis_inv = parent_invs[0]
 
361
        except IndexError:
 
362
            basis_inv = Inventory(root_id=None)
 
363
 
 
364
        # ie.revision is always None if the InventoryEntry is considered
 
365
        # for committing. We may record the previous parents revision if the
 
366
        # content is actually unchanged against a sole head.
 
367
        if ie.revision is not None:
 
368
            if not self._versioned_root and path == '':
 
369
                # repositories that do not version the root set the root's
 
370
                # revision to the new commit even when no change occurs (more
 
371
                # specifically, they do not record a revision on the root; and
 
372
                # the rev id is assigned to the root during deserialisation -
 
373
                # this masks when a change may have occurred against the basis.
 
374
                # To match this we always issue a delta, because the revision
 
375
                # of the root will always be changing.
 
376
                if ie.file_id in basis_inv:
 
377
                    delta = (basis_inv.id2path(ie.file_id), path,
 
378
                        ie.file_id, ie)
 
379
                else:
 
380
                    # add
 
381
                    delta = (None, path, ie.file_id, ie)
 
382
                self._basis_delta.append(delta)
 
383
                return delta, False, None
 
384
            else:
 
385
                # we don't need to commit this, because the caller already
 
386
                # determined that an existing revision of this file is
 
387
                # appropriate. If it's not being considered for committing then
 
388
                # it and all its parents to the root must be unaltered so
 
389
                # no-change against the basis.
 
390
                if ie.revision == self._new_revision_id:
 
391
                    raise AssertionError("Impossible situation, a skipped "
 
392
                        "inventory entry (%r) claims to be modified in this "
 
393
                        "commit (%r).", (ie, self._new_revision_id))
 
394
                return None, False, None
 
395
        # XXX: Friction: parent_candidates should return a list not a dict
 
396
        #      so that we don't have to walk the inventories again.
 
397
        parent_candiate_entries = ie.parent_candidates(parent_invs)
 
398
        head_set = self._heads(ie.file_id, parent_candiate_entries.keys())
 
399
        heads = []
 
400
        for inv in parent_invs:
 
401
            if ie.file_id in inv:
 
402
                old_rev = inv[ie.file_id].revision
 
403
                if old_rev in head_set:
 
404
                    heads.append(inv[ie.file_id].revision)
 
405
                    head_set.remove(inv[ie.file_id].revision)
 
406
 
 
407
        store = False
 
408
        # now we check to see if we need to write a new record to the
 
409
        # file-graph.
 
410
        # We write a new entry unless there is one head to the ancestors, and
 
411
        # the kind-derived content is unchanged.
 
412
 
 
413
        # Cheapest check first: no ancestors, or more the one head in the
 
414
        # ancestors, we write a new node.
 
415
        if len(heads) != 1:
 
416
            store = True
 
417
        if not store:
 
418
            # There is a single head, look it up for comparison
 
419
            parent_entry = parent_candiate_entries[heads[0]]
 
420
            # if the non-content specific data has changed, we'll be writing a
 
421
            # node:
 
422
            if (parent_entry.parent_id != ie.parent_id or
 
423
                parent_entry.name != ie.name):
 
424
                store = True
 
425
        # now we need to do content specific checks:
 
426
        if not store:
 
427
            # if the kind changed the content obviously has
 
428
            if kind != parent_entry.kind:
 
429
                store = True
 
430
        # Stat cache fingerprint feedback for the caller - None as we usually
 
431
        # don't generate one.
 
432
        fingerprint = None
 
433
        if kind == 'file':
 
434
            if content_summary[2] is None:
 
435
                raise ValueError("Files must not have executable = None")
 
436
            if not store:
 
437
                # We can't trust a check of the file length because of content
 
438
                # filtering...
 
439
                if (# if the exec bit has changed we have to store:
 
440
                    parent_entry.executable != content_summary[2]):
 
441
                    store = True
 
442
                elif parent_entry.text_sha1 == content_summary[3]:
 
443
                    # all meta and content is unchanged (using a hash cache
 
444
                    # hit to check the sha)
 
445
                    ie.revision = parent_entry.revision
 
446
                    ie.text_size = parent_entry.text_size
 
447
                    ie.text_sha1 = parent_entry.text_sha1
 
448
                    ie.executable = parent_entry.executable
 
449
                    return self._get_delta(ie, basis_inv, path), False, None
 
450
                else:
 
451
                    # Either there is only a hash change(no hash cache entry,
 
452
                    # or same size content change), or there is no change on
 
453
                    # this file at all.
 
454
                    # Provide the parent's hash to the store layer, so that the
 
455
                    # content is unchanged we will not store a new node.
 
456
                    nostore_sha = parent_entry.text_sha1
 
457
            if store:
 
458
                # We want to record a new node regardless of the presence or
 
459
                # absence of a content change in the file.
 
460
                nostore_sha = None
 
461
            ie.executable = content_summary[2]
 
462
            file_obj, stat_value = tree.get_file_with_stat(ie.file_id, path)
 
463
            try:
 
464
                text = file_obj.read()
 
465
            finally:
 
466
                file_obj.close()
 
467
            try:
 
468
                ie.text_sha1, ie.text_size = self._add_text_to_weave(
 
469
                    ie.file_id, text, heads, nostore_sha)
 
470
                # Let the caller know we generated a stat fingerprint.
 
471
                fingerprint = (ie.text_sha1, stat_value)
 
472
            except errors.ExistingContent:
 
473
                # Turns out that the file content was unchanged, and we were
 
474
                # only going to store a new node if it was changed. Carry over
 
475
                # the entry.
 
476
                ie.revision = parent_entry.revision
 
477
                ie.text_size = parent_entry.text_size
 
478
                ie.text_sha1 = parent_entry.text_sha1
 
479
                ie.executable = parent_entry.executable
 
480
                return self._get_delta(ie, basis_inv, path), False, None
 
481
        elif kind == 'directory':
 
482
            if not store:
 
483
                # all data is meta here, nothing specific to directory, so
 
484
                # carry over:
 
485
                ie.revision = parent_entry.revision
 
486
                return self._get_delta(ie, basis_inv, path), False, None
 
487
            self._add_text_to_weave(ie.file_id, '', heads, None)
 
488
        elif kind == 'symlink':
 
489
            current_link_target = content_summary[3]
 
490
            if not store:
 
491
                # symlink target is not generic metadata, check if it has
 
492
                # changed.
 
493
                if current_link_target != parent_entry.symlink_target:
 
494
                    store = True
 
495
            if not store:
 
496
                # unchanged, carry over.
 
497
                ie.revision = parent_entry.revision
 
498
                ie.symlink_target = parent_entry.symlink_target
 
499
                return self._get_delta(ie, basis_inv, path), False, None
 
500
            ie.symlink_target = current_link_target
 
501
            self._add_text_to_weave(ie.file_id, '', heads, None)
 
502
        elif kind == 'tree-reference':
 
503
            if not store:
 
504
                if content_summary[3] != parent_entry.reference_revision:
 
505
                    store = True
 
506
            if not store:
 
507
                # unchanged, carry over.
 
508
                ie.reference_revision = parent_entry.reference_revision
 
509
                ie.revision = parent_entry.revision
 
510
                return self._get_delta(ie, basis_inv, path), False, None
 
511
            ie.reference_revision = content_summary[3]
 
512
            if ie.reference_revision is None:
 
513
                raise AssertionError("invalid content_summary for nested tree: %r"
 
514
                    % (content_summary,))
 
515
            self._add_text_to_weave(ie.file_id, '', heads, None)
 
516
        else:
 
517
            raise NotImplementedError('unknown kind')
 
518
        ie.revision = self._new_revision_id
 
519
        self._any_changes = True
 
520
        return self._get_delta(ie, basis_inv, path), True, fingerprint
 
521
 
 
522
    def record_iter_changes(self, tree, basis_revision_id, iter_changes,
 
523
        _entry_factory=entry_factory):
 
524
        """Record a new tree via iter_changes.
 
525
 
 
526
        :param tree: The tree to obtain text contents from for changed objects.
 
527
        :param basis_revision_id: The revision id of the tree the iter_changes
 
528
            has been generated against. Currently assumed to be the same
 
529
            as self.parents[0] - if it is not, errors may occur.
 
530
        :param iter_changes: An iter_changes iterator with the changes to apply
 
531
            to basis_revision_id. The iterator must not include any items with
 
532
            a current kind of None - missing items must be either filtered out
 
533
            or errored-on beefore record_iter_changes sees the item.
 
534
        :param _entry_factory: Private method to bind entry_factory locally for
 
535
            performance.
 
536
        :return: A generator of (file_id, relpath, fs_hash) tuples for use with
 
537
            tree._observed_sha1.
 
538
        """
 
539
        # Create an inventory delta based on deltas between all the parents and
 
540
        # deltas between all the parent inventories. We use inventory delta's 
 
541
        # between the inventory objects because iter_changes masks
 
542
        # last-changed-field only changes.
 
543
        # Working data:
 
544
        # file_id -> change map, change is fileid, paths, changed, versioneds,
 
545
        # parents, names, kinds, executables
 
546
        merged_ids = {}
 
547
        # {file_id -> revision_id -> inventory entry, for entries in parent
 
548
        # trees that are not parents[0]
 
549
        parent_entries = {}
 
550
        ghost_basis = False
 
551
        try:
 
552
            revtrees = list(self.repository.revision_trees(self.parents))
 
553
        except errors.NoSuchRevision:
 
554
            # one or more ghosts, slow path.
 
555
            revtrees = []
 
556
            for revision_id in self.parents:
 
557
                try:
 
558
                    revtrees.append(self.repository.revision_tree(revision_id))
 
559
                except errors.NoSuchRevision:
 
560
                    if not revtrees:
 
561
                        basis_revision_id = _mod_revision.NULL_REVISION
 
562
                        ghost_basis = True
 
563
                    revtrees.append(self.repository.revision_tree(
 
564
                        _mod_revision.NULL_REVISION))
 
565
        # The basis inventory from a repository 
 
566
        if revtrees:
 
567
            basis_inv = revtrees[0].inventory
 
568
        else:
 
569
            basis_inv = self.repository.revision_tree(
 
570
                _mod_revision.NULL_REVISION).inventory
 
571
        if len(self.parents) > 0:
 
572
            if basis_revision_id != self.parents[0] and not ghost_basis:
 
573
                raise Exception(
 
574
                    "arbitrary basis parents not yet supported with merges")
 
575
            for revtree in revtrees[1:]:
 
576
                for change in revtree.inventory._make_delta(basis_inv):
 
577
                    if change[1] is None:
 
578
                        # Not present in this parent.
 
579
                        continue
 
580
                    if change[2] not in merged_ids:
 
581
                        if change[0] is not None:
 
582
                            basis_entry = basis_inv[change[2]]
 
583
                            merged_ids[change[2]] = [
 
584
                                # basis revid
 
585
                                basis_entry.revision,
 
586
                                # new tree revid
 
587
                                change[3].revision]
 
588
                            parent_entries[change[2]] = {
 
589
                                # basis parent
 
590
                                basis_entry.revision:basis_entry,
 
591
                                # this parent 
 
592
                                change[3].revision:change[3],
 
593
                                }
 
594
                        else:
 
595
                            merged_ids[change[2]] = [change[3].revision]
 
596
                            parent_entries[change[2]] = {change[3].revision:change[3]}
 
597
                    else:
 
598
                        merged_ids[change[2]].append(change[3].revision)
 
599
                        parent_entries[change[2]][change[3].revision] = change[3]
 
600
        else:
 
601
            merged_ids = {}
 
602
        # Setup the changes from the tree:
 
603
        # changes maps file_id -> (change, [parent revision_ids])
 
604
        changes= {}
 
605
        for change in iter_changes:
 
606
            # This probably looks up in basis_inv way to much.
 
607
            if change[1][0] is not None:
 
608
                head_candidate = [basis_inv[change[0]].revision]
 
609
            else:
 
610
                head_candidate = []
 
611
            changes[change[0]] = change, merged_ids.get(change[0],
 
612
                head_candidate)
 
613
        unchanged_merged = set(merged_ids) - set(changes)
 
614
        # Extend the changes dict with synthetic changes to record merges of
 
615
        # texts.
 
616
        for file_id in unchanged_merged:
 
617
            # Record a merged version of these items that did not change vs the
 
618
            # basis. This can be either identical parallel changes, or a revert
 
619
            # of a specific file after a merge. The recorded content will be
 
620
            # that of the current tree (which is the same as the basis), but
 
621
            # the per-file graph will reflect a merge.
 
622
            # NB:XXX: We are reconstructing path information we had, this
 
623
            # should be preserved instead.
 
624
            # inv delta  change: (file_id, (path_in_source, path_in_target),
 
625
            #   changed_content, versioned, parent, name, kind,
 
626
            #   executable)
 
627
            try:
 
628
                basis_entry = basis_inv[file_id]
 
629
            except errors.NoSuchId:
 
630
                # a change from basis->some_parents but file_id isn't in basis
 
631
                # so was new in the merge, which means it must have changed
 
632
                # from basis -> current, and as it hasn't the add was reverted
 
633
                # by the user. So we discard this change.
 
634
                pass
 
635
            else:
 
636
                change = (file_id,
 
637
                    (basis_inv.id2path(file_id), tree.id2path(file_id)),
 
638
                    False, (True, True),
 
639
                    (basis_entry.parent_id, basis_entry.parent_id),
 
640
                    (basis_entry.name, basis_entry.name),
 
641
                    (basis_entry.kind, basis_entry.kind),
 
642
                    (basis_entry.executable, basis_entry.executable))
 
643
                changes[file_id] = (change, merged_ids[file_id])
 
644
        # changes contains tuples with the change and a set of inventory
 
645
        # candidates for the file.
 
646
        # inv delta is:
 
647
        # old_path, new_path, file_id, new_inventory_entry
 
648
        seen_root = False # Is the root in the basis delta?
 
649
        inv_delta = self._basis_delta
 
650
        modified_rev = self._new_revision_id
 
651
        for change, head_candidates in changes.values():
 
652
            if change[3][1]: # versioned in target.
 
653
                # Several things may be happening here:
 
654
                # We may have a fork in the per-file graph
 
655
                #  - record a change with the content from tree
 
656
                # We may have a change against < all trees  
 
657
                #  - carry over the tree that hasn't changed
 
658
                # We may have a change against all trees
 
659
                #  - record the change with the content from tree
 
660
                kind = change[6][1]
 
661
                file_id = change[0]
 
662
                entry = _entry_factory[kind](file_id, change[5][1],
 
663
                    change[4][1])
 
664
                head_set = self._heads(change[0], set(head_candidates))
 
665
                heads = []
 
666
                # Preserve ordering.
 
667
                for head_candidate in head_candidates:
 
668
                    if head_candidate in head_set:
 
669
                        heads.append(head_candidate)
 
670
                        head_set.remove(head_candidate)
 
671
                carried_over = False
 
672
                if len(heads) == 1:
 
673
                    # Could be a carry-over situation:
 
674
                    parent_entry_revs = parent_entries.get(file_id, None)
 
675
                    if parent_entry_revs:
 
676
                        parent_entry = parent_entry_revs.get(heads[0], None)
 
677
                    else:
 
678
                        parent_entry = None
 
679
                    if parent_entry is None:
 
680
                        # The parent iter_changes was called against is the one
 
681
                        # that is the per-file head, so any change is relevant
 
682
                        # iter_changes is valid.
 
683
                        carry_over_possible = False
 
684
                    else:
 
685
                        # could be a carry over situation
 
686
                        # A change against the basis may just indicate a merge,
 
687
                        # we need to check the content against the source of the
 
688
                        # merge to determine if it was changed after the merge
 
689
                        # or carried over.
 
690
                        if (parent_entry.kind != entry.kind or
 
691
                            parent_entry.parent_id != entry.parent_id or
 
692
                            parent_entry.name != entry.name):
 
693
                            # Metadata common to all entries has changed
 
694
                            # against per-file parent
 
695
                            carry_over_possible = False
 
696
                        else:
 
697
                            carry_over_possible = True
 
698
                        # per-type checks for changes against the parent_entry
 
699
                        # are done below.
 
700
                else:
 
701
                    # Cannot be a carry-over situation
 
702
                    carry_over_possible = False
 
703
                # Populate the entry in the delta
 
704
                if kind == 'file':
 
705
                    # XXX: There is still a small race here: If someone reverts the content of a file
 
706
                    # after iter_changes examines and decides it has changed,
 
707
                    # we will unconditionally record a new version even if some
 
708
                    # other process reverts it while commit is running (with
 
709
                    # the revert happening after iter_changes did its
 
710
                    # examination).
 
711
                    if change[7][1]:
 
712
                        entry.executable = True
 
713
                    else:
 
714
                        entry.executable = False
 
715
                    if (carry_over_possible and
 
716
                        parent_entry.executable == entry.executable):
 
717
                            # Check the file length, content hash after reading
 
718
                            # the file.
 
719
                            nostore_sha = parent_entry.text_sha1
 
720
                    else:
 
721
                        nostore_sha = None
 
722
                    file_obj, stat_value = tree.get_file_with_stat(file_id, change[1][1])
 
723
                    try:
 
724
                        text = file_obj.read()
 
725
                    finally:
 
726
                        file_obj.close()
 
727
                    try:
 
728
                        entry.text_sha1, entry.text_size = self._add_text_to_weave(
 
729
                            file_id, text, heads, nostore_sha)
 
730
                        yield file_id, change[1][1], (entry.text_sha1, stat_value)
 
731
                    except errors.ExistingContent:
 
732
                        # No content change against a carry_over parent
 
733
                        # Perhaps this should also yield a fs hash update?
 
734
                        carried_over = True
 
735
                        entry.text_size = parent_entry.text_size
 
736
                        entry.text_sha1 = parent_entry.text_sha1
 
737
                elif kind == 'symlink':
 
738
                    # Wants a path hint?
 
739
                    entry.symlink_target = tree.get_symlink_target(file_id)
 
740
                    if (carry_over_possible and
 
741
                        parent_entry.symlink_target == entry.symlink_target):
 
742
                        carried_over = True
 
743
                    else:
 
744
                        self._add_text_to_weave(change[0], '', heads, None)
 
745
                elif kind == 'directory':
 
746
                    if carry_over_possible:
 
747
                        carried_over = True
 
748
                    else:
 
749
                        # Nothing to set on the entry.
 
750
                        # XXX: split into the Root and nonRoot versions.
 
751
                        if change[1][1] != '' or self.repository.supports_rich_root():
 
752
                            self._add_text_to_weave(change[0], '', heads, None)
 
753
                elif kind == 'tree-reference':
 
754
                    if not self.repository._format.supports_tree_reference:
 
755
                        # This isn't quite sane as an error, but we shouldn't
 
756
                        # ever see this code path in practice: tree's don't
 
757
                        # permit references when the repo doesn't support tree
 
758
                        # references.
 
759
                        raise errors.UnsupportedOperation(tree.add_reference,
 
760
                            self.repository)
 
761
                    reference_revision = tree.get_reference_revision(change[0])
 
762
                    entry.reference_revision = reference_revision
 
763
                    if (carry_over_possible and
 
764
                        parent_entry.reference_revision == reference_revision):
 
765
                        carried_over = True
 
766
                    else:
 
767
                        self._add_text_to_weave(change[0], '', heads, None)
 
768
                else:
 
769
                    raise AssertionError('unknown kind %r' % kind)
 
770
                if not carried_over:
 
771
                    entry.revision = modified_rev
 
772
                else:
 
773
                    entry.revision = parent_entry.revision
 
774
            else:
 
775
                entry = None
 
776
            new_path = change[1][1]
 
777
            inv_delta.append((change[1][0], new_path, change[0], entry))
 
778
            if new_path == '':
 
779
                seen_root = True
 
780
        self.new_inventory = None
 
781
        if len(inv_delta):
 
782
            # This should perhaps be guarded by a check that the basis we
 
783
            # commit against is the basis for the commit and if not do a delta
 
784
            # against the basis.
 
785
            self._any_changes = True
 
786
        if not seen_root:
 
787
            # housekeeping root entry changes do not affect no-change commits.
 
788
            self._require_root_change(tree)
 
789
        self.basis_delta_revision = basis_revision_id
 
790
 
 
791
    def _add_text_to_weave(self, file_id, new_text, parents, nostore_sha):
 
792
        parent_keys = tuple([(file_id, parent) for parent in parents])
 
793
        return self.repository.texts._add_text(
 
794
            (file_id, self._new_revision_id), parent_keys, new_text,
 
795
            nostore_sha=nostore_sha, random_id=self.random_revid)[0:2]
 
796
 
 
797
 
 
798
class VersionedFileRootCommitBuilder(VersionedFileCommitBuilder):
 
799
    """This commitbuilder actually records the root id"""
 
800
 
 
801
    # the root entry gets versioned properly by this builder.
 
802
    _versioned_root = True
 
803
 
 
804
    def _check_root(self, ie, parent_invs, tree):
 
805
        """Helper for record_entry_contents.
 
806
 
 
807
        :param ie: An entry being added.
 
808
        :param parent_invs: The inventories of the parent revisions of the
 
809
            commit.
 
810
        :param tree: The tree that is being committed.
 
811
        """
 
812
 
 
813
    def _require_root_change(self, tree):
 
814
        """Enforce an appropriate root object change.
 
815
 
 
816
        This is called once when record_iter_changes is called, if and only if
 
817
        the root was not in the delta calculated by record_iter_changes.
 
818
 
 
819
        :param tree: The tree which is being committed.
 
820
        """
 
821
        # versioned roots do not change unless the tree found a change.
 
822
 
 
823
 
67
824
class VersionedFileRepository(Repository):
68
825
    """Repository holding history for one or more branches.
69
826
 
117
874
        pointing to .bzr/repository.
118
875
    """
119
876
 
 
877
    # What class to use for a CommitBuilder. Often it's simpler to change this
 
878
    # in a Repository class subclass rather than to override
 
879
    # get_commit_builder.
 
880
    _commit_builder_class = VersionedFileCommitBuilder
 
881
 
120
882
    def add_fallback_repository(self, repository):
121
883
        """Add a repository to use for looking up data not held locally.
122
884
 
408
1170
        # rather copying them?
409
1171
        self._safe_to_return_from_cache = False
410
1172
 
 
1173
    def get_commit_builder(self, branch, parents, config, timestamp=None,
 
1174
                           timezone=None, committer=None, revprops=None,
 
1175
                           revision_id=None, lossy=False):
 
1176
        """Obtain a CommitBuilder for this repository.
 
1177
 
 
1178
        :param branch: Branch to commit to.
 
1179
        :param parents: Revision ids of the parents of the new revision.
 
1180
        :param config: Configuration to use.
 
1181
        :param timestamp: Optional timestamp recorded for commit.
 
1182
        :param timezone: Optional timezone for timestamp.
 
1183
        :param committer: Optional committer to set for commit.
 
1184
        :param revprops: Optional dictionary of revision properties.
 
1185
        :param revision_id: Optional revision id.
 
1186
        :param lossy: Whether to discard data that can not be natively
 
1187
            represented, when pushing to a foreign VCS
 
1188
        """
 
1189
        if self._fallback_repositories and not self._format.supports_chks:
 
1190
            raise errors.BzrError("Cannot commit directly to a stacked branch"
 
1191
                " in pre-2a formats. See "
 
1192
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
 
1193
        result = self._commit_builder_class(self, parents, config,
 
1194
            timestamp, timezone, committer, revprops, revision_id,
 
1195
            lossy)
 
1196
        self.start_write_group()
 
1197
        return result
 
1198
 
411
1199
    def get_missing_parent_inventories(self, check_for_missing_texts=True):
412
1200
        """Return the keys of missing inventory parents for revisions added in
413
1201
        this write group.