71
class VersionedFileCommitBuilder(CommitBuilder):
72
"""Commit builder implementation for versioned files based repositories.
75
# this commit builder supports the record_entry_contents interface
76
supports_record_entry_contents = True
78
# the default CommitBuilder does not manage trees whose root is versioned.
79
_versioned_root = False
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,
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
98
def will_record_deletes(self):
99
"""Tell the commit builder that deletes are being notified.
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().
105
self._recording_deletes = True
107
basis_id = self.parents[0]
109
basis_id = _mod_revision.NULL_REVISION
110
self.basis_delta_revision = basis_id
112
def any_changes(self):
113
"""Return True if any entries were changed.
115
This includes merge-only changes. It is the core for the --unchanged
118
:return: True if any changes have occured.
120
return self._any_changes
122
def _ensure_fallback_inventories(self):
123
"""Ensure that appropriate inventories are available.
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.
130
if not self.repository._fallback_repositories:
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]
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)
154
raise errors.BzrError('Unable to fill in parent inventories for a'
157
def commit(self, message):
158
"""Make the actual commit.
160
:return: The revision id of the recorded revision.
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,
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
179
"""Abort the commit that is being built.
181
self.repository.abort_write_group()
183
def revision_tree(self):
184
"""Return the tree that was just committed.
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
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)
198
def finish_inventory(self):
199
"""Tell the builder that the inventory is finished.
201
:return: The inventory id in the repository, which can be used with
202
repository.get_inventory.
204
if self.new_inventory is None:
205
# an inventory delta was accumulated without creating a new
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,
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,
225
return self._new_revision_id
227
def _check_root(self, ie, parent_invs, tree):
228
"""Helper for record_entry_contents.
230
:param ie: An entry being added.
231
:param parent_invs: The inventories of the parent revisions of the
233
:param tree: The tree that is being committed.
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
238
ie.revision = self._new_revision_id
240
def _require_root_change(self, tree):
241
"""Enforce an appropriate root object change.
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.
246
:param tree: The tree which is being committed.
248
if len(self.parents) == 0:
249
raise errors.RootMissing()
250
entry = entry_factory['directory'](tree.path2id(''), '',
252
entry.revision = self._new_revision_id
253
self._basis_delta.append(('', '', entry.file_id, entry))
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:
259
result = (None, path, ie.file_id, ie)
260
self._basis_delta.append(result)
262
elif ie != basis_inv[ie.file_id]:
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)
272
def _heads(self, file_id, revision_ids):
273
"""Calculate the graph heads for revision_ids in the graph of file_id.
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.
278
return self.__heads(revision_ids)
280
def get_basis_delta(self):
281
"""Return the complete inventory delta versus the basis inventory.
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
288
:return: An inventory delta, suitable for use with apply_delta, or
289
Repository.add_inventory_by_delta, etc.
291
if not self._recording_deletes:
292
raise AssertionError("recording deletes not activated.")
293
return self._basis_delta
295
def record_delete(self, path, file_id):
296
"""Record that a delete occured against a basis tree.
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.
303
:param path: The path of the thing deleted.
304
:param file_id: The file id that was deleted.
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
313
def record_entry_contents(self, ie, parent_invs, path, tree,
315
"""Record the content of ie from tree into the commit if needed.
317
Side effect: sets ie.revision when unchanged
319
:param ie: An inventory entry present in the commit.
320
:param parent_invs: The inventories of the parent revisions of the
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
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
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()).
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]
347
# ie is carried over from a prior commit
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)
358
# TODO: slow, take it out of the inner loop.
360
basis_inv = parent_invs[0]
362
basis_inv = Inventory(root_id=None)
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,
381
delta = (None, path, ie.file_id, ie)
382
self._basis_delta.append(delta)
383
return delta, False, None
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())
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)
408
# now we check to see if we need to write a new record to the
410
# We write a new entry unless there is one head to the ancestors, and
411
# the kind-derived content is unchanged.
413
# Cheapest check first: no ancestors, or more the one head in the
414
# ancestors, we write a new node.
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
422
if (parent_entry.parent_id != ie.parent_id or
423
parent_entry.name != ie.name):
425
# now we need to do content specific checks:
427
# if the kind changed the content obviously has
428
if kind != parent_entry.kind:
430
# Stat cache fingerprint feedback for the caller - None as we usually
431
# don't generate one.
434
if content_summary[2] is None:
435
raise ValueError("Files must not have executable = None")
437
# We can't trust a check of the file length because of content
439
if (# if the exec bit has changed we have to store:
440
parent_entry.executable != content_summary[2]):
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
451
# Either there is only a hash change(no hash cache entry,
452
# or same size content change), or there is no change on
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
458
# We want to record a new node regardless of the presence or
459
# absence of a content change in the file.
461
ie.executable = content_summary[2]
462
file_obj, stat_value = tree.get_file_with_stat(ie.file_id, path)
464
text = file_obj.read()
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
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':
483
# all data is meta here, nothing specific to directory, so
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]
491
# symlink target is not generic metadata, check if it has
493
if current_link_target != parent_entry.symlink_target:
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':
504
if content_summary[3] != parent_entry.reference_revision:
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)
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
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.
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
536
:return: A generator of (file_id, relpath, fs_hash) tuples for use with
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.
544
# file_id -> change map, change is fileid, paths, changed, versioneds,
545
# parents, names, kinds, executables
547
# {file_id -> revision_id -> inventory entry, for entries in parent
548
# trees that are not parents[0]
552
revtrees = list(self.repository.revision_trees(self.parents))
553
except errors.NoSuchRevision:
554
# one or more ghosts, slow path.
556
for revision_id in self.parents:
558
revtrees.append(self.repository.revision_tree(revision_id))
559
except errors.NoSuchRevision:
561
basis_revision_id = _mod_revision.NULL_REVISION
563
revtrees.append(self.repository.revision_tree(
564
_mod_revision.NULL_REVISION))
565
# The basis inventory from a repository
567
basis_inv = revtrees[0].inventory
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:
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.
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]] = [
585
basis_entry.revision,
588
parent_entries[change[2]] = {
590
basis_entry.revision:basis_entry,
592
change[3].revision:change[3],
595
merged_ids[change[2]] = [change[3].revision]
596
parent_entries[change[2]] = {change[3].revision:change[3]}
598
merged_ids[change[2]].append(change[3].revision)
599
parent_entries[change[2]][change[3].revision] = change[3]
602
# Setup the changes from the tree:
603
# changes maps file_id -> (change, [parent revision_ids])
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]
611
changes[change[0]] = change, merged_ids.get(change[0],
613
unchanged_merged = set(merged_ids) - set(changes)
614
# Extend the changes dict with synthetic changes to record merges of
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,
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.
637
(basis_inv.id2path(file_id), tree.id2path(file_id)),
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.
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
662
entry = _entry_factory[kind](file_id, change[5][1],
664
head_set = self._heads(change[0], set(head_candidates))
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)
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)
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
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
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
697
carry_over_possible = True
698
# per-type checks for changes against the parent_entry
701
# Cannot be a carry-over situation
702
carry_over_possible = False
703
# Populate the entry in the delta
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
712
entry.executable = True
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
719
nostore_sha = parent_entry.text_sha1
722
file_obj, stat_value = tree.get_file_with_stat(file_id, change[1][1])
724
text = file_obj.read()
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?
735
entry.text_size = parent_entry.text_size
736
entry.text_sha1 = parent_entry.text_sha1
737
elif kind == 'symlink':
739
entry.symlink_target = tree.get_symlink_target(file_id)
740
if (carry_over_possible and
741
parent_entry.symlink_target == entry.symlink_target):
744
self._add_text_to_weave(change[0], '', heads, None)
745
elif kind == 'directory':
746
if carry_over_possible:
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
759
raise errors.UnsupportedOperation(tree.add_reference,
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):
767
self._add_text_to_weave(change[0], '', heads, None)
769
raise AssertionError('unknown kind %r' % kind)
771
entry.revision = modified_rev
773
entry.revision = parent_entry.revision
776
new_path = change[1][1]
777
inv_delta.append((change[1][0], new_path, change[0], entry))
780
self.new_inventory = None
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
785
self._any_changes = True
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
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]
798
class VersionedFileRootCommitBuilder(VersionedFileCommitBuilder):
799
"""This commitbuilder actually records the root id"""
801
# the root entry gets versioned properly by this builder.
802
_versioned_root = True
804
def _check_root(self, ie, parent_invs, tree):
805
"""Helper for record_entry_contents.
807
:param ie: An entry being added.
808
:param parent_invs: The inventories of the parent revisions of the
810
:param tree: The tree that is being committed.
813
def _require_root_change(self, tree):
814
"""Enforce an appropriate root object change.
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.
819
:param tree: The tree which is being committed.
821
# versioned roots do not change unless the tree found a change.
67
824
class VersionedFileRepository(Repository):
68
825
"""Repository holding history for one or more branches.