run-tests.py: Only pass the --svnadmin option to cvs2svn when needed.
[cvs2svn.git] / cvs2svn_lib / repository_mirror.py
blob23a1c3b04083fc76e3027c50b10910ea65f863dc
1 # (Be in -*- python -*- mode.)
3 # ====================================================================
4 # Copyright (c) 2000-2009 CollabNet. All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://subversion.tigris.org/license-1.html.
9 # If newer versions of this license are posted there, you may use a
10 # newer version instead, at your option.
12 # This software consists of voluntary contributions made by many
13 # individuals. For exact contribution history, see the revision
14 # history and logs, available at http://cvs2svn.tigris.org/.
15 # ====================================================================
17 """This module contains the RepositoryMirror class and supporting classes.
19 RepositoryMirror represents the skeleton of a versioned file tree with
20 multiple lines of development ('LODs'). It records the presence or
21 absence of files and directories, but not their contents. Given three
22 values (revnum, lod, cvs_path), it can tell you whether the specified
23 CVSPath existed on the specified LOD in the given revision number.
24 The file trees corresponding to the most recent revision can be
25 modified.
27 The individual file trees are stored using immutable tree structures.
28 Each directory node is represented as a MirrorDirectory instance,
29 which is basically a map {cvs_path : node_id}, where cvs_path is a
30 CVSPath within the directory, and node_id is an integer ID that
31 uniquely identifies another directory node if that node is a
32 CVSDirectory, or None if that node is a CVSFile. If a directory node
33 is to be modified, then first a new node is created with a copy of the
34 original node's contents, then the copy is modified. A reference to
35 the copy also has to be stored in the parent node, meaning that the
36 parent node needs to be modified, and so on recursively to the root
37 node of the file tree. This data structure allows cheap deep copies,
38 which is useful for tagging and branching.
40 The class must also be able to find the root directory node
41 corresponding to a particular (revnum, lod). This is done by keeping
42 an LODHistory instance for each LOD, which can determine the root
43 directory node ID for that LOD for any revnum. It does so by
44 recording changes to the root directory node ID only for revisions in
45 which it changed. Thus it stores two arrays, revnums (a list of the
46 revision numbers when the ID changed), and ids (a list of the
47 corresponding IDs). To find the ID for a particular revnum, first a
48 binary search is done in the revnums array to find the index of the
49 last change preceding revnum, then the corresponding ID is read from
50 the ids array. Since most revisions change only one LOD, this allows
51 storage of the history of potentially tens of thousands of LODs over
52 hundreds of thousands of revisions in an amount of space that scales
53 as O(numberOfLODs + numberOfRevisions), rather than O(numberOfLODs *
54 numberOfRevisions) as would be needed if the information were stored
55 in the equivalent of a 2D array.
57 The internal operation of these classes is somewhat intricate, but the
58 interface attempts to hide the complexity, enforce the usage rules,
59 and allow efficient access. The most important facts to remember are
60 (1) that a directory node can be used for multiple purposes (for
61 multiple branches and for multiple revisions on a single branch), (2)
62 that only a node that has been created within the current revision is
63 allowed to be mutated, and (3) that the current revision can include
64 nodes carried over from prior revisions, which are immutable.
66 This leads to a bewildering variety of MirrorDirectory classes. The
67 most important distinction is between OldMirrorDirectories and
68 CurrentMirrorDirectories. A single node can be represented multiple
69 ways in memory at the same time, depending on whether it was looked up
70 as part of the current revision or part of an old revision:
72 MirrorDirectory -- the base class for all MirrorDirectory nodes.
73 This class allows lookup of subnodes and iteration over
74 subnodes.
76 OldMirrorDirectory -- a MirrorDirectory that was looked up for an
77 old revision. These instances are immutable, as only the
78 current revision is allowed to be modified.
80 CurrentMirrorDirectory -- a MirrorDirectory that was looked up for
81 the current revision. Such an instance is always logically
82 mutable, though mutating it might require the node to be
83 copied first. Such an instance might represent a node that
84 has already been copied during this revision and can therefore
85 be modified freely (such nodes implement
86 _WritableMirrorDirectoryMixin), or it might represent a node
87 that was carried over from an old revision and hasn't been
88 copied yet (such nodes implement
89 _ReadOnlyMirrorDirectoryMixin). If the latter, then the node
90 copies itself (and bubbles up the change) before allowing
91 itself to be modified. But the distinction is managed
92 internally; client classes should not have to worry about it.
94 CurrentMirrorLODDirectory -- A CurrentMirrorDirectory representing
95 the root directory of a line of development in the current
96 revision. This class has two concrete subclasses,
97 _CurrentMirrorReadOnlyLODDirectory and
98 _CurrentMirrorWritableLODDirectory, depending on whether the
99 node has already been copied during this revision.
102 CurrentMirrorSubdirectory -- A CurrentMirrorDirectory representing
103 a subdirectory within a line of development's directory tree
104 in the current revision. This class has two concrete
105 subclasses, _CurrentMirrorReadOnlySubdirectory and
106 _CurrentMirrorWritableSubdirectory, depending on whether the
107 node has already been copied during this revision.
109 DeletedCurrentMirrorDirectory -- a MirrorDirectory that has been
110 deleted. Such an instance is disabled so that it cannot
111 accidentally be used.
113 While a revision is being processed, RepositoryMirror._new_nodes holds
114 every writable CurrentMirrorDirectory instance (i.e., every node that
115 has been created in the revision). Since these nodes are mutable, it
116 is important that there be exactly one instance associated with each
117 node; otherwise there would be problems keeping the instances
118 synchronized. These are written to the database by
119 RepositoryMirror.end_commit().
121 OldMirrorDirectory and read-only CurrentMirrorDirectory instances are
122 *not* cached; they are recreated whenever they are referenced. There
123 might be multiple instances referring to the same node. A read-only
124 CurrentMirrorDirectory instance is mutated in place into a writable
125 CurrentMirrorDirectory instance if it needs to be modified.
127 FIXME: The rules for when a MirrorDirectory instance can continue to
128 be used vs. when it has to be read again (because it has been modified
129 indirectly and therefore copied) are confusing and error-prone.
130 Probably the semantics should be changed.
135 import bisect
137 from cvs2svn_lib import config
138 from cvs2svn_lib.common import DB_OPEN_NEW
139 from cvs2svn_lib.common import InternalError
140 from cvs2svn_lib.log import logger
141 from cvs2svn_lib.context import Ctx
142 from cvs2svn_lib.cvs_path import CVSFile
143 from cvs2svn_lib.cvs_path import CVSDirectory
144 from cvs2svn_lib.key_generator import KeyGenerator
145 from cvs2svn_lib.artifact_manager import artifact_manager
146 from cvs2svn_lib.serializer import MarshalSerializer
147 from cvs2svn_lib.indexed_database import IndexedDatabase
150 class RepositoryMirrorError(Exception):
151 """An error related to the RepositoryMirror."""
153 pass
156 class LODExistsError(RepositoryMirrorError):
157 """The LOD already exists in the repository.
159 Exception raised if an attempt is made to add an LOD to the
160 repository mirror and that LOD already exists in the youngest
161 revision of the repository."""
163 pass
166 class PathExistsError(RepositoryMirrorError):
167 """The path already exists in the repository.
169 Exception raised if an attempt is made to add a path to the
170 repository mirror and that path already exists in the youngest
171 revision of the repository."""
173 pass
176 class DeletedNodeReusedError(RepositoryMirrorError):
177 """The MirrorDirectory has already been deleted and shouldn't be reused."""
179 pass
182 class CopyFromCurrentNodeError(RepositoryMirrorError):
183 """A CurrentMirrorDirectory cannot be copied to the current revision."""
185 pass
188 class MirrorDirectory(object):
189 """Represent a node within the RepositoryMirror.
191 Instances of this class act like a map {CVSPath : MirrorDirectory},
192 where CVSPath is an item within this directory (i.e., a file or
193 subdirectory within this directory). The value is either another
194 MirrorDirectory instance (for directories) or None (for files)."""
196 def __init__(self, repo, id, entries):
197 # The RepositoryMirror containing this directory:
198 self.repo = repo
200 # The id of this node:
201 self.id = id
203 # The entries within this directory, stored as a map {CVSPath :
204 # node_id}. The node_ids are integers for CVSDirectories, None
205 # for CVSFiles:
206 self._entries = entries
208 def __getitem__(self, cvs_path):
209 """Return the MirrorDirectory associated with the specified subnode.
211 Return a MirrorDirectory instance if the subnode is a
212 CVSDirectory; None if it is a CVSFile. Raise KeyError if the
213 specified subnode does not exist."""
215 raise NotImplementedError()
217 def __len__(self):
218 """Return the number of CVSPaths within this node."""
220 return len(self._entries)
222 def __contains__(self, cvs_path):
223 """Return True iff CVS_PATH is contained in this node."""
225 return cvs_path in self._entries
227 def __iter__(self):
228 """Iterate over the CVSPaths within this node."""
230 return self._entries.__iter__()
232 def _format_entries(self):
233 """Format the entries map for output in subclasses' __repr__() methods."""
235 def format_item(key, value):
236 if value is None:
237 return str(key)
238 else:
239 return '%s -> %x' % (key, value,)
241 items = self._entries.items()
242 items.sort()
243 return '{%s}' % (', '.join([format_item(*item) for item in items]),)
245 def __str__(self):
246 """For convenience only. The format is subject to change at any time."""
248 return '%s<%x>' % (self.__class__.__name__, self.id,)
251 class OldMirrorDirectory(MirrorDirectory):
252 """Represent a historical directory within the RepositoryMirror."""
254 def __getitem__(self, cvs_path):
255 id = self._entries[cvs_path]
256 if id is None:
257 # This represents a leaf node.
258 return None
259 else:
260 return OldMirrorDirectory(self.repo, id, self.repo._node_db[id])
262 def __repr__(self):
263 """For convenience only. The format is subject to change at any time."""
265 return '%s(%s)' % (self, self._format_entries(),)
268 class CurrentMirrorDirectory(MirrorDirectory):
269 """Represent a directory that currently exists in the RepositoryMirror."""
271 def __init__(self, repo, id, lod, cvs_path, entries):
272 MirrorDirectory.__init__(self, repo, id, entries)
273 self.lod = lod
274 self.cvs_path = cvs_path
276 def __getitem__(self, cvs_path):
277 id = self._entries[cvs_path]
278 if id is None:
279 # This represents a leaf node.
280 return None
281 else:
282 try:
283 return self.repo._new_nodes[id]
284 except KeyError:
285 return _CurrentMirrorReadOnlySubdirectory(
286 self.repo, id, self.lod, cvs_path, self,
287 self.repo._node_db[id]
290 def __setitem__(self, cvs_path, node):
291 """Create or overwrite a subnode of this node.
293 CVS_PATH is the path of the subnode. NODE will be the new value
294 of the node; for CVSDirectories it should be a MirrorDirectory
295 instance; for CVSFiles it should be None."""
297 if isinstance(node, DeletedCurrentMirrorDirectory):
298 raise DeletedNodeReusedError(
299 '%r has already been deleted and should not be reused' % (node,)
301 elif isinstance(node, CurrentMirrorDirectory):
302 raise CopyFromCurrentNodeError(
303 '%r was created in the current node and cannot be copied' % (node,)
305 else:
306 self._set_entry(cvs_path, node)
308 def __delitem__(self, cvs_path):
309 """Remove the subnode of this node at CVS_PATH.
311 If the node does not exist, then raise a KeyError."""
313 node = self[cvs_path]
314 self._del_entry(cvs_path)
315 if isinstance(node, _WritableMirrorDirectoryMixin):
316 node._mark_deleted()
318 def mkdir(self, cvs_directory):
319 """Create an empty subdirectory of this node at CVS_PATH.
321 Return the CurrentDirectory that was created."""
323 assert isinstance(cvs_directory, CVSDirectory)
324 if cvs_directory in self:
325 raise PathExistsError(
326 'Attempt to create directory \'%s\' in %s in repository mirror '
327 'when it already exists.'
328 % (cvs_directory, self.lod,)
331 new_node = _CurrentMirrorWritableSubdirectory(
332 self.repo, self.repo._key_generator.gen_id(), self.lod, cvs_directory,
333 self, {}
335 self._set_entry(cvs_directory, new_node)
336 self.repo._new_nodes[new_node.id] = new_node
337 return new_node
339 def add_file(self, cvs_file):
340 """Create a file within this node at CVS_FILE."""
342 assert isinstance(cvs_file, CVSFile)
343 if cvs_file in self:
344 raise PathExistsError(
345 'Attempt to create file \'%s\' in %s in repository mirror '
346 'when it already exists.'
347 % (cvs_file, self.lod,)
350 self._set_entry(cvs_file, None)
352 def __repr__(self):
353 """For convenience only. The format is subject to change at any time."""
355 return '%s(%r, %r, %s)' % (
356 self, self.lod, self.cvs_path, self._format_entries(),
360 class DeletedCurrentMirrorDirectory(object):
361 """A MirrorDirectory that has been deleted.
363 A MirrorDirectory that used to be a _WritableMirrorDirectoryMixin
364 but then was deleted. Such instances are turned into this class so
365 that nobody can accidentally mutate them again."""
367 pass
370 class _WritableMirrorDirectoryMixin:
371 """Mixin for MirrorDirectories that are already writable.
373 A MirrorDirectory is writable if it has already been recreated
374 during the current revision."""
376 def _set_entry(self, cvs_path, node):
377 """Create or overwrite a subnode of this node, with no checks."""
379 if node is None:
380 self._entries[cvs_path] = None
381 else:
382 self._entries[cvs_path] = node.id
384 def _del_entry(self, cvs_path):
385 """Remove the subnode of this node at CVS_PATH, with no checks."""
387 del self._entries[cvs_path]
389 def _mark_deleted(self):
390 """Mark this object and any writable descendants as being deleted."""
392 self.__class__ = DeletedCurrentMirrorDirectory
394 for (cvs_path, id) in self._entries.iteritems():
395 if id in self.repo._new_nodes:
396 node = self[cvs_path]
397 if isinstance(node, _WritableMirrorDirectoryMixin):
398 # Mark deleted and recurse:
399 node._mark_deleted()
402 class _ReadOnlyMirrorDirectoryMixin:
403 """Mixin for a CurrentMirrorDirectory that hasn't yet been made writable."""
405 def _make_writable(self):
406 raise NotImplementedError()
408 def _set_entry(self, cvs_path, node):
409 """Create or overwrite a subnode of this node, with no checks."""
411 self._make_writable()
412 self._set_entry(cvs_path, node)
414 def _del_entry(self, cvs_path):
415 """Remove the subnode of this node at CVS_PATH, with no checks."""
417 self._make_writable()
418 self._del_entry(cvs_path)
421 class CurrentMirrorLODDirectory(CurrentMirrorDirectory):
422 """Represent an LOD's main directory in the mirror's current version."""
424 def __init__(self, repo, id, lod, entries):
425 CurrentMirrorDirectory.__init__(
426 self, repo, id, lod, lod.project.get_root_cvs_directory(), entries
429 def delete(self):
430 """Remove the directory represented by this object."""
432 lod_history = self.repo._get_lod_history(self.lod)
433 assert lod_history.exists()
434 lod_history.update(self.repo._youngest, None)
435 self._mark_deleted()
438 class _CurrentMirrorReadOnlyLODDirectory(
439 CurrentMirrorLODDirectory, _ReadOnlyMirrorDirectoryMixin
441 """Represent an LOD's main directory in the mirror's current version."""
443 def _make_writable(self):
444 self.__class__ = _CurrentMirrorWritableLODDirectory
445 # Create a new ID:
446 self.id = self.repo._key_generator.gen_id()
447 self.repo._new_nodes[self.id] = self
448 self.repo._get_lod_history(self.lod).update(self.repo._youngest, self.id)
449 self._entries = self._entries.copy()
452 class _CurrentMirrorWritableLODDirectory(
453 CurrentMirrorLODDirectory, _WritableMirrorDirectoryMixin
455 pass
458 class CurrentMirrorSubdirectory(CurrentMirrorDirectory):
459 """Represent a subdirectory in the mirror's current version."""
461 def __init__(self, repo, id, lod, cvs_path, parent_mirror_dir, entries):
462 CurrentMirrorDirectory.__init__(self, repo, id, lod, cvs_path, entries)
463 self.parent_mirror_dir = parent_mirror_dir
465 def delete(self):
466 """Remove the directory represented by this object."""
468 del self.parent_mirror_dir[self.cvs_path]
471 class _CurrentMirrorReadOnlySubdirectory(
472 CurrentMirrorSubdirectory, _ReadOnlyMirrorDirectoryMixin
474 """Represent a subdirectory in the mirror's current version."""
476 def _make_writable(self):
477 self.__class__ = _CurrentMirrorWritableSubdirectory
478 # Create a new ID:
479 self.id = self.repo._key_generator.gen_id()
480 self.repo._new_nodes[self.id] = self
481 self.parent_mirror_dir._set_entry(self.cvs_path, self)
482 self._entries = self._entries.copy()
485 class _CurrentMirrorWritableSubdirectory(
486 CurrentMirrorSubdirectory, _WritableMirrorDirectoryMixin
488 pass
491 class LODHistory(object):
492 """The history of root nodes for a line of development.
494 Members:
496 _mirror -- (RepositoryMirror) the RepositoryMirror that manages
497 this LODHistory.
499 lod -- (LineOfDevelopment) the LOD described by this LODHistory.
501 revnums -- (list of int) the revision numbers in which the id
502 changed, in numerical order.
504 ids -- (list of (int or None)) the ID of the node describing the
505 root of this LOD starting at the corresponding revision
506 number, or None if the LOD did not exist in that revision.
508 To find the root id for a given revision number, a binary search is
509 done within REVNUMS to find the index of the most recent revision at
510 the time of REVNUM, then that index is used to read the id out of
511 IDS.
513 A sentry is written at the zeroth index of both arrays to describe
514 the initial situation, namely, that the LOD doesn't exist in
515 revision r0."""
517 __slots__ = ['_mirror', 'lod', 'revnums', 'ids']
519 def __init__(self, mirror, lod):
520 self._mirror = mirror
521 self.lod = lod
522 self.revnums = [0]
523 self.ids = [None]
525 def get_id(self, revnum):
526 """Get the ID of the root path for this LOD in REVNUM.
528 Raise KeyError if this LOD didn't exist in REVNUM."""
530 index = bisect.bisect_right(self.revnums, revnum) - 1
531 id = self.ids[index]
533 if id is None:
534 raise KeyError(revnum)
536 return id
538 def get_current_id(self):
539 """Get the ID of the root path for this LOD in the current revision.
541 Raise KeyError if this LOD doesn't currently exist."""
543 id = self.ids[-1]
545 if id is None:
546 raise KeyError()
548 return id
550 def exists(self):
551 """Return True iff LOD exists in the current revision."""
553 return self.ids[-1] is not None
555 def update(self, revnum, id):
556 """Indicate that the root node of this LOD changed to ID at REVNUM.
558 REVNUM is a revision number that must be the same as that of the
559 previous recorded change (in which case the previous change is
560 overwritten) or later (in which the new change is appended).
562 ID can be a node ID, or it can be None to indicate that this LOD
563 ceased to exist in REVNUM."""
565 if revnum < self.revnums[-1]:
566 raise KeyError(revnum)
567 elif revnum == self.revnums[-1]:
568 # This is an attempt to overwrite an entry that was already
569 # updated during this revision. Don't allow the replacement
570 # None -> None or allow one new id to be replaced with another:
571 old_id = self.ids[-1]
572 if old_id is None and id is None:
573 raise InternalError(
574 'ID changed from None -> None for %s, r%d' % (self.lod, revnum,)
576 elif (old_id is not None and id is not None
577 and old_id in self._mirror._new_nodes):
578 raise InternalError(
579 'ID changed from %x -> %x for %s, r%d'
580 % (old_id, id, self.lod, revnum,)
582 self.ids[-1] = id
583 else:
584 self.revnums.append(revnum)
585 self.ids.append(id)
588 class _NodeDatabase(object):
589 """A database storing all of the directory nodes.
591 The nodes are written in groups every time write_new_nodes() is
592 called. To the database is written a dictionary {node_id :
593 [(cvs_path.id, node_id),...]}, where the keys are the node_ids of
594 the new nodes. When a node is read, its whole group is read and
595 cached under the assumption that the other nodes in the group are
596 likely to be needed soon. The cache is retained across revisions
597 and cleared when _cache_max_size is exceeded.
599 The dictionaries for nodes that have been read from the database
600 during the current revision are cached by node_id in the _cache
601 member variable. The corresponding dictionaries are *not* copied
602 when read. To avoid cross-talk between distinct MirrorDirectory
603 instances that have the same node_id, users of these dictionaries
604 have to copy them before modification."""
606 # How many entries should be allowed in the cache for each
607 # CVSDirectory in the repository. (This number is very roughly the
608 # number of complete lines of development that can be stored in the
609 # cache at one time.)
610 CACHE_SIZE_MULTIPLIER = 5
612 # But the cache will never be limited to less than this number:
613 MIN_CACHE_LIMIT = 5000
615 def __init__(self):
616 self.cvs_path_db = Ctx()._cvs_path_db
617 self.db = IndexedDatabase(
618 artifact_manager.get_temp_file(config.MIRROR_NODES_STORE),
619 artifact_manager.get_temp_file(config.MIRROR_NODES_INDEX_TABLE),
620 DB_OPEN_NEW, serializer=MarshalSerializer(),
623 # A list of the maximum node_id stored by each call to
624 # write_new_nodes():
625 self._max_node_ids = [0]
627 # A map {node_id : {cvs_path : node_id}}:
628 self._cache = {}
630 # The number of directories in the repository:
631 num_dirs = len([
632 cvs_path
633 for cvs_path in self.cvs_path_db.itervalues()
634 if isinstance(cvs_path, CVSDirectory)
637 self._cache_max_size = max(
638 int(self.CACHE_SIZE_MULTIPLIER * num_dirs),
639 self.MIN_CACHE_LIMIT,
642 def _load(self, items):
643 retval = {}
644 for (id, value) in items:
645 retval[self.cvs_path_db.get_path(id)] = value
646 return retval
648 def _dump(self, node):
649 return [
650 (cvs_path.id, value)
651 for (cvs_path, value) in node.iteritems()
654 def _determine_index(self, id):
655 """Return the index of the record holding the node with ID."""
657 return bisect.bisect_left(self._max_node_ids, id)
659 def __getitem__(self, id):
660 try:
661 items = self._cache[id]
662 except KeyError:
663 index = self._determine_index(id)
664 for (node_id, items) in self.db[index].items():
665 self._cache[node_id] = self._load(items)
666 items = self._cache[id]
668 return items
670 def write_new_nodes(self, nodes):
671 """Write NODES to the database.
673 NODES is an iterable of writable CurrentMirrorDirectory instances."""
675 if len(self._cache) > self._cache_max_size:
676 # The size of the cache has exceeded the threshold. Discard the
677 # old cache values (but still store the new nodes into the
678 # cache):
679 logger.debug('Clearing node cache')
680 self._cache.clear()
682 data = {}
683 max_node_id = 0
684 for node in nodes:
685 max_node_id = max(max_node_id, node.id)
686 data[node.id] = self._dump(node._entries)
687 self._cache[node.id] = node._entries
689 self.db[len(self._max_node_ids)] = data
691 if max_node_id == 0:
692 # Rewrite last value:
693 self._max_node_ids.append(self._max_node_ids[-1])
694 else:
695 self._max_node_ids.append(max_node_id)
697 def close(self):
698 self._cache.clear()
699 self.db.close()
700 self.db = None
703 class RepositoryMirror:
704 """Mirror a repository and its history.
706 Mirror a repository as it is constructed, one revision at a time.
707 For each LineOfDevelopment we store a skeleton of the directory
708 structure within that LOD for each revnum in which it changed.
710 For each LOD that has been seen so far, an LODHistory instance is
711 stored in self._lod_histories. An LODHistory keeps track of each
712 revnum in which files were added to or deleted from that LOD, as
713 well as the node id of the root of the node tree describing the LOD
714 contents at that revision.
716 The LOD trees themselves are stored in the _node_db database, which
717 maps node ids to nodes. A node is a map from CVSPath to ids of the
718 corresponding subnodes. The _node_db is stored on disk and each
719 access is expensive.
721 The _node_db database only holds the nodes for old revisions. The
722 revision that is being constructed is kept in memory in the
723 _new_nodes map, which is cheap to access.
725 You must invoke start_commit() before each commit and end_commit()
726 afterwards."""
728 def register_artifacts(self, which_pass):
729 """Register the artifacts that will be needed for this object."""
731 artifact_manager.register_temp_file(
732 config.MIRROR_NODES_INDEX_TABLE, which_pass
734 artifact_manager.register_temp_file(
735 config.MIRROR_NODES_STORE, which_pass
738 def open(self):
739 """Set up the RepositoryMirror and prepare it for commits."""
741 self._key_generator = KeyGenerator()
743 # A map from LOD to LODHistory instance for all LODs that have
744 # been referenced so far:
745 self._lod_histories = {}
747 # This corresponds to the 'nodes' table in a Subversion fs. (We
748 # don't need a 'representations' or 'strings' table because we
749 # only track file existence, not file contents.)
750 self._node_db = _NodeDatabase()
752 # Start at revision 0 without a root node.
753 self._youngest = 0
755 def start_commit(self, revnum):
756 """Start a new commit."""
758 assert revnum > self._youngest
759 self._youngest = revnum
761 # A map {node_id : _WritableMirrorDirectoryMixin}.
762 self._new_nodes = {}
764 def end_commit(self):
765 """Called at the end of each commit.
767 This method copies the newly created nodes to the on-disk nodes
768 db."""
770 # Copy the new nodes to the _node_db
771 self._node_db.write_new_nodes([
772 node
773 for node in self._new_nodes.values()
774 if not isinstance(node, DeletedCurrentMirrorDirectory)
777 del self._new_nodes
779 def _get_lod_history(self, lod):
780 """Return the LODHistory instance describing LOD.
782 Create a new (empty) LODHistory if it doesn't yet exist."""
784 try:
785 return self._lod_histories[lod]
786 except KeyError:
787 lod_history = LODHistory(self, lod)
788 self._lod_histories[lod] = lod_history
789 return lod_history
791 def get_old_lod_directory(self, lod, revnum):
792 """Return the directory for the root path of LOD at revision REVNUM.
794 Return an instance of MirrorDirectory if the path exists;
795 otherwise, raise KeyError."""
797 lod_history = self._get_lod_history(lod)
798 id = lod_history.get_id(revnum)
799 return OldMirrorDirectory(self, id, self._node_db[id])
801 def get_old_path(self, cvs_path, lod, revnum):
802 """Return the node for CVS_PATH from LOD at REVNUM.
804 If CVS_PATH is a CVSDirectory, then return an instance of
805 OldMirrorDirectory. If CVS_PATH is a CVSFile, return None.
807 If CVS_PATH does not exist in the specified LOD and REVNUM, raise
808 KeyError."""
810 node = self.get_old_lod_directory(lod, revnum)
812 for sub_path in cvs_path.get_ancestry()[1:]:
813 node = node[sub_path]
815 return node
817 def get_current_lod_directory(self, lod):
818 """Return the directory for the root path of LOD in the current revision.
820 Return an instance of CurrentMirrorDirectory. Raise KeyError if
821 the path doesn't already exist."""
823 lod_history = self._get_lod_history(lod)
824 id = lod_history.get_current_id()
825 try:
826 return self._new_nodes[id]
827 except KeyError:
828 return _CurrentMirrorReadOnlyLODDirectory(
829 self, id, lod, self._node_db[id]
832 def get_current_path(self, cvs_path, lod):
833 """Return the node for CVS_PATH from LOD in the current revision.
835 If CVS_PATH is a CVSDirectory, then return an instance of
836 CurrentMirrorDirectory. If CVS_PATH is a CVSFile, return None.
838 If CVS_PATH does not exist in the current revision of the
839 specified LOD, raise KeyError."""
841 node = self.get_current_lod_directory(lod)
843 for sub_path in cvs_path.get_ancestry()[1:]:
844 node = node[sub_path]
846 return node
848 def add_lod(self, lod):
849 """Create a new LOD in this repository.
851 Return the CurrentMirrorDirectory that was created. If the LOD
852 already exists, raise LODExistsError."""
854 lod_history = self._get_lod_history(lod)
855 if lod_history.exists():
856 raise LODExistsError(
857 'Attempt to create %s in repository mirror when it already exists.'
858 % (lod,)
860 new_node = _CurrentMirrorWritableLODDirectory(
861 self, self._key_generator.gen_id(), lod, {}
863 lod_history.update(self._youngest, new_node.id)
864 self._new_nodes[new_node.id] = new_node
865 return new_node
867 def copy_lod(self, src_lod, dest_lod, src_revnum):
868 """Copy all of SRC_LOD at SRC_REVNUM to DST_LOD.
870 In the youngest revision of the repository, the destination LOD
871 *must not* already exist.
873 Return the new node at DEST_LOD, as a CurrentMirrorDirectory."""
875 # Get the node of our src_path
876 src_node = self.get_old_lod_directory(src_lod, src_revnum)
878 dest_lod_history = self._get_lod_history(dest_lod)
879 if dest_lod_history.exists():
880 raise LODExistsError(
881 'Attempt to copy to %s in repository mirror when it already exists.'
882 % (dest_lod,)
885 dest_lod_history.update(self._youngest, src_node.id)
887 # Return src_node, except packaged up as a CurrentMirrorDirectory:
888 return self.get_current_lod_directory(dest_lod)
890 def close(self):
891 """Free resources and close databases."""
893 self._lod_histories = None
894 self._node_db.close()
895 self._node_db = None