/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

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