Back to index

plone3  3.1.7
VersionHistory.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 # 
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE
00011 # 
00012 ##############################################################################
00013 
00014 __version__='$Revision: 1.4 $'[11:-2]
00015 
00016 from Globals import InitializeClass, Persistent
00017 from AccessControl import ClassSecurityInfo
00018 from EventLog import EventLog, LogEntry
00019 from Utility import VersionControlError
00020 from ZopeVersion import ZopeVersion
00021 from BTrees.IOBTree import IOBTree
00022 from BTrees.IIBTree import IIBTree
00023 from BTrees.OOBTree import OOBTree
00024 from Acquisition import Implicit
00025 import sys, time
00026 
00027 
00028 class VersionHistory(Implicit, Persistent):
00029     """A version history maintains the information about the changes
00030        to a particular version-controlled resource over time."""
00031 
00032     def __init__(self, history_id, object):
00033         # The _versions mapping maps version ids to version objects. All
00034         # of the actual version data is looked up there. The _labels
00035         # mapping maps labels to specific version ids. The _branches map
00036         # manages BranchInfo objects that maintain branch information.
00037         self._eventLog = EventLog()
00038         self._versions = OOBTree()
00039         self._branches = OOBTree()
00040         self._labels = OOBTree()
00041         mainline = self.createBranch('mainline', None)
00042         self.id = history_id
00043 
00044     security = ClassSecurityInfo()
00045 
00046     security.declarePublic('getId')
00047     def getId(self):
00048         return self.id
00049 
00050     security.declarePrivate('addLogEntry')
00051     def addLogEntry(self, version_id, action, path=None, message=''):
00052         """Add a new log entry associated with this version history."""
00053         entry = LogEntry(version_id, action, path, message)
00054         self._eventLog.addEntry(entry)
00055 
00056     security.declarePrivate('getLogEntries')
00057     def getLogEntries(self):
00058         """Return a sequence of the log entries for this version history."""
00059         return self._eventLog.getEntries()
00060 
00061     security.declarePrivate('getLabels')
00062     def getLabels(self):
00063         return self._labels.keys()
00064 
00065     security.declarePrivate('labelVersion')
00066     def labelVersion(self, version_id, label, force=0):
00067         """Associate a particular version in a version history with the
00068            given label, removing any existing association with that label
00069            if force is true, or raising an error if force is false and
00070            an association with the given label already exists."""
00071         current = self._labels.get(label)
00072         if current is not None:
00073             if current == version_id:
00074                 return
00075             if not force:
00076                 raise VersionControlError(
00077                     'The label %s is already associated with a version.' % (
00078                      label
00079                     ))
00080             del self._labels[label]
00081         self._labels[label] = version_id
00082 
00083     security.declarePrivate('createBranch')
00084     def createBranch(self, branch_id, version_id):
00085         """Create a new branch associated with the given branch_id. The
00086            new branch is rooted at the version named by version_id."""
00087         if self._branches.has_key(branch_id):
00088             raise VersionControlError(
00089                 'Activity already exists: %s' % branch_id
00090                 )
00091         branch = BranchInfo(branch_id, version_id)
00092         self._branches[branch_id] = branch
00093         return branch
00094 
00095     security.declarePrivate('createVersion')
00096     def createVersion(self, object, branch_id):
00097         """Create a new version in the line of descent named by the given
00098            branch_id, returning the newly created version object."""
00099         branch = self._branches.get(branch_id)
00100         if branch is None:
00101             branch = self.createBranch(branch_id, None)
00102         if branch.name != 'mainline':
00103             version_id = '%s.%d' % (branch.name, len(branch) + 1)
00104         else:
00105             version_id = '%d' % (len(branch) + 1)
00106         version = ZopeVersion(version_id, object)
00107 
00108         # Update the  predecessor, successor and branch relationships.
00109         # This is something of a hedge against the future. Versions will
00110         # always know enough to reconstruct their lineage without the help
00111         # of optimized data structures, which will make it easier to change
00112         # internals in the future if we need to.
00113         latest = branch.latest()
00114         if latest is not None:
00115             last = self._versions[latest]
00116             last.next = last.next + (version_id,)
00117             version.prev = latest
00118 
00119         # If the branch is not the mainline, store the branch name in the
00120         # version. Versions have 'mainline' as the default class attribute
00121         # which is the common case and saves a minor bit of storage space.
00122         if branch.name != 'mainline':
00123             version.branch = branch.name
00124 
00125         branch.append(version)
00126         self._versions[version_id] = version
00127         # Call saveState() only after version has been linked into the
00128         # database, ensuring it goes into the correct database.
00129         version.saveState(object)
00130         return version.__of__(self)
00131 
00132     security.declarePrivate('hasVersionId')
00133     def hasVersionId(self, version_id):
00134         """Return true if history contains a version with the given id."""
00135         return self._versions.has_key(version_id)
00136 
00137     security.declarePrivate('isLatestVersion')
00138     def isLatestVersion(self, version_id, branch_id):
00139         """Return true if version id is the latest in its branch."""
00140         branch = self._branches[branch_id]
00141         return version_id == branch.latest()
00142 
00143     security.declarePrivate('getLatestVersion')
00144     def getLatestVersion(self, branch_id):
00145         """Return the latest version object within the given branch, or
00146            None if the branch contains no versions."""
00147         branch = self._branches[branch_id]
00148         version = self._versions[branch.latest()]
00149         return version.__of__(self)
00150 
00151     security.declarePrivate('findBranchId')
00152     def findBranchId(self, version_id):
00153         """Given a version id, return the id of the branch of the version.
00154            Note that we cheat, since we can find this out from the id."""
00155         parts = version_id.split('.')
00156         if len(parts) > 1:
00157             return parts[-2]
00158         return 'mainline'
00159 
00160     security.declarePrivate('getVersionById')
00161     def getVersionById(self, version_id):
00162         """Return the version object named by the given version id, or
00163            raise a VersionControlError if the version is not found."""
00164         version = self._versions.get(version_id)
00165         if version is None:
00166             raise VersionControlError(
00167                 'Unknown version id: %s' % version_id
00168                 )
00169         return version.__of__(self)
00170 
00171     security.declarePrivate('getVersionByLabel')
00172     def getVersionByLabel(self, label):
00173         """Return the version associated with the given label, or None
00174            if no version matches the given label."""
00175         version_id = self._labels.get(label)
00176         version = self._versions.get(version_id)
00177         if version is None:
00178             return None
00179         return version.__of__(self)
00180 
00181     security.declarePrivate('getVersionByDate')
00182     def getVersionByDate(self, branch_id, timestamp):
00183         """Return the last version committed in the given branch on or
00184            before the given time value. The timestamp should be a float
00185            (time.time format) value in UTC."""
00186         branch = self._branches[branch_id]
00187         tvalue = int(timestamp / 60.0)
00188         while 1:
00189             # Try to find a version with a commit date <= the given time
00190             # using the timestamp index in the branch information.
00191             if branch.m_order:
00192                 try:
00193                     match = branch.m_date.maxKey(tvalue)
00194                     match = branch.m_order[branch.m_date[match]]
00195                     return self._versions[match].__of__(self)
00196                 except ValueError:
00197                     pass
00198 
00199             # If we've run out of lineage without finding a version with
00200             # a commit date <= the given timestamp, we return None. It is
00201             # up to the caller to decide what to do in this situation.
00202             if branch.root is None:
00203                 return None
00204             
00205             # If the branch has a root (a version in another branch), then
00206             # we check the root and do it again with the ancestor branch.
00207             rootver = self._versions[branch.root]
00208             if int(rootver.date_created / 60.0) < tvalue:
00209                 return rootver.__of__(self)
00210             branch = self._branches[rootver.branch]
00211 
00212     security.declarePrivate('getVersionIds')
00213     def getVersionIds(self, branch_id=None):
00214         """Return a sequence of version ids for the versions in this
00215            version history. If a branch_id is given, only version ids
00216            from that branch will be returned. Note that the sequence
00217            of ids returned does not include the id of the branch root."""
00218         if branch_id is not None:
00219             return self._branches[branch_id].versionIds()
00220         return self._versions.keys()
00221 
00222 InitializeClass(VersionHistory)
00223 
00224 
00225 class BranchInfo(Implicit, Persistent):
00226     """A utility class to hold branch (line-of-descent) information. It
00227        maintains the name of the branch, the version id of the root of
00228        the branch and indices to allow for efficient lookups."""
00229 
00230     def __init__(self, name, root):
00231         # m_order maintains a newest-first mapping of int -> version id.
00232         # m_date maintains a mapping of a packed date (int # of minutes
00233         # since the epoch) to a lookup key in m_order. The two structures
00234         # are separate because we only support minute precision for date
00235         # lookups (and multiple versions could be added in a minute).
00236         self.date_created = time.time()
00237         self.m_order = IOBTree()
00238         self.m_date = IIBTree()
00239         self.name = name
00240         self.root = root
00241 
00242     def append(self, version):
00243         """Append a version to the branch information. Note that this
00244            does not store the actual version, but metadata about the
00245            version to support ordering and date lookups."""
00246         if len(self.m_order):
00247             key = self.m_order.minKey() - 1
00248         else: key = sys.maxint
00249         self.m_order[key] = version.id
00250         timestamp = int(version.date_created / 60.0)
00251         self.m_date[timestamp] = key
00252 
00253     def versionIds(self):
00254         """Return a newest-first sequence of version ids in the branch."""
00255         return self.m_order.values()
00256 
00257     def latest(self):
00258         """Return the version id of the latest version in the branch."""
00259         mapping = self.m_order
00260         if not len(mapping):
00261             return self.root
00262         return mapping[mapping.keys()[0]]
00263 
00264     def __len__(self):
00265         return len(self.m_order)
00266 
00267 InitializeClass(BranchInfo)