aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'cvs2svn_lib/cvs_file_items.py')
-rw-r--r--cvs2svn_lib/cvs_file_items.py1075
1 files changed, 1075 insertions, 0 deletions
diff --git a/cvs2svn_lib/cvs_file_items.py b/cvs2svn_lib/cvs_file_items.py
new file mode 100644
index 0000000..f0dc782
--- /dev/null
+++ b/cvs2svn_lib/cvs_file_items.py
@@ -0,0 +1,1075 @@
+# (Be in -*- python -*- mode.)
+#
+# ====================================================================
+# Copyright (c) 2006-2008 CollabNet. All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://subversion.tigris.org/license-1.html.
+# If newer versions of this license are posted there, you may use a
+# newer version instead, at your option.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://cvs2svn.tigris.org/.
+# ====================================================================
+
+"""This module contains a class to manage the CVSItems related to one file."""
+
+
+import re
+
+from cvs2svn_lib.common import InternalError
+from cvs2svn_lib.common import FatalError
+from cvs2svn_lib.context import Ctx
+from cvs2svn_lib.log import Log
+from cvs2svn_lib.symbol import Trunk
+from cvs2svn_lib.symbol import Branch
+from cvs2svn_lib.symbol import Tag
+from cvs2svn_lib.symbol import ExcludedSymbol
+from cvs2svn_lib.cvs_item import CVSRevision
+from cvs2svn_lib.cvs_item import CVSRevisionModification
+from cvs2svn_lib.cvs_item import CVSRevisionAbsent
+from cvs2svn_lib.cvs_item import CVSRevisionNoop
+from cvs2svn_lib.cvs_item import CVSSymbol
+from cvs2svn_lib.cvs_item import CVSBranch
+from cvs2svn_lib.cvs_item import CVSTag
+from cvs2svn_lib.cvs_item import cvs_revision_type_map
+from cvs2svn_lib.cvs_item import cvs_branch_type_map
+from cvs2svn_lib.cvs_item import cvs_tag_type_map
+
+
+class VendorBranchError(Exception):
+ """There is an error in the structure of the file revision tree."""
+
+ pass
+
+
+class LODItems(object):
+ def __init__(self, lod, cvs_branch, cvs_revisions, cvs_branches, cvs_tags):
+ # The LineOfDevelopment described by this instance.
+ self.lod = lod
+
+ # The CVSBranch starting this LOD, if any; otherwise, None.
+ self.cvs_branch = cvs_branch
+
+ # The list of CVSRevisions on this LOD, if any. The CVSRevisions
+ # are listed in dependency order.
+ self.cvs_revisions = cvs_revisions
+
+ # A list of CVSBranches that sprout from this LOD (either from
+ # cvs_branch or from one of the CVSRevisions).
+ self.cvs_branches = cvs_branches
+
+ # A list of CVSTags that sprout from this LOD (either from
+ # cvs_branch or from one of the CVSRevisions).
+ self.cvs_tags = cvs_tags
+
+ def is_trivial_import(self):
+ """Return True iff this LOD is a trivial import branch in this file.
+
+ A trivial import branch is a branch that was used for a single
+ import and nothing else. Such a branch is eligible for being
+ grafted onto trunk, even if it has branch blockers."""
+
+ return (
+ len(self.cvs_revisions) == 1
+ and self.cvs_revisions[0].ntdbr
+ )
+
+ def is_pure_ntdb(self):
+ """Return True iff this LOD is a pure NTDB in this file.
+
+ A pure non-trunk default branch is defined to be a branch that
+ contains only NTDB revisions (and at least one of them). Such a
+ branch is eligible for being grafted onto trunk, even if it has
+ branch blockers."""
+
+ return (
+ self.cvs_revisions
+ and self.cvs_revisions[-1].ntdbr
+ )
+
+ def iter_blockers(self):
+ if self.is_pure_ntdb():
+ # Such a branch has no blockers, because the blockers can be
+ # grafted to trunk.
+ pass
+ else:
+ # Other branches are only blocked by symbols that sprout from
+ # non-NTDB revisions:
+ non_ntdbr_revision_ids = set()
+ for cvs_revision in self.cvs_revisions:
+ if not cvs_revision.ntdbr:
+ non_ntdbr_revision_ids.add(cvs_revision.id)
+
+ for cvs_tag in self.cvs_tags:
+ if cvs_tag.source_id in non_ntdbr_revision_ids:
+ yield cvs_tag
+
+ for cvs_branch in self.cvs_branches:
+ if cvs_branch.source_id in non_ntdbr_revision_ids:
+ yield cvs_branch
+
+
+class CVSFileItems(object):
+ def __init__(self, cvs_file, trunk, cvs_items):
+ # The file whose data this instance holds.
+ self.cvs_file = cvs_file
+
+ # The symbol that represents "Trunk" in this file.
+ self.trunk = trunk
+
+ # A map from CVSItem.id to CVSItem:
+ self._cvs_items = {}
+
+ # The cvs_item_id of each root in the CVSItem forest. (A root is
+ # defined to be any CVSRevision with no prev_id.)
+ self.root_ids = set()
+
+ for cvs_item in cvs_items:
+ self.add(cvs_item)
+ if isinstance(cvs_item, CVSRevision) and cvs_item.prev_id is None:
+ self.root_ids.add(cvs_item.id)
+
+ def __getstate__(self):
+ return (self.cvs_file.id, self.values(),)
+
+ def __setstate__(self, state):
+ (cvs_file_id, cvs_items,) = state
+ cvs_file = Ctx()._cvs_file_db.get_file(cvs_file_id)
+ CVSFileItems.__init__(
+ self, cvs_file, cvs_file.project.get_trunk(), cvs_items,
+ )
+
+ def add(self, cvs_item):
+ self._cvs_items[cvs_item.id] = cvs_item
+
+ def __getitem__(self, id):
+ """Return the CVSItem with the specified ID."""
+
+ return self._cvs_items[id]
+
+ def get(self, id, default=None):
+ return self._cvs_items.get(id, default)
+
+ def __delitem__(self, id):
+ assert id not in self.root_ids
+ del self._cvs_items[id]
+
+ def values(self):
+ return self._cvs_items.values()
+
+ def check_link_consistency(self):
+ """Check that the CVSItems are linked correctly with each other."""
+
+ for cvs_item in self.values():
+ try:
+ cvs_item.check_links(self)
+ except AssertionError:
+ Log().error(
+ 'Link consistency error in %s\n'
+ 'This is probably a bug internal to cvs2svn. Please file a bug\n'
+ 'report including the following stack trace (see FAQ for more '
+ 'info).'
+ % (cvs_item,))
+ raise
+
+ def _get_lod(self, lod, cvs_branch, start_id):
+ """Return the indicated LODItems.
+
+ LOD is the corresponding LineOfDevelopment. CVS_BRANCH is the
+ CVSBranch instance that starts the LOD if any; otherwise it is
+ None. START_ID is the id of the first CVSRevision on this LOD, or
+ None if there are none."""
+
+ cvs_revisions = []
+ cvs_branches = []
+ cvs_tags = []
+
+ def process_subitems(cvs_item):
+ """Process the branches and tags that are rooted in CVS_ITEM.
+
+ CVS_ITEM can be a CVSRevision or a CVSBranch."""
+
+ for branch_id in cvs_item.branch_ids[:]:
+ cvs_branches.append(self[branch_id])
+
+ for tag_id in cvs_item.tag_ids:
+ cvs_tags.append(self[tag_id])
+
+ if cvs_branch is not None:
+ # Include the symbols sprouting directly from the CVSBranch:
+ process_subitems(cvs_branch)
+
+ id = start_id
+ while id is not None:
+ cvs_rev = self[id]
+ cvs_revisions.append(cvs_rev)
+ process_subitems(cvs_rev)
+ id = cvs_rev.next_id
+
+ return LODItems(lod, cvs_branch, cvs_revisions, cvs_branches, cvs_tags)
+
+ def get_lod_items(self, cvs_branch):
+ """Return an LODItems describing the branch that starts at CVS_BRANCH.
+
+ CVS_BRANCH must be an instance of CVSBranch contained in this
+ CVSFileItems."""
+
+ return self._get_lod(cvs_branch.symbol, cvs_branch, cvs_branch.next_id)
+
+ def iter_root_lods(self):
+ """Iterate over the LODItems for all root LODs (non-recursively)."""
+
+ for id in list(self.root_ids):
+ cvs_item = self[id]
+ if isinstance(cvs_item, CVSRevision):
+ # This LOD doesn't have a CVSBranch associated with it.
+ # Either it is Trunk, or it is a branch whose CVSBranch has
+ # been deleted.
+ yield self._get_lod(cvs_item.lod, None, id)
+ elif isinstance(cvs_item, CVSBranch):
+ # This is a Branch that has been severed from the rest of the
+ # tree.
+ yield self._get_lod(cvs_item.symbol, cvs_item, cvs_item.next_id)
+ else:
+ raise InternalError('Unexpected root item: %s' % (cvs_item,))
+
+ def _iter_tree(self, lod, cvs_branch, start_id):
+ """Iterate over the tree that starts at the specified line of development.
+
+ LOD is the LineOfDevelopment where the iteration should start.
+ CVS_BRANCH is the CVSBranch instance that starts the LOD if any;
+ otherwise it is None. ID is the id of the first CVSRevision on
+ this LOD, or None if there are none.
+
+ There are two cases handled by this routine: trunk (where LOD is a
+ Trunk instance, CVS_BRANCH is None, and ID is the id of the 1.1
+ revision) and a branch (where LOD is a Branch instance, CVS_BRANCH
+ is a CVSBranch instance, and ID is either the id of the first
+ CVSRevision on the branch or None if there are no CVSRevisions on
+ the branch). Note that CVS_BRANCH and ID cannot simultaneously be
+ None.
+
+ Yield an LODItems instance for each line of development."""
+
+ cvs_revisions = []
+ cvs_branches = []
+ cvs_tags = []
+
+ def process_subitems(cvs_item):
+ """Process the branches and tags that are rooted in CVS_ITEM.
+
+ CVS_ITEM can be a CVSRevision or a CVSBranch."""
+
+ for branch_id in cvs_item.branch_ids[:]:
+ # Recurse into the branch:
+ branch = self[branch_id]
+ for lod_items in self._iter_tree(
+ branch.symbol, branch, branch.next_id
+ ):
+ yield lod_items
+ # The caller might have deleted the branch that we just
+ # yielded. If it is no longer present, then do not add it to
+ # the list of cvs_branches.
+ try:
+ cvs_branches.append(self[branch_id])
+ except KeyError:
+ pass
+
+ for tag_id in cvs_item.tag_ids:
+ cvs_tags.append(self[tag_id])
+
+ if cvs_branch is not None:
+ # Include the symbols sprouting directly from the CVSBranch:
+ for lod_items in process_subitems(cvs_branch):
+ yield lod_items
+
+ id = start_id
+ while id is not None:
+ cvs_rev = self[id]
+ cvs_revisions.append(cvs_rev)
+
+ for lod_items in process_subitems(cvs_rev):
+ yield lod_items
+
+ id = cvs_rev.next_id
+
+ yield LODItems(lod, cvs_branch, cvs_revisions, cvs_branches, cvs_tags)
+
+ def iter_lods(self):
+ """Iterate over LinesOfDevelopment in this file, in depth-first order.
+
+ For each LOD, yield an LODItems instance. The traversal starts at
+ each root node but returns the LODs in depth-first order.
+
+ It is allowed to modify the CVSFileItems instance while the
+ traversal is occurring, but only in ways that don't affect the
+ tree structure above (i.e., towards the trunk from) the current
+ LOD."""
+
+ # Make a list out of root_ids so that callers can change it:
+ for id in list(self.root_ids):
+ cvs_item = self[id]
+ if isinstance(cvs_item, CVSRevision):
+ # This LOD doesn't have a CVSBranch associated with it.
+ # Either it is Trunk, or it is a branch whose CVSBranch has
+ # been deleted.
+ lod = cvs_item.lod
+ cvs_branch = None
+ elif isinstance(cvs_item, CVSBranch):
+ # This is a Branch that has been severed from the rest of the
+ # tree.
+ lod = cvs_item.symbol
+ id = cvs_item.next_id
+ cvs_branch = cvs_item
+ else:
+ raise InternalError('Unexpected root item: %s' % (cvs_item,))
+
+ for lod_items in self._iter_tree(lod, cvs_branch, id):
+ yield lod_items
+
+ def iter_deltatext_ancestors(self, cvs_rev):
+ """Generate the delta-dependency ancestors of CVS_REV.
+
+ Generate then ancestors of CVS_REV in deltatext order; i.e., back
+ along branches towards trunk, then outwards along trunk towards
+ HEAD."""
+
+ while True:
+ # Determine the next candidate source revision:
+ if isinstance(cvs_rev.lod, Trunk):
+ if cvs_rev.next_id is None:
+ # HEAD has no ancestors, so we are done:
+ return
+ else:
+ cvs_rev = self[cvs_rev.next_id]
+ else:
+ cvs_rev = self[cvs_rev.prev_id]
+
+ yield cvs_rev
+
+ def _sever_branch(self, lod_items):
+ """Sever the branch from its source and discard the CVSBranch.
+
+ LOD_ITEMS describes a branch that should be severed from its
+ source, deleting the CVSBranch and creating a new root. Also set
+ LOD_ITEMS.cvs_branch to none.
+
+ This method can only be used before symbols have been grafted onto
+ CVSBranches. It does not adjust NTDBR, NTDBR_PREV_ID or
+ NTDBR_NEXT_ID even if LOD_ITEMS describes a NTDB."""
+
+ cvs_branch = lod_items.cvs_branch
+ assert cvs_branch is not None
+ assert not cvs_branch.tag_ids
+ assert not cvs_branch.branch_ids
+ source_rev = self[cvs_branch.source_id]
+
+ # We only cover the following case, even though after
+ # FilterSymbolsPass cvs_branch.source_id might refer to another
+ # CVSBranch.
+ assert isinstance(source_rev, CVSRevision)
+
+ # Delete the CVSBranch itself:
+ lod_items.cvs_branch = None
+ del self[cvs_branch.id]
+
+ # Delete the reference from the source revision to the CVSBranch:
+ source_rev.branch_ids.remove(cvs_branch.id)
+
+ # Delete the reference from the first revision on the branch to
+ # the CVSBranch:
+ if lod_items.cvs_revisions:
+ first_rev = lod_items.cvs_revisions[0]
+
+ # Delete the reference from first_rev to the CVSBranch:
+ first_rev.first_on_branch_id = None
+
+ # Delete the reference from the source revision to the first
+ # revision on the branch:
+ source_rev.branch_commit_ids.remove(first_rev.id)
+
+ # ...and vice versa:
+ first_rev.prev_id = None
+
+ # Change the type of first_rev (e.g., from Change to Add):
+ first_rev.__class__ = cvs_revision_type_map[
+ (isinstance(first_rev, CVSRevisionModification), False,)
+ ]
+
+ # Now first_rev is a new root:
+ self.root_ids.add(first_rev.id)
+
+ def adjust_ntdbrs(self, ntdbr_cvs_revs):
+ """Adjust the specified non-trunk default branch revisions.
+
+ NTDBR_CVS_REVS is a list of CVSRevision instances in this file
+ that have been determined to be non-trunk default branch
+ revisions.
+
+ The first revision on the default branch is handled strangely by
+ CVS. If a file is imported (as opposed to being added), CVS
+ creates a 1.1 revision, then creates a vendor branch 1.1.1 based
+ on 1.1, then creates a 1.1.1.1 revision that is identical to the
+ 1.1 revision (i.e., its deltatext is empty). The log message that
+ the user typed when importing is stored with the 1.1.1.1 revision.
+ The 1.1 revision always contains a standard, generated log
+ message, 'Initial revision\n'.
+
+ When we detect a straightforward import like this, we want to
+ handle it by deleting the 1.1 revision (which doesn't contain any
+ useful information) and making 1.1.1.1 into an independent root in
+ the file's dependency tree. In SVN, 1.1.1.1 will be added
+ directly to the vendor branch with its initial content. Then in a
+ special 'post-commit', the 1.1.1.1 revision is copied back to
+ trunk.
+
+ If the user imports again to the same vendor branch, then CVS
+ creates revisions 1.1.1.2, 1.1.1.3, etc. on the vendor branch,
+ *without* counterparts in trunk (even though these revisions
+ effectively play the role of trunk revisions). So after we add
+ such revisions to the vendor branch, we also copy them back to
+ trunk in post-commits.
+
+ Set the ntdbr members of the revisions listed in NTDBR_CVS_REVS to
+ True. Also, if there is a 1.2 revision, then set that revision to
+ depend on the last non-trunk default branch revision and possibly
+ adjust its type accordingly."""
+
+ for cvs_rev in ntdbr_cvs_revs:
+ cvs_rev.ntdbr = True
+
+ # Look for a 1.2 revision:
+ rev_1_1 = self[ntdbr_cvs_revs[0].prev_id]
+
+ rev_1_2 = self.get(rev_1_1.next_id)
+ if rev_1_2 is not None:
+ # Revision 1.2 logically follows the imported revisions, not
+ # 1.1. Accordingly, connect it to the last NTDBR and possibly
+ # change its type.
+ last_ntdbr = ntdbr_cvs_revs[-1]
+ rev_1_2.ntdbr_prev_id = last_ntdbr.id
+ last_ntdbr.ntdbr_next_id = rev_1_2.id
+ rev_1_2.__class__ = cvs_revision_type_map[(
+ isinstance(rev_1_2, CVSRevisionModification),
+ isinstance(last_ntdbr, CVSRevisionModification),
+ )]
+
+ def process_live_ntdb(self, vendor_lod_items):
+ """VENDOR_LOD_ITEMS is a live default branch; process it.
+
+ In this case, all revisions on the default branch are NTDBRs and
+ it is an error if there is also a '1.2' revision.
+
+ Return True iff this transformation really does something. Raise
+ a VendorBranchError if there is a '1.2' revision."""
+
+ rev_1_1 = self[vendor_lod_items.cvs_branch.source_id]
+ rev_1_2_id = rev_1_1.next_id
+ if rev_1_2_id is not None:
+ raise VendorBranchError(
+ 'File \'%s\' has default branch=%s but also a revision %s'
+ % (self.cvs_file.filename,
+ vendor_lod_items.cvs_branch.branch_number, self[rev_1_2_id].rev,)
+ )
+
+ ntdbr_cvs_revs = list(vendor_lod_items.cvs_revisions)
+
+ if ntdbr_cvs_revs:
+ self.adjust_ntdbrs(ntdbr_cvs_revs)
+ return True
+ else:
+ return False
+
+ def process_historical_ntdb(self, vendor_lod_items):
+ """There appears to have been a non-trunk default branch in the past.
+
+ There is currently no default branch, but the branch described by
+ file appears to have been imported. So our educated guess is that
+ all revisions on the '1.1.1' branch (described by
+ VENDOR_LOD_ITEMS) with timestamps prior to the timestamp of '1.2'
+ were non-trunk default branch revisions.
+
+ Return True iff this transformation really does something.
+
+ This really only handles standard '1.1.1.*'-style vendor
+ revisions. One could conceivably have a file whose default branch
+ is 1.1.3 or whatever, or was that at some point in time, with
+ vendor revisions 1.1.3.1, 1.1.3.2, etc. But with the default
+ branch gone now, we'd have no basis for assuming that the
+ non-standard vendor branch had ever been the default branch
+ anyway.
+
+ Note that we rely on comparisons between the timestamps of the
+ revisions on the vendor branch and that of revision 1.2, even
+ though the timestamps might be incorrect due to clock skew. We
+ could do a slightly better job if we used the changeset
+ timestamps, as it is possible that the dependencies that went into
+ determining those timestamps are more accurate. But that would
+ require an extra pass or two."""
+
+ rev_1_1 = self[vendor_lod_items.cvs_branch.source_id]
+ rev_1_2_id = rev_1_1.next_id
+
+ if rev_1_2_id is None:
+ rev_1_2_timestamp = None
+ else:
+ rev_1_2_timestamp = self[rev_1_2_id].timestamp
+
+ ntdbr_cvs_revs = []
+ for cvs_rev in vendor_lod_items.cvs_revisions:
+ if rev_1_2_timestamp is not None \
+ and cvs_rev.timestamp >= rev_1_2_timestamp:
+ # That's the end of the once-default branch.
+ break
+ ntdbr_cvs_revs.append(cvs_rev)
+
+ if ntdbr_cvs_revs:
+ self.adjust_ntdbrs(ntdbr_cvs_revs)
+ return True
+ else:
+ return False
+
+ def imported_remove_1_1(self, vendor_lod_items):
+ """This file was imported. Remove the 1.1 revision if possible.
+
+ VENDOR_LOD_ITEMS is the LODItems instance for the vendor branch.
+ See adjust_ntdbrs() for more information."""
+
+ assert vendor_lod_items.cvs_revisions
+ cvs_rev = vendor_lod_items.cvs_revisions[0]
+
+ if isinstance(cvs_rev, CVSRevisionModification) \
+ and not cvs_rev.deltatext_exists:
+ cvs_branch = vendor_lod_items.cvs_branch
+ rev_1_1 = self[cvs_branch.source_id]
+ assert isinstance(rev_1_1, CVSRevision)
+ Log().debug('Removing unnecessary revision %s' % (rev_1_1,))
+
+ # Delete the 1.1.1 CVSBranch and sever the vendor branch from trunk:
+ self._sever_branch(vendor_lod_items)
+
+ # Delete rev_1_1:
+ self.root_ids.remove(rev_1_1.id)
+ del self[rev_1_1.id]
+ rev_1_2_id = rev_1_1.next_id
+ if rev_1_2_id is not None:
+ rev_1_2 = self[rev_1_2_id]
+ rev_1_2.prev_id = None
+ self.root_ids.add(rev_1_2.id)
+
+ # Move any tags and branches from rev_1_1 to cvs_rev:
+ cvs_rev.tag_ids.extend(rev_1_1.tag_ids)
+ for id in rev_1_1.tag_ids:
+ cvs_tag = self[id]
+ cvs_tag.source_lod = cvs_rev.lod
+ cvs_tag.source_id = cvs_rev.id
+ cvs_rev.branch_ids[0:0] = rev_1_1.branch_ids
+ for id in rev_1_1.branch_ids:
+ cvs_branch = self[id]
+ cvs_branch.source_lod = cvs_rev.lod
+ cvs_branch.source_id = cvs_rev.id
+ cvs_rev.branch_commit_ids[0:0] = rev_1_1.branch_commit_ids
+ for id in rev_1_1.branch_commit_ids:
+ cvs_rev2 = self[id]
+ cvs_rev2.prev_id = cvs_rev.id
+
+ def _delete_unneeded(self, cvs_item, metadata_db):
+ if isinstance(cvs_item, CVSRevisionNoop) \
+ and cvs_item.rev == '1.1' \
+ and isinstance(cvs_item.lod, Trunk) \
+ and len(cvs_item.branch_ids) >= 1 \
+ and self[cvs_item.branch_ids[0]].next_id is not None \
+ and not cvs_item.closed_symbols \
+ and not cvs_item.ntdbr:
+ # FIXME: This message will not match if the RCS file was renamed
+ # manually after it was created.
+ log_msg = metadata_db[cvs_item.metadata_id].log_msg
+ cvs_generated_msg = 'file %s was initially added on branch %s.\n' % (
+ self.cvs_file.basename,
+ self[cvs_item.branch_ids[0]].symbol.name,)
+ return log_msg == cvs_generated_msg
+ else:
+ return False
+
+ def remove_unneeded_deletes(self, metadata_db):
+ """Remove unneeded deletes for this file.
+
+ If a file is added on a branch, then a trunk revision is added at
+ the same time in the 'Dead' state. This revision doesn't do
+ anything useful, so delete it."""
+
+ for id in self.root_ids:
+ cvs_item = self[id]
+ if self._delete_unneeded(cvs_item, metadata_db):
+ Log().debug('Removing unnecessary delete %s' % (cvs_item,))
+
+ # Delete cvs_item:
+ self.root_ids.remove(cvs_item.id)
+ del self[id]
+ if cvs_item.next_id is not None:
+ cvs_rev_next = self[cvs_item.next_id]
+ cvs_rev_next.prev_id = None
+ self.root_ids.add(cvs_rev_next.id)
+
+ # Delete all CVSBranches rooted at this revision. If there is
+ # a CVSRevision on the branch, it should already be an add so
+ # it doesn't have to be changed.
+ for cvs_branch_id in cvs_item.branch_ids:
+ cvs_branch = self[cvs_branch_id]
+ del self[cvs_branch.id]
+
+ if cvs_branch.next_id is not None:
+ cvs_branch_next = self[cvs_branch.next_id]
+ cvs_branch_next.first_on_branch_id = None
+ cvs_branch_next.prev_id = None
+ self.root_ids.add(cvs_branch_next.id)
+
+ # Tagging a dead revision doesn't do anything, so remove any
+ # tags that were set on 1.1:
+ for cvs_tag_id in cvs_item.tag_ids:
+ del self[cvs_tag_id]
+
+ # This can only happen once per file, and we might have just
+ # changed self.root_ids, so break out of the loop:
+ break
+
+ def _initial_branch_delete_unneeded(self, lod_items, metadata_db):
+ """Return True iff the initial revision in LOD_ITEMS can be deleted."""
+
+ if lod_items.cvs_branch is not None \
+ and lod_items.cvs_branch.source_id is not None \
+ and len(lod_items.cvs_revisions) >= 2:
+ cvs_revision = lod_items.cvs_revisions[0]
+ cvs_rev_source = self[lod_items.cvs_branch.source_id]
+ if isinstance(cvs_revision, CVSRevisionAbsent) \
+ and not cvs_revision.tag_ids \
+ and not cvs_revision.branch_ids \
+ and abs(cvs_revision.timestamp - cvs_rev_source.timestamp) <= 2:
+ # FIXME: This message will not match if the RCS file was renamed
+ # manually after it was created.
+ log_msg = metadata_db[cvs_revision.metadata_id].log_msg
+ return bool(re.match(
+ r'file %s was added on branch .* on '
+ r'\d{4}\-\d{2}\-\d{2} \d{2}\:\d{2}\:\d{2}( [\+\-]\d{4})?'
+ '\n' % (re.escape(self.cvs_file.basename),),
+ log_msg,
+ ))
+ return False
+
+ def remove_initial_branch_deletes(self, metadata_db):
+ """If the first revision on a branch is an unnecessary delete, remove it.
+
+ If a file is added on a branch (whether or not it already existed
+ on trunk), then new versions of CVS add a first branch revision in
+ the 'dead' state (to indicate that the file did not exist on the
+ branch when the branch was created) followed by the second branch
+ revision, which is an add. When we encounter this situation, we
+ sever the branch from trunk and delete the first branch
+ revision."""
+
+ for lod_items in self.iter_lods():
+ if self._initial_branch_delete_unneeded(lod_items, metadata_db):
+ cvs_revision = lod_items.cvs_revisions[0]
+ Log().debug(
+ 'Removing unnecessary initial branch delete %s' % (cvs_revision,)
+ )
+ cvs_branch = lod_items.cvs_branch
+ cvs_rev_source = self[cvs_branch.source_id]
+ cvs_rev_next = lod_items.cvs_revisions[1]
+
+ # Delete cvs_revision:
+ del self[cvs_revision.id]
+ cvs_rev_next.prev_id = None
+ self.root_ids.add(cvs_rev_next.id)
+ cvs_rev_source.branch_commit_ids.remove(cvs_revision.id)
+
+ # Delete the CVSBranch on which it is located:
+ del self[cvs_branch.id]
+ cvs_rev_source.branch_ids.remove(cvs_branch.id)
+
+ def _exclude_tag(self, cvs_tag):
+ """Exclude the specified CVS_TAG."""
+
+ del self[cvs_tag.id]
+
+ # A CVSTag is the successor of the CVSRevision that it
+ # sprouts from. Delete this tag from that revision's
+ # tag_ids:
+ self[cvs_tag.source_id].tag_ids.remove(cvs_tag.id)
+
+ def _exclude_branch(self, lod_items):
+ """Exclude the branch described by LOD_ITEMS, including its revisions.
+
+ (Do not update the LOD_ITEMS instance itself.)
+
+ If the LOD starts with non-trunk default branch revisions, leave
+ the branch and the NTDB revisions in place, but delete any
+ subsequent revisions that are not NTDB revisions. In this case,
+ return True; otherwise return False"""
+
+ if lod_items.cvs_revisions and lod_items.cvs_revisions[0].ntdbr:
+ for cvs_rev in lod_items.cvs_revisions:
+ if not cvs_rev.ntdbr:
+ # We've found the first non-NTDBR, and it's stored in cvs_rev:
+ break
+ else:
+ # There was no revision following the NTDBRs:
+ cvs_rev = None
+
+ if cvs_rev:
+ last_ntdbr = self[cvs_rev.prev_id]
+ last_ntdbr.next_id = None
+ while True:
+ del self[cvs_rev.id]
+ if cvs_rev.next_id is None:
+ break
+ cvs_rev = self[cvs_rev.next_id]
+
+ return True
+
+ else:
+ if lod_items.cvs_branch is not None:
+ # Delete the CVSBranch itself:
+ cvs_branch = lod_items.cvs_branch
+
+ del self[cvs_branch.id]
+
+ # A CVSBranch is the successor of the CVSRevision that it
+ # sprouts from. Delete this branch from that revision's
+ # branch_ids:
+ self[cvs_branch.source_id].branch_ids.remove(cvs_branch.id)
+
+ if lod_items.cvs_revisions:
+ # The first CVSRevision on the branch has to be either detached
+ # from the revision from which the branch sprang, or removed
+ # from self.root_ids:
+ cvs_rev = lod_items.cvs_revisions[0]
+ if cvs_rev.prev_id is None:
+ self.root_ids.remove(cvs_rev.id)
+ else:
+ self[cvs_rev.prev_id].branch_commit_ids.remove(cvs_rev.id)
+
+ for cvs_rev in lod_items.cvs_revisions:
+ del self[cvs_rev.id]
+
+ return False
+
+ def graft_ntdbr_to_trunk(self):
+ """Graft the non-trunk default branch revisions to trunk.
+
+ They should already be alone on a branch that may or may not have
+ a CVSBranch connecting it to trunk."""
+
+ for lod_items in self.iter_lods():
+ if lod_items.cvs_revisions and lod_items.cvs_revisions[0].ntdbr:
+ assert lod_items.is_pure_ntdb()
+
+ first_rev = lod_items.cvs_revisions[0]
+ last_rev = lod_items.cvs_revisions[-1]
+ rev_1_1 = self.get(first_rev.prev_id)
+ rev_1_2 = self.get(last_rev.ntdbr_next_id)
+
+ if lod_items.cvs_branch is not None:
+ self._sever_branch(lod_items)
+
+ if rev_1_1 is not None:
+ rev_1_1.next_id = first_rev.id
+ first_rev.prev_id = rev_1_1.id
+
+ self.root_ids.remove(first_rev.id)
+
+ first_rev.__class__ = cvs_revision_type_map[(
+ isinstance(first_rev, CVSRevisionModification),
+ isinstance(rev_1_1, CVSRevisionModification),
+ )]
+
+ if rev_1_2 is not None:
+ rev_1_2.ntdbr_prev_id = None
+ last_rev.ntdbr_next_id = None
+
+ if rev_1_2.prev_id is None:
+ self.root_ids.remove(rev_1_2.id)
+
+ rev_1_2.prev_id = last_rev.id
+ last_rev.next_id = rev_1_2.id
+
+ # The effective_pred_id of rev_1_2 was not changed, so we
+ # don't have to change rev_1_2's type.
+
+ for cvs_rev in lod_items.cvs_revisions:
+ cvs_rev.ntdbr = False
+ cvs_rev.lod = self.trunk
+
+ for cvs_branch in lod_items.cvs_branches:
+ cvs_branch.source_lod = self.trunk
+
+ for cvs_tag in lod_items.cvs_tags:
+ cvs_tag.source_lod = self.trunk
+
+ return
+
+ def exclude_non_trunk(self):
+ """Delete all tags and branches."""
+
+ ntdbr_excluded = False
+ for lod_items in self.iter_lods():
+ for cvs_tag in lod_items.cvs_tags[:]:
+ self._exclude_tag(cvs_tag)
+ lod_items.cvs_tags.remove(cvs_tag)
+
+ if not isinstance(lod_items.lod, Trunk):
+ assert not lod_items.cvs_branches
+
+ ntdbr_excluded |= self._exclude_branch(lod_items)
+
+ if ntdbr_excluded:
+ self.graft_ntdbr_to_trunk()
+
+ def filter_excluded_symbols(self, revision_excluder):
+ """Delete any excluded symbols and references to them.
+
+ Call the revision_excluder's callback methods to let it know what
+ is being excluded."""
+
+ ntdbr_excluded = False
+ for lod_items in self.iter_lods():
+ # Delete any excluded tags:
+ for cvs_tag in lod_items.cvs_tags[:]:
+ if isinstance(cvs_tag.symbol, ExcludedSymbol):
+ self._exclude_tag(cvs_tag)
+
+ lod_items.cvs_tags.remove(cvs_tag)
+
+ # Delete the whole branch if it is to be excluded:
+ if isinstance(lod_items.lod, ExcludedSymbol):
+ # A symbol can only be excluded if no other symbols spring
+ # from it. This was already checked in CollateSymbolsPass, so
+ # these conditions should already be satisfied.
+ assert not list(lod_items.iter_blockers())
+
+ ntdbr_excluded |= self._exclude_branch(lod_items)
+
+ if ntdbr_excluded:
+ self.graft_ntdbr_to_trunk()
+
+ revision_excluder.process_file(self)
+
+ def _mutate_branch_to_tag(self, cvs_branch):
+ """Mutate the branch CVS_BRANCH into a tag."""
+
+ if cvs_branch.next_id is not None:
+ # This shouldn't happen because it was checked in
+ # CollateSymbolsPass:
+ raise FatalError('Attempt to exclude a branch with commits.')
+ cvs_tag = CVSTag(
+ cvs_branch.id, cvs_branch.cvs_file, cvs_branch.symbol,
+ cvs_branch.source_lod, cvs_branch.source_id,
+ cvs_branch.revision_recorder_token,
+ )
+ self.add(cvs_tag)
+ cvs_revision = self[cvs_tag.source_id]
+ cvs_revision.branch_ids.remove(cvs_tag.id)
+ cvs_revision.tag_ids.append(cvs_tag.id)
+
+ def _mutate_tag_to_branch(self, cvs_tag):
+ """Mutate the tag into a branch."""
+
+ cvs_branch = CVSBranch(
+ cvs_tag.id, cvs_tag.cvs_file, cvs_tag.symbol,
+ None, cvs_tag.source_lod, cvs_tag.source_id, None,
+ cvs_tag.revision_recorder_token,
+ )
+ self.add(cvs_branch)
+ cvs_revision = self[cvs_branch.source_id]
+ cvs_revision.tag_ids.remove(cvs_branch.id)
+ cvs_revision.branch_ids.append(cvs_branch.id)
+
+ def _mutate_symbol(self, cvs_symbol):
+ """Mutate CVS_SYMBOL if necessary."""
+
+ symbol = cvs_symbol.symbol
+ if isinstance(cvs_symbol, CVSBranch) and isinstance(symbol, Tag):
+ self._mutate_branch_to_tag(cvs_symbol)
+ elif isinstance(cvs_symbol, CVSTag) and isinstance(symbol, Branch):
+ self._mutate_tag_to_branch(cvs_symbol)
+
+ def mutate_symbols(self):
+ """Force symbols to be tags/branches based on self.symbol_db."""
+
+ for cvs_item in self.values():
+ if isinstance(cvs_item, CVSRevision):
+ # This CVSRevision may be affected by the mutation of any
+ # CVSSymbols that it references, but there is nothing to do
+ # here directly.
+ pass
+ elif isinstance(cvs_item, CVSSymbol):
+ self._mutate_symbol(cvs_item)
+ else:
+ raise RuntimeError('Unknown cvs item type')
+
+ def _adjust_tag_parent(self, cvs_tag):
+ """Adjust the parent of CVS_TAG if possible and preferred.
+
+ CVS_TAG is an instance of CVSTag. This method must be called in
+ leaf-to-trunk order."""
+
+ # The Symbol that cvs_tag would like to have as a parent:
+ preferred_parent = Ctx()._symbol_db.get_symbol(
+ cvs_tag.symbol.preferred_parent_id)
+
+ if cvs_tag.source_lod == preferred_parent:
+ # The preferred parent is already the parent.
+ return
+
+ # The CVSRevision that is its direct parent:
+ source = self[cvs_tag.source_id]
+ assert isinstance(source, CVSRevision)
+
+ if isinstance(preferred_parent, Trunk):
+ # It is not possible to graft *onto* Trunk:
+ return
+
+ # Try to find the preferred parent among the possible parents:
+ for branch_id in source.branch_ids:
+ if self[branch_id].symbol == preferred_parent:
+ # We found it!
+ break
+ else:
+ # The preferred parent is not a possible parent in this file.
+ return
+
+ parent = self[branch_id]
+ assert isinstance(parent, CVSBranch)
+
+ Log().debug('Grafting %s from %s (on %s) onto %s' % (
+ cvs_tag, source, source.lod, parent,))
+ # Switch parent:
+ source.tag_ids.remove(cvs_tag.id)
+ parent.tag_ids.append(cvs_tag.id)
+ cvs_tag.source_lod = parent.symbol
+ cvs_tag.source_id = parent.id
+
+ def _adjust_branch_parents(self, cvs_branch):
+ """Adjust the parent of CVS_BRANCH if possible and preferred.
+
+ CVS_BRANCH is an instance of CVSBranch. This method must be
+ called in leaf-to-trunk order."""
+
+ # The Symbol that cvs_branch would like to have as a parent:
+ preferred_parent = Ctx()._symbol_db.get_symbol(
+ cvs_branch.symbol.preferred_parent_id)
+
+ if cvs_branch.source_lod == preferred_parent:
+ # The preferred parent is already the parent.
+ return
+
+ # The CVSRevision that is its direct parent:
+ source = self[cvs_branch.source_id]
+ # This is always a CVSRevision because we haven't adjusted it yet:
+ assert isinstance(source, CVSRevision)
+
+ if isinstance(preferred_parent, Trunk):
+ # It is not possible to graft *onto* Trunk:
+ return
+
+ # Try to find the preferred parent among the possible parents:
+ for branch_id in source.branch_ids:
+ possible_parent = self[branch_id]
+ if possible_parent.symbol == preferred_parent:
+ # We found it!
+ break
+ elif possible_parent.symbol == cvs_branch.symbol:
+ # Only branches that precede the branch to be adjusted are
+ # considered possible parents. Leave parentage unchanged:
+ return
+ else:
+ # This point should never be reached.
+ raise InternalError(
+ 'Possible parent search did not terminate as expected')
+
+ parent = possible_parent
+ assert isinstance(parent, CVSBranch)
+
+ Log().debug('Grafting %s from %s (on %s) onto %s' % (
+ cvs_branch, source, source.lod, parent,))
+ # Switch parent:
+ source.branch_ids.remove(cvs_branch.id)
+ parent.branch_ids.append(cvs_branch.id)
+ cvs_branch.source_lod = parent.symbol
+ cvs_branch.source_id = parent.id
+
+ def adjust_parents(self):
+ """Adjust the parents of symbols to their preferred parents.
+
+ If a CVSSymbol has a preferred parent that is different than its
+ current parent, and if the preferred parent is an allowed parent
+ of the CVSSymbol in this file, then graft the CVSSymbol onto its
+ preferred parent."""
+
+ for lod_items in self.iter_lods():
+ for cvs_tag in lod_items.cvs_tags:
+ self._adjust_tag_parent(cvs_tag)
+
+ for cvs_branch in lod_items.cvs_branches:
+ self._adjust_branch_parents(cvs_branch)
+
+ def _get_revision_source(self, cvs_symbol):
+ """Return the CVSRevision that is the ultimate source of CVS_SYMBOL."""
+
+ while True:
+ cvs_item = self[cvs_symbol.source_id]
+ if isinstance(cvs_item, CVSRevision):
+ return cvs_item
+ else:
+ cvs_symbol = cvs_item
+
+ def refine_symbols(self):
+ """Refine the types of the CVSSymbols in this file.
+
+ Adjust the symbol types based on whether the source exists:
+ CVSBranch vs. CVSBranchNoop and CVSTag vs. CVSTagNoop."""
+
+ for lod_items in self.iter_lods():
+ for cvs_tag in lod_items.cvs_tags:
+ source = self._get_revision_source(cvs_tag)
+ cvs_tag.__class__ = cvs_tag_type_map[
+ isinstance(source, CVSRevisionModification)
+ ]
+
+ for cvs_branch in lod_items.cvs_branches:
+ source = self._get_revision_source(cvs_branch)
+ cvs_branch.__class__ = cvs_branch_type_map[
+ isinstance(source, CVSRevisionModification)
+ ]
+
+ def record_opened_symbols(self):
+ """Set CVSRevision.opened_symbols for the surviving revisions."""
+
+ for cvs_item in self.values():
+ if isinstance(cvs_item, (CVSRevision, CVSBranch)):
+ cvs_item.opened_symbols = []
+ for cvs_symbol_opened_id in cvs_item.get_cvs_symbol_ids_opened():
+ cvs_symbol_opened = self[cvs_symbol_opened_id]
+ cvs_item.opened_symbols.append(
+ (cvs_symbol_opened.symbol.id, cvs_symbol_opened.id,)
+ )
+
+ def record_closed_symbols(self):
+ """Set CVSRevision.closed_symbols for the surviving revisions.
+
+ A CVSRevision closes the symbols that were opened by the CVSItems
+ that the CVSRevision closes. Got it?
+
+ This method must be called after record_opened_symbols()."""
+
+ for cvs_item in self.values():
+ if isinstance(cvs_item, CVSRevision):
+ cvs_item.closed_symbols = []
+ for cvs_item_closed_id in cvs_item.get_ids_closed():
+ cvs_item_closed = self[cvs_item_closed_id]
+ cvs_item.closed_symbols.extend(cvs_item_closed.opened_symbols)
+
+