[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/versioncontrol/
Ported Zope Vesion Control to Zope 3
Jim Fulton
jim at zope.com
Sat Sep 11 13:08:11 EDT 2004
Log message for revision 27498:
Ported Zope Vesion Control to Zope 3
This is work done by Fred Drake and myself, but mostly by Fred.
To do:
- Provide a basic UI and functional tests
- Complete the configuration (the current configuration,
which just captures a permission declaration) isn't even tied
in with the application server.
- Provide packaging files.
- We need to review the issues in isues.txt, removing the ones that
have been resolved.
- Probably change the strategy for copying into the repo so that the
objects in the repo are whole.
Changed:
A Zope3/trunk/src/zope/app/versioncontrol/
A Zope3/trunk/src/zope/app/versioncontrol/README.txt
A Zope3/trunk/src/zope/app/versioncontrol/__init__.py
A Zope3/trunk/src/zope/app/versioncontrol/configure.zcml
A Zope3/trunk/src/zope/app/versioncontrol/history.py
A Zope3/trunk/src/zope/app/versioncontrol/interfaces.py
A Zope3/trunk/src/zope/app/versioncontrol/issues.txt
A Zope3/trunk/src/zope/app/versioncontrol/nonversioned.py
A Zope3/trunk/src/zope/app/versioncontrol/repository.py
A Zope3/trunk/src/zope/app/versioncontrol/tests.py
A Zope3/trunk/src/zope/app/versioncontrol/utility.py
A Zope3/trunk/src/zope/app/versioncontrol/version.py
-=-
Added: Zope3/trunk/src/zope/app/versioncontrol/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/README.txt 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/README.txt 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,733 @@
+Version Control
+===============
+
+This package provides a framework for managing multiple versions of
+objects within a ZODB database. The framework defines several
+interfaces that objects may provide to participate with the framework.
+For an object to particpate in version control, it must provide
+IVersionable. IVersionable is an interface that promises that there
+will be adapters to:
+
+- INonVersionedData, and
+
+- IPhysicallyLocatable.
+
+It also requires that instances support IPersistent and IAnnotatable.
+
+Normally, these interfaces will be provided by adapters. To simplify
+the example, we'll just create a class that already implements the
+required interfaces directly. We need to be careful to avoid
+including the __name__ and __parent__ attributes in state copies, so
+even a fairly simple implementation of INonVersionedData has to deal
+with these for objects that contain their own location information.
+
+::
+
+ >>> import persistent
+ >>> import zope.interface
+ >>> import zope.app.annotation.attribute
+ >>> import zope.app.annotation.interfaces
+ >>> import zope.app.traversing.interfaces
+ >>> from zope.app.versioncontrol import interfaces
+
+ >>> marker = object()
+
+ >>> class Sample(persistent.Persistent):
+ ... zope.interface.implements(
+ ... interfaces.IVersionable,
+ ... interfaces.INonVersionedData,
+ ... zope.app.annotation.interfaces.IAttributeAnnotatable,
+ ... zope.app.traversing.interfaces.IPhysicallyLocatable,
+ ... )
+ ...
+ ... # Methods defined by INonVersionedData
+ ... # This is a trivial implementation; using INonVersionedData
+ ... # is discussed later.
+ ...
+ ... def listNonVersionedObjects(self):
+ ... return ()
+ ...
+ ... def removeNonVersionedData(self):
+ ... if "__name__" in self.__dict__:
+ ... del self.__name__
+ ... if "__parent__" in self.__dict__:
+ ... del self.__parent__
+ ...
+ ... def getNonVersionedData(self):
+ ... return (getattr(self, "__name__", marker),
+ ... getattr(self, "__parent__", marker))
+ ...
+ ... def restoreNonVersionedData(self, data):
+ ... name, parent = data
+ ... if name is not marker:
+ ... self.__name__ = name
+ ... if parent is not marker:
+ ... self.__parent__ = parent
+ ...
+ ... # Method from IPhysicallyLocatable that is actually used:
+ ... def getPath(self):
+ ... return '/' + self.__name__
+
+ >>> from zope.app.tests import ztapi
+ >>> ztapi.provideAdapter(zope.app.annotation.interfaces.IAttributeAnnotatable,
+ ... zope.app.annotation.interfaces.IAnnotations,
+ ... zope.app.annotation.attribute.AttributeAnnotations)
+
+Now we need to create a database with an instance of our sample object
+to work with::
+
+ >>> from ZODB.tests import util
+ >>> db = util.DB()
+ >>> connection = db.open()
+ >>> root = connection.root()
+
+ >>> samp = Sample()
+ >>> samp.__name__ = "samp"
+ >>> root["samp"] = samp
+ >>> util.commit()
+
+Some basic queries may be asked of ojects without using an instance of
+IVersionControl. In particular, we can determine whether an object
+can be managed by version control by checking for the IVersionable
+interface::
+
+ >>> interfaces.IVersionable.providedBy(samp)
+ True
+ >>> interfaces.IVersionable.providedBy(42)
+ False
+
+We can also determine whether an object is actually under version
+control using the IVersioned interface::
+
+ >>> interfaces.IVersioned.providedBy(samp)
+ False
+ >>> interfaces.IVersioned.providedBy(42)
+ False
+
+Placing an object under version control requires an instance of an
+IVersionControl object. This package provides an implementation of
+this interface on the `Repository` class (from
+`zope.app.versioncontrol.repository`). Only the IVersionControl instance is
+responsible for providing version control operations; an instance
+should never be asked to perform operations directly.
+
+::
+
+ >>> import zope.app.versioncontrol.repository
+ >>> import zope.interface.verify
+
+ >>> repository = zope.app.versioncontrol.repository.Repository()
+ >>> zope.interface.verify.verifyObject(
+ ... interfaces.IVersionControl,
+ ... repository)
+ True
+
+In order to actually use version control, there must be an
+interaction. This is needed to allow the framework to determine the
+user making changes. Let's set up an interaction now. First we need a
+principal. For our purposes, a principal just needs to have an id::
+
+ >>> class FauxPrincipal:
+ ... def __init__(self, id):
+ ... self.id = id
+ >>> principal = FauxPrincipal('bob')
+
+Then we need to define an participation for the principal in the
+interaction::
+
+ >>> class FauxParticipation:
+ ... interaction=None
+ ... def __init__(self, principal):
+ ... self.principal = principal
+ >>> participation = FauxParticipation(principal)
+
+Finally, we can create the interaction::
+
+ >>> import zope.security.management
+ >>> zope.security.management.newInteraction(participation)
+
+Now, let's put an object under version control and verify that we can
+determine that fact by checking against the interface::
+
+ >>> repository.applyVersionControl(samp)
+ >>> interfaces.IVersioned.providedBy(samp)
+ True
+ >>> util.commit()
+
+Once an object is under version control, it's possible to get an
+information object that provides some interesting bits of data::
+
+ >>> info = repository.getVersionInfo(samp)
+ >>> type(info.history_id)
+ <type 'str'>
+
+It's an error to ask for the version info for an object which isn't
+under revision control::
+
+ >>> samp2 = Sample()
+ >>> repository.getVersionInfo(samp2)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+ >>> repository.getVersionInfo(42)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+You can retrieve a version of an object using the `.history_id` and a
+version selector. A version selector is a string that specifies which
+available version to return. The value 'mainline' tells the
+IVersionControl to return the most recent version on the main branch.
+
+::
+
+ >>> ob = repository.getVersionOfResource(info.history_id, 'mainline')
+ >>> type(ob)
+ <class 'zope.app.versioncontrol.README.Sample'>
+ >>> ob is samp
+ False
+ >>> root["ob"] = ob
+ >>> ob.__name__ = "ob"
+ >>> ob_info = repository.getVersionInfo(ob)
+ >>> ob_info.history_id == info.history_id
+ True
+ >>> ob_info is info
+ False
+
+Once version control has been applied, the object can be "checked
+out", modified and "checked in" to create new versions. For many
+applications, this parallels form-based changes to objects, but this
+is a matter of policy.
+
+Let's save some information about the current version of the object so
+we can see that it changes::
+
+ >>> orig_history_id = info.history_id
+ >>> orig_version_id = info.version_id
+
+Now, let's check out the object and add an attribute::
+
+ >>> repository.checkoutResource(ob)
+ >>> ob.value = 42
+ >>> repository.checkinResource(ob)
+ >>> util.commit()
+
+We can now compare information about the updated version with the
+original information::
+
+ >>> newinfo = repository.getVersionInfo(ob)
+ >>> newinfo.history_id == orig_history_id
+ True
+ >>> newinfo.version_id != orig_version_id
+ True
+
+Retrieving both versions of the object allows use to see the
+differences between the two::
+
+ >>> o1 = repository.getVersionOfResource(orig_history_id,
+ ... orig_version_id)
+ >>> o2 = repository.getVersionOfResource(orig_history_id,
+ ... newinfo.version_id)
+ >>> o1.value
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'Sample' object has no attribute 'value'
+ >>> o2.value
+ 42
+
+We can determine whether an object that's been checked out is
+up-to-date with the most recent version from the repository::
+
+ >>> repository.isResourceUpToDate(o1)
+ False
+ >>> repository.isResourceUpToDate(o2)
+ True
+
+Asking whether a non-versioned object is up-to-date produces an error::
+
+ >>> repository.isResourceUpToDate(42)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+ >>> repository.isResourceUpToDate(samp2)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+It's also possible to check whether an object has been changed since
+it was checked out. Since we're only looking at changes that have
+been committed to the database, we'll start by making a change and
+committing it without checking a new version into the version control
+repository.
+
+::
+
+ >>> repository.updateResource(samp)
+ >>> repository.checkoutResource(samp)
+ >>> util.commit()
+
+ >>> repository.isResourceChanged(samp)
+ False
+ >>> samp.value += 1
+ >>> util.commit()
+
+We can now see that the object has been changed since it was last
+checked in::
+
+ >>> repository.isResourceChanged(samp)
+ True
+
+Checking in the object and commiting shows that we can now veryify
+that the object is considered up-to-date after a subsequent checkout.
+We'll also demonstrate that `checkinResource()` can take an optional
+message argument; we'll see later how this can be used.
+
+::
+
+ >>> repository.checkinResource(samp, 'sample checkin')
+ >>> util.commit()
+
+ >>> repository.checkoutResource(samp)
+ >>> util.commit()
+
+ >>> repository.isResourceUpToDate(samp)
+ True
+ >>> repository.isResourceChanged(samp)
+ False
+ >>> repository.getVersionInfo(samp).version_id
+ '3'
+
+It's also possible to use version control to discard changes that
+haven't been checked in yet, even though they've been committed to the
+database for the "working copy". This is done using the
+`uncheckoutResource()` method of the IVersionControl object::
+
+ >>> samp.value
+ 43
+ >>> samp.value += 2
+ >>> samp.value
+ 45
+ >>> util.commit()
+ >>> repository.isResourceChanged(samp)
+ True
+ >>> repository.uncheckoutResource(samp)
+ >>> util.commit()
+
+ >>> samp.value
+ 43
+ >>> repository.isResourceChanged(samp)
+ False
+ >>> version_id = repository.getVersionInfo(samp).version_id
+ >>> version_id
+ '3'
+
+An old copy of an object can be "updated" to the most recent version
+of an object::
+
+ >>> ob = repository.getVersionOfResource(orig_history_id, orig_version_id)
+ >>> ob.__name__ = "foo"
+ >>> repository.isResourceUpToDate(ob)
+ False
+ >>> repository.getVersionInfo(ob).version_id
+ '1'
+ >>> repository.updateResource(ob, version_id)
+ >>> repository.getVersionInfo(ob).version_id == version_id
+ True
+ >>> ob.value
+ 43
+
+It's possible to get a list of all the versions of a particular object
+from the repository as well. We can use any copy of the object to
+make the request::
+
+ >>> list(repository.getVersionIds(samp))
+ ['1', '2', '3']
+ >>> list(repository.getVersionIds(ob))
+ ['1', '2', '3']
+
+No version information is available for objects that have not had
+version control applied::
+
+ >>> repository.getVersionIds(samp2)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+ >>> repository.getVersionIds(42)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: Object is not under version control.
+
+
+Naming specific revisions
+-------------------------
+
+Similar to other version control systems, specific versions may be
+given symbolic names, and these names may be used to retrieve versions
+from the repository. This package calls these names *labels*; they
+are similar to *tags* in CVS.
+
+Labels can be assigned to objects that are checked into the
+repository::
+
+ >>> repository.labelResource(samp, 'my-first-label')
+ >>> repository.labelResource(samp, 'my-second-label')
+
+The list of labels assigned to some version of an object can be
+retrieved using the repository's `getLabelsForResource()` method::
+
+ >>> list(repository.getLabelsForResource(samp))
+ ['my-first-label', 'my-second-label']
+
+The labels can be retrieved using any object that refers to the same
+line of history in the repository::
+
+ >>> list(repository.getLabelsForResource(ob))
+ ['my-first-label', 'my-second-label']
+
+Labels can be used to retrieve specific versions of an object from the
+repository::
+
+ >>> repository.getVersionInfo(samp).version_id
+ '3'
+ >>> ob = repository.getVersionOfResource(orig_history_id, 'my-first-label')
+ >>> repository.getVersionInfo(ob).version_id
+ '3'
+
+It's also possible to move a label from one version to another, but
+only when this is specifically indicated as allowed::
+
+ >>> ob = repository.getVersionOfResource(orig_history_id, orig_version_id)
+ >>> ob.__name__ = "bar"
+ >>> repository.labelResource(ob, 'my-second-label')
+ Traceback (most recent call last):
+ ...
+ VersionControlError: The label my-second-label is already associated with a version.
+ >>> repository.labelResource(ob, 'my-second-label', force=True)
+
+Labels can also be used to update an object to a specific version:
+
+ >>> repository.getVersionInfo(ob).version_id
+ '1'
+ >>> repository.updateResource(ob, 'my-first-label')
+ >>> repository.getVersionInfo(ob).version_id
+ '3'
+ >>> ob.value
+ 43
+
+
+Sticky settings
+---------------
+
+Similar to CVS, this package supports a sort of "sticky" updating: if
+an object is updated to a specific date, determination of whether
+it is up-to-date or changed is based on the version it was updated to.
+
+::
+
+ >>> repository.updateResource(samp, orig_version_id)
+ >>> util.commit()
+
+ >>> samp.value
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'Sample' object has no attribute 'value'
+
+ >>> repository.getVersionInfo(samp).version_id == orig_version_id
+ True
+ >>> repository.isResourceChanged(samp)
+ False
+ >>> repository.isResourceUpToDate(samp)
+ False
+
+The `isResourceUpToDate()` method indicates whether
+`checkoutResource()` will succeed or raise an exception::
+
+ >>> repository.checkoutResource(samp)
+ Traceback (most recent call last):
+ ...
+ VersionControlError: The selected resource has been updated to a particular version, label or date. The resource must be updated to the mainline or a branch before it may be checked out.
+
+
+TODO: Figure out how to write date-based tests. Perhaps the
+repository should implement a hook used to get the current date so
+tests can hook that.
+
+
+Examining the change history
+----------------------------
+
+::
+
+ >>> actions = {
+ ... interfaces.ACTION_CHECKIN: "Check in",
+ ... interfaces.ACTION_CHECKOUT: "Check out",
+ ... interfaces.ACTION_UNCHECKOUT: "Uncheckout",
+ ... interfaces.ACTION_UPDATE: "Update",
+ ... }
+
+ >>> entries = repository.getLogEntries(samp)
+ >>> for entry in entries:
+ ... print "Action:", actions[entry.action]
+ ... print "Version:", entry.version_id
+ ... print "Path:", entry.path
+ ... if entry.message:
+ ... print "Message:", entry.message
+ ... print "--"
+ Action: Update
+ Version: 1
+ Path: /samp
+ --
+ Action: Update
+ Version: 3
+ Path: /bar
+ --
+ Action: Update
+ Version: 3
+ Path: /foo
+ --
+ Action: Uncheckout
+ Version: 3
+ Path: /samp
+ --
+ Action: Check out
+ Version: 3
+ Path: /samp
+ --
+ Action: Check in
+ Version: 3
+ Path: /samp
+ Message: sample checkin
+ --
+ Action: Check out
+ Version: 2
+ Path: /samp
+ --
+ Action: Update
+ Version: 2
+ Path: /samp
+ --
+ Action: Check in
+ Version: 2
+ Path: /ob
+ --
+ Action: Check out
+ Version: 1
+ Path: /ob
+ --
+ Action: Check in
+ Version: 1
+ Path: /samp
+ Message: Initial checkin.
+ --
+
+Note that the entry with the checkin entry for version 3 includes the
+comment passed to `checkinResource()`.
+
+The version history also contains the principal id related to each
+entry::
+
+ >>> entries[0].user_id
+ 'bob'
+
+
+Branches
+--------
+
+The implementation contains some support for branching, but it's not
+fully exposed in the interface at this time. It's too early to
+document at this time. Branches will interact heavily with
+"stickiness".
+
+
+Supporting separately versioned subobjects
+------------------------------------------
+
+INonVersionedData is responsible for dealing with parts of the object
+state that should *not* be versioned as part of this object. This can
+include both subobjects that are versioned independently as well as
+object-specific data that isn't part of the abstract resource the
+version control framework is supporting.
+
+For the sake of examples, let's create a simple class that actually
+implements these to interfaces. In this example, we'll create a
+simple object that excluses any versionable subobjects and any
+subobjects with names that start with "bob". Note that as for the
+`Sample` class above, we're still careful to consider the values for
+`__name__` and `__parent__` to be non-versioned::
+
+ >>> def ignored_item(name, ob):
+ ... """Return True for non-versioned items."""
+ ... return (interfaces.IVersionable.providedBy(ob)
+ ... or name.startswith("bob")
+ ... or (name in ["__name__", "__parent__"]))
+
+ >>> class SampleContainer(Sample):
+ ...
+ ... # Methods defined by INonVersionedData
+ ... def listNonVersionedObjects(self):
+ ... return [ob for (name, ob) in self.__dict__.items()
+ ... if ignored_item(name, ob)
+ ... ]
+ ...
+ ... def removeNonVersionedData(self):
+ ... for name, value in self.__dict__.items():
+ ... if ignored_item(name, value):
+ ... del self.__dict__[name]
+ ...
+ ... def getNonVersionedData(self):
+ ... return [(name, ob) for (name, ob) in self.__dict__.items()
+ ... if ignored_item(name, ob)
+ ... ]
+ ...
+ ... def restoreNonVersionedData(self, data):
+ ... for name, value in data:
+ ... if name not in self.__dict__:
+ ... self.__dict__[name] = value
+
+Let's take a look at how the INonVersionedData interface is used.
+We'll start by creating an instance of our sample container and
+storing it in the database::
+
+ >>> box = SampleContainer()
+ >>> box.__name__ = "box"
+ >>> root[box.__name__] = box
+
+We'll also add some contained objects::
+
+ >>> box.aList = [1, 2, 3]
+
+ >>> samp1 = Sample()
+ >>> samp1.__name__ = "box/samp1"
+ >>> samp1.__parent__ = box
+ >>> box.samp1 = samp1
+
+ >>> box.bob_list = [3, 2, 1]
+
+ >>> bob_samp = Sample()
+ >>> bob_samp.__name__ = "box/bob_samp"
+ >>> bob_samp.__parent__ = box
+ >>> box.bob_samp = bob_samp
+
+ >>> util.commit()
+
+Let's apply version control to the container::
+
+ >>> repository.applyVersionControl(box)
+
+We'll start by showing some basics of how the INonVersionedData
+interface is used.
+
+The `getNonVersionedData()`, `removeNonVersionedData()`, and
+`restoreNonVersionedData()` methods work together, allowing the
+version control framework to ensure that data that is not versioned as
+part of the object is not lost or inappropriately stored in the
+repository as part of version control operations.
+
+The basic pattern for this trio of operations is simple:
+
+1. Use `getNonVersionedData()` to get a value that can be used to
+ restore the current non-versioned data of the object.
+
+2. Use `removeNonVersionedData()` to remove any non-versioned data
+ from the object so it doesn't enter the repository as object state
+ is copied around.
+
+3. Make object state changes based on the version control operation
+ being performed.
+
+4. Use `restoreNonVersionedData()` to restore the data retrieved using
+ `getNonVersionedData()`.
+
+This is fairly simple to see in an example. Step 1 is to save the
+non-versioned data::
+
+ >>> saved = box.getNonVersionedData()
+
+While the version control framework treats this as an opaque value, we
+can take a closer look to make sure we got what we expected (since we
+know our implementation)::
+
+ >>> names = [name for (name, ob) in saved]
+ >>> names.sort()
+ >>> names
+ ['__name__', 'bob_list', 'bob_samp', 'samp1']
+
+Step 2 is to remove the data from the object::
+
+ >>> box.removeNonVersionedData()
+
+The non-versioned data should no longer be part of the object::
+
+ >>> box.bob_samp
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'SampleContainer' object has no attribute 'bob_samp'
+
+While versioned data should remain present::
+
+ >>> box.aList
+ [1, 2, 3]
+
+At this point, the version control framework will perform any
+appropriate state copies are needed.
+
+Once that's done, `restoreNonVersionedData()` will be called with the
+saved data to perform the restore operation::
+
+ >>> box.restoreNonVersionedData(saved)
+
+We can verify that the restoraion has been performed by checking the
+non-versioned data::
+
+ >>> box.bob_list
+ [3, 2, 1]
+ >>> type(box.samp1)
+ <class 'zope.app.versioncontrol.README.Sample'>
+
+We can see how this is affects object state by making some changes to
+the container object's versioned and non-versioned data and watching
+how those attributes are affected by updating to specific versions
+using `updateResource()` and retrieving specific versions using
+`getVersionOfResource()`. Let's start by generating some new
+revisions in the repository::
+
+ >>> repository.checkoutResource(box)
+ >>> util.commit()
+ >>> version_id = repository.getVersionInfo(box).version_id
+
+ >>> box.aList.append(4)
+ >>> box.bob_list.append(0)
+ >>> repository.checkinResource(box)
+ >>> util.commit()
+
+ >>> box.aList
+ [1, 2, 3, 4]
+ >>> box.bob_list
+ [3, 2, 1, 0]
+
+ >>> repository.updateResource(box, version_id)
+ >>> box.aList
+ [1, 2, 3]
+ >>> box.bob_list
+ [3, 2, 1, 0]
+
+The list remaining method of the INonVersionedData interface is a
+little different, but remains very tightly tied to the details of the
+object's state. The `listNonVersionedObjects()` method should return
+a sequence of all the objects that should not be copied as part of the
+object's state. The difference between this method and
+`getNonVersionedData()` may seem simple, but is significant in
+practice.
+
+The `listNonVersionedObjects()` method allows the version control
+framework to identify data that should not be included in state
+copies, without saying anything else about the data. The
+`getNonVersionedData()` method allows the INonVersionedData
+implementation to communicate with itself (by providing data to be
+restored by the `restoreNonVersionedData()` method) without exposing
+any information about how it communicates with itself (it could store
+all the relevant data into an external file and use the value returned
+to locate the state file again, if that was needed for some reason).
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/__init__.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/__init__.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1 @@
+# This is a Python package
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/configure.zcml 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/configure.zcml 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,7 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zope.app.versioncontrol">
+
+ <permission id="zope.app.versioncontrol.UseVersionControl"
+ title="Use version control" />
+
+</configure>
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/configure.zcml
___________________________________________________________________
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/history.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/history.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/history.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,300 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import sys
+import time
+
+import persistent
+
+from BTrees.IOBTree import IOBTree
+from BTrees.IIBTree import IIBTree
+from BTrees.OOBTree import OOBTree
+
+import zope.app.location
+
+import zope.app.versioncontrol.utility
+import zope.app.versioncontrol.version
+
+from zope.app.versioncontrol.interfaces import VersionControlError
+
+
+class VersionHistory(persistent.Persistent, zope.app.location.Location):
+ """A version history maintains the information about the changes
+ to a particular version-controlled resource over time."""
+
+ def __init__(self, history_id):
+ # The _versions mapping maps version ids to version objects. All
+ # of the actual version data is looked up there. The _labels
+ # mapping maps labels to specific version ids. The _branches map
+ # manages BranchInfo objects that maintain branch information.
+ self._eventLog = EventLog()
+ self._versions = OOBTree()
+ self._branches = OOBTree()
+ self._labels = OOBTree()
+ mainline = self.createBranch('mainline', None)
+ self.__name__ = history_id
+
+ def addLogEntry(self, version_id, action, path=None, message=''):
+ """Add a new log entry associated with this version history."""
+ entry = LogEntry(version_id, action, path, message)
+ self._eventLog.addEntry(entry)
+
+ def getLogEntries(self):
+ """Return a sequence of the log entries for this version history."""
+ return self._eventLog.getEntries()
+
+ def getLabels(self):
+ return self._labels.keys()
+
+ def labelVersion(self, version_id, label, force=0):
+ """Associate a particular version in a version history with the
+ given label, removing any existing association with that label
+ if force is true, or raising an error if force is false and
+ an association with the given label already exists."""
+ current = self._labels.get(label)
+ if current is not None:
+ if current == version_id:
+ return
+ if not force:
+ raise VersionControlError(
+ 'The label %s is already associated with a version.' % (
+ label
+ ))
+ del self._labels[label]
+ self._labels[label] = version_id
+
+ def createBranch(self, branch_id, version_id):
+ """Create a new branch associated with the given branch_id. The
+ new branch is rooted at the version named by version_id."""
+ if self._branches.has_key(branch_id):
+ raise VersionControlError(
+ 'Activity already exists: %s' % branch_id
+ )
+ branch = BranchInfo(branch_id, version_id)
+ branch.__parent__ = self
+ self._branches[branch_id] = branch
+ return branch
+
+ def createVersion(self, object, branch_id):
+ """Create a new version in the line of descent named by the given
+ branch_id, returning the newly created version object."""
+ branch = self._branches.get(branch_id)
+ if branch is None:
+ branch = self.createBranch(branch_id, None)
+ if branch.__name__ != 'mainline':
+ version_id = '%s.%d' % (branch.__name__, len(branch) + 1)
+ else:
+ version_id = '%d' % (len(branch) + 1)
+ version = zope.app.versioncontrol.version.Version(version_id)
+
+ # Update the predecessor, successor and branch relationships.
+ # This is something of a hedge against the future. Versions will
+ # always know enough to reconstruct their lineage without the help
+ # of optimized data structures, which will make it easier to change
+ # internals in the future if we need to.
+ latest = branch.latest()
+ if latest is not None:
+ last = self._versions[latest]
+ last.next = last.next + (version_id,)
+ version.prev = latest
+
+ # If the branch is not the mainline, store the branch name in the
+ # version. Versions have 'mainline' as the default class attribute
+ # which is the common case and saves a minor bit of storage space.
+ if branch.__name__ != 'mainline':
+ version.branch = branch.__name__
+
+ branch.append(version)
+ self._versions[version_id] = version
+ # Call saveState() only after version has been linked into the
+ # database, ensuring it goes into the correct database.
+ version.saveState(object)
+ version.__parent__ = self
+ return version
+
+ def hasVersionId(self, version_id):
+ """Return true if history contains a version with the given id."""
+ return self._versions.has_key(version_id)
+
+ def isLatestVersion(self, version_id, branch_id):
+ """Return true if version id is the latest in its branch."""
+ branch = self._branches[branch_id]
+ return version_id == branch.latest()
+
+ def getLatestVersion(self, branch_id):
+ """Return the latest version object within the given branch, or
+ None if the branch contains no versions.
+ """
+ branch = self._branches[branch_id]
+ version = self._versions[branch.latest()]
+ return version
+
+ def findBranchId(self, version_id):
+ """Given a version id, return the id of the branch of the version.
+ Note that we cheat, since we can find this out from the id.
+ """
+ parts = version_id.split('.')
+ if len(parts) > 1:
+ return parts[-2]
+ return 'mainline'
+
+ def getVersionById(self, version_id):
+ """Return the version object named by the given version id, or
+ raise a VersionControlError if the version is not found.
+ """
+ version = self._versions.get(version_id)
+ if version is None:
+ raise VersionControlError(
+ 'Unknown version id: %s' % version_id
+ )
+ return version
+
+ def getVersionByLabel(self, label):
+ """Return the version associated with the given label, or None
+ if no version matches the given label.
+ """
+ version_id = self._labels.get(label)
+ version = self._versions.get(version_id)
+ if version is None:
+ return None
+ return version
+
+ def getVersionByDate(self, branch_id, timestamp):
+ """Return the last version committed in the given branch on or
+ before the given time value. The timestamp should be a float
+ (time.time format) value in UTC.
+ """
+ branch = self._branches[branch_id]
+ tvalue = int(timestamp / 60.0)
+ while 1:
+ # Try to find a version with a commit date <= the given time
+ # using the timestamp index in the branch information.
+ if branch.m_order:
+ try:
+ match = branch.m_date.maxKey(tvalue)
+ match = branch.m_order[branch.m_date[match]]
+ return self._versions[match]
+ except ValueError:
+ pass
+
+ # If we've run out of lineage without finding a version with
+ # a commit date <= the given timestamp, we return None. It is
+ # up to the caller to decide what to do in this situation.
+ if branch.root is None:
+ return None
+
+ # If the branch has a root (a version in another branch), then
+ # we check the root and do it again with the ancestor branch.
+ rootver = self._versions[branch.root]
+ if int(rootver.date_created / 60.0) < tvalue:
+ return rootver
+ branch = self._branches[rootver.branch]
+
+ def getVersionIds(self, branch_id=None):
+ """Return a sequence of version ids for versions in this history.
+
+ If a branch_id is given, only version ids from that branch
+ will be returned. Note that the sequence of ids returned does
+ not include the id of the branch root.
+ """
+ if branch_id is not None:
+ return self._branches[branch_id].versionIds()
+ return self._versions.keys()
+
+
+class BranchInfo(persistent.Persistent, zope.app.location.Location):
+ """A utility class to hold branch (line-of-descent) information.
+
+ It maintains the name of the branch, the version id of the root of
+ the branch and indices to allow for efficient lookups.
+ """
+
+ def __init__(self, name, root):
+ # m_order maintains a newest-first mapping of int -> version id.
+ # m_date maintains a mapping of a packed date (int # of minutes
+ # since the epoch) to a lookup key in m_order. The two structures
+ # are separate because we only support minute precision for date
+ # lookups (and multiple versions could be added in a minute).
+ self.date_created = time.time()
+ self.m_order = IOBTree()
+ self.m_date = IIBTree()
+ self.__name__ = name
+ self.root = root
+
+ def append(self, version):
+ """Append a version to the branch information.
+
+ Note that this does not store the actual version, but metadata
+ about the version to support ordering and date lookups.
+ """
+ if len(self.m_order):
+ key = self.m_order.minKey() - 1
+ else:
+ key = sys.maxint
+ self.m_order[key] = version.__name__
+ timestamp = int(version.date_created / 60.0)
+ self.m_date[timestamp] = key
+
+ def versionIds(self):
+ """Return a newest-first sequence of version ids in the branch."""
+ return self.m_order.values()
+
+ def latest(self):
+ """Return the version id of the latest version in the branch."""
+ mapping = self.m_order
+ if not len(mapping):
+ return self.root
+ return mapping[mapping.keys()[0]]
+
+ def __len__(self):
+ return len(self.m_order)
+
+
+class EventLog(persistent.Persistent):
+ """An EventLog encapsulates a collection of log entries."""
+
+ def __init__(self):
+ self._data = IOBTree()
+
+ def addEntry(self, entry):
+ """Add a new log entry."""
+ if len(self._data):
+ key = self._data.minKey() - 1
+ else:
+ key = sys.maxint
+ self._data[key] = entry
+
+ def getEntries(self):
+ """Return a sequence of log entries."""
+ return self._data.values()
+
+ def __len__(self):
+ return len(self._data)
+
+ def __nonzero__(self):
+ return bool(self._data)
+
+
+class LogEntry(persistent.Persistent):
+ """A LogEntry contains audit information about a version control
+ operation. Actions that cause audit records to be created include
+ checkout and checkin. Log entry information can be read (but not
+ changed) by restricted code."""
+
+ def __init__(self, version_id, action, path=None, message=''):
+ self.timestamp = time.time()
+ self.version_id = version_id
+ self.action = action
+ self.message = message
+ self.user_id = zope.app.versioncontrol.utility._findUserId()
+ self.path = path
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/history.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/interfaces.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/interfaces.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,317 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import persistent.interfaces
+
+import zope.interface
+import zope.schema
+
+from zope.schema.vocabulary import SimpleVocabulary
+
+import zope.app.annotation.interfaces
+
+from zope.i18nmessageid import MessageIDFactory
+_ = MessageIDFactory('zope.app.versioncontrol')
+
+
+class VersionControlError(Exception):
+ pass
+
+
+class IVersionControl(zope.interface.Interface):
+ """Main API for version control operations.
+
+ This interface hides most of the details of version data storage
+ and retrieval.
+
+ In Zope 3, the version control interface will probably be
+ implemented by a version control utility. In the meantime, it may
+ be implemented directly by repository implementations (or other
+ things, like CMF tools).
+
+ The goal of this version of the version control interface is to
+ support simple linear versioning with support for labelled
+ versions. Future versions or extensions of this interface will
+ likely support more advanced version control features such as
+ concurrent lines of descent (activities) and collection
+ versioning.
+
+ """
+
+ def isResourceUpToDate(object, require_branch=False):
+ """
+ Returns true if a resource is based on the latest version. Note
+ that the latest version is in the context of any activity (branch).
+
+ If the require_branch flag is true, this method returns false if
+ the resource is updated to a particular version, label, or date.
+ Useful for determining whether a call to checkoutResource()
+ will succeed.
+
+ Permission: public
+ """
+
+ def isResourceChanged(object):
+ """
+ Return true if the state of a resource has changed in a transaction
+ *after* the version bookkeeping was saved. Note that this method is
+ not appropriate for detecting changes within a transaction!
+
+ Permission: public
+ """
+
+ def getVersionInfo(object):
+ """
+ Return the VersionInfo associated with the given object.
+
+ The VersionInfo object contains version control bookkeeping
+ information. If the object is not under version control, a
+ VersionControlError will be raised.
+
+ Permission: public
+ """
+
+ def applyVersionControl(object, message=None):
+ """
+ Place the given object under version control. A VersionControlError
+ will be raised if the object is already under version control.
+
+ After being placed under version control, the resource is logically
+ in the 'checked-in' state.
+
+ If no message is passed the 'Initial checkin.' message string is
+ written as the message log entry.
+
+ Permission: Use version control
+ """
+
+ def checkoutResource(object):
+ """
+ Put the given version-controlled object into the 'checked-out'
+ state, allowing changes to be made to the object. If the object is
+ not under version control or the object is already checked out, a
+ VersionControlError will be raised.
+
+ Permission: Use version control
+ """
+
+ def checkinResource(object, message=''):
+ """
+ Check-in (create a new version) of the given object, updating the
+ state and bookkeeping information of the given object. The optional
+ message should describe the changes being committed. If the object
+ is not under version control or is already in the checked-in state,
+ a VersionControlError will be raised.
+
+ Permission: Use version control
+ """
+
+ def uncheckoutResource(object):
+ """
+ Discard changes to the given object made since the last checkout.
+ If the object is not under version control or is not checked out,
+ a VersionControlError will be raised.
+ """
+
+ def updateResource(object, selector=None):
+ """
+ Update the state of the given object to that of a specific version
+ of the object. The object must be in the checked-in state to be
+ updated. The selector must be a string (version id, activity id,
+ label or date) that is used to select a version from the version
+ history.
+
+ Permission: Use version control
+ """
+
+ def labelResource(object, label, force=None):
+ """
+ Associate the given resource with a label. If force is true, then
+ any existing association with the given label will be removed and
+ replaced with the new association. If force is false and there is
+ an existing association with the given label, a VersionControlError
+ will be raised.
+
+ Permission: Use version control
+ """
+
+ def getVersionOfResource(history_id, selector):
+ """
+ Given a version history id and a version selector, return the
+ object as of that version. Note that the returned object has no
+ acquisition context. The selector must be a string (version id,
+ activity id, label or date) that is used to select a version
+ from the version history.
+
+ Permission: Use version control
+ """
+
+ def getVersionIds(object):
+ """
+ Return a sequence of the (string) version ids corresponding to the
+ available versions of an object. This should be used by UI elements
+ to populate version selection widgets, etc.
+
+ Permission: Use version control
+ """
+
+ def getLabelsForResource(object):
+ """
+ Return a sequence of the (string) labels corresponding to the
+ versions of the given object that have been associated with a
+ label. This should be used by UI elements to populate version
+ selection widgets, etc.
+
+ Permission: Use version control
+ """
+
+ def getLogEntries(object):
+ """
+ Return a sequence of LogEntry objects (most recent first) that
+ are associated with a version-controlled object.
+
+ Permission: Use version control
+ """
+
+CHECKED_OUT = 0
+CHECKED_IN = 1
+
+class IVersionInfo(zope.interface.Interface):
+ """Version control bookkeeping information."""
+
+ # TODO: This *should* be a datetime, but we don't yet know how it's used.
+ timestamp = zope.schema.Float(
+ description=_("time value indicating the"
+ " when the bookkeeping information was created"),
+ required=False)
+
+ # TODO: This *should* be an ASCIILine, but there isn't one (yet).
+ history_id = zope.schema.ASCII(
+ description=_("""
+ Id of the version history related to the version controlled resource.
+
+ If this isn't set (is None),
+ """),
+ required=False)
+
+ # TODO: This *should* be an ASCIILine, but there isn't one (yet).
+ version_id = zope.schema.ASCII(
+ description=_(
+ "version id that the version controlled resource is based upon"))
+
+ status = zope.schema.Choice(
+ description=_("status of the version controlled resource"),
+ vocabulary=SimpleVocabulary.fromItems([
+ (_("Checked out"), CHECKED_OUT),
+ (_("Checked in"), CHECKED_IN)]))
+
+ sticky = zope.interface.Attribute(
+ "tag information used internally by the version control implementation"
+ )
+
+ user_id = zope.schema.TextLine(
+ description=_("id of the effective user at the time the bookkeeping"
+ " information was created"))
+
+
+ACTION_CHECKOUT = 0
+ACTION_CHECKIN = 1
+ACTION_UNCHECKOUT = 2
+ACTION_UPDATE = 3
+
+class ILogEntry(zope.interface.Interface):
+ """The ILogEntry interface provides access to the information in an
+ audit log entry."""
+
+ timestamp = zope.schema.Float(
+ description=_("time that the log entry was created"))
+
+ version_id = zope.schema.ASCII(
+ description=_("version id of the resource related to the log entry"))
+
+ action = zope.schema.Choice(
+ description=_("the action that was taken"),
+ vocabulary=SimpleVocabulary.fromItems(
+ [(_("Checkout"), ACTION_CHECKOUT),
+ (_("Checkin"), ACTION_CHECKIN),
+ (_("Uncheckout"), ACTION_UNCHECKOUT),
+ (_("Update"), ACTION_UPDATE)]))
+
+ message = zope.schema.Text(
+ description=_("Message provided by the user at the time of the"
+ " action. This may be empty."))
+
+ user_id = zope.schema.TextLine(
+ description=_("id of the user causing the audited action"))
+
+ path = zope.schema.TextLine(
+ description=_("path to the object upon which the action was taken"))
+
+
+class INonVersionedData(zope.interface.Interface):
+ """Controls what parts of an object fall outside version control.
+
+ Containerish objects implement this interface to allow the items they
+ contain to be versioned independently of the container.
+ """
+
+ def listNonVersionedObjects():
+ """Returns a list of subobjects that should not be pickled.
+
+ The objects in the list must not be wrapped, because only the
+ identity of the objects will be considered. The version
+ repository uses this method to avoid cloning subobjects that
+ will soon be removed by removeNonVersionedData.
+ """
+
+ def removeNonVersionedData():
+ """Removes the non-versioned data from this object.
+
+ The version repository uses this method before storing an
+ object in the version repository.
+ """
+
+ def getNonVersionedData():
+ """Returns an opaque object containing the non-versioned data.
+
+ The version repository uses this method before reverting an
+ object to a revision.
+ """
+
+ def restoreNonVersionedData(data):
+ """Restores non-versioned data to this objecti
+
+ The version repository uses this method after reverting an
+ object to a revision. `data` is a value provided by the
+ `getNonVersionedData()` method of this instance.
+
+ XXX question for Gary:
+ This should not overwrite data that exists in the object but
+ that is included in the passed-in data.
+
+ """
+
+# TODO: figure out if we can get rid of this
+IVersionedContainer = INonVersionedData
+
+
+
+class IVersionable(persistent.interfaces.IPersistent,
+ zope.app.annotation.interfaces.IAnnotatable):
+ """Version control is allowed for objects that provide this."""
+
+class INonVersionable(zope.interface.Interface):
+ """Version control is not allowed for objects that provide this."""
+
+class IVersioned(IVersionable):
+ """Version control is in effect for this object."""
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/interfaces.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/issues.txt
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/issues.txt 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/issues.txt 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,12 @@
+- What is the significance of isResourceChanged()?
+
+- Why might an info object change after creation?
+
+- Can we better apply an application-provided modification time than
+ _p_mtime?
+
+- Why are VersionInfo's cloned instead of simply updated?
+
+- .timestamp is not used, what's the deal?
+
+- should the user be passed in or use the interaction?
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/issues.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/nonversioned.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/nonversioned.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/nonversioned.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,133 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Support for non-versioned data embedded in versioned objects.
+
+$Id$
+"""
+
+import zope.interface
+
+from zope.app.versioncontrol.interfaces import INonVersionedData, IVersionable
+
+def isAVersionableResource(object):
+ return IVersionable.providedBy(object)
+
+
+try:
+ # Optional support for references.
+ from Products.References.Proxy import proxyBase
+ from Products.References.PathReference import PathReference
+except ImportError:
+ isProxyOrReference = None
+else:
+ def isProxyOrReference(obj):
+ if proxyBase(obj) is not obj:
+ return 1
+ if isinstance(obj, PathReference):
+ return 1
+ return 0
+
+
+def listNonVersionedObjects(obj):
+ return INonVersionedData(obj).listNonVersionedObjects()
+
+def getNonVersionedData(obj):
+ return INonVersionedData(obj).getNonVersionedData()
+
+def removeNonVersionedData(obj):
+ INonVersionedData(obj).removeNonVersionedData()
+
+def restoreNonVersionedData(obj, dict):
+ INonVersionedData(obj).restoreNonVersionedData(dict)
+
+
+
+class StandardNonVersionedDataAdapter:
+ """Non-versioned data adapter for arbitrary things.
+ """
+ zope.interface.implements(INonVersionedData)
+
+ attrs = ()
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def listNonVersionedObjects(self):
+ # Assume it's OK to clone all of the attributes.
+ # They will be removed later by removeNonVersionedData.
+ return ()
+
+ def removeNonVersionedData(self):
+ for attr in self.attrs:
+ try:
+ delattr(self.obj, attr)
+ except AttributeError:
+ pass
+
+ def getNonVersionedData(self):
+ data = {}
+ for attr in self.attrs:
+ if hasattr(self.obj, attr):
+ data[attr] = getattr(self.obj), attr
+ return data
+
+ def restoreNonVersionedData(self, data):
+ for attr in self.attrs:
+ if data.has_key(attr):
+ setattr(self.obj, attr, data[attr])
+
+
+class ObjectManagerNonVersionedDataAdapter(StandardNonVersionedDataAdapter):
+ """Non-versioned data adapter for object managers.
+ """
+ zope.interface.implements(INonVersionedData)
+
+ def listNonVersionedObjects(self):
+ contents = self.getNonVersionedData()['contents']
+ return contents.values()
+
+ def removeNonVersionedData(self):
+ StandardNonVersionedDataAdapter.removeNonVersionedData(self)
+ obj = self.obj
+ removed = {}
+ contents = self.getNonVersionedData()['contents']
+ for name, value in contents.items():
+ obj._delOb(name)
+ removed[name] = 1
+ if obj._objects:
+ obj._objects = tuple([info for info in obj._objects
+ if not removed.has_key(info['id'])])
+
+ def getNonVersionedData(self):
+ contents = {}
+ attributes = StandardNonVersionedDataAdapter.getNonVersionedData(self)
+ for name, value in self.obj.objectItems():
+ if not isAVersionableResource(value):
+ # This object should include the state of subobjects
+ # that won't be versioned independently.
+ continue
+ if isProxyOrReference is not None:
+ if isProxyOrReference(value):
+ # This object should include the state of
+ # subobjects that are references.
+ continue
+ contents[name] = value
+ return {'contents': contents, 'attributes': attributes}
+
+ def restoreNonVersionedData(self, data):
+ StandardNonVersionedDataAdapter.restoreNonVersionedData(
+ self, data['attributes'])
+ # Restore the items of the container if not already present:
+ for name, value in data['contents'].items():
+ if name not in obj:
+ obj[name] = value
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/nonversioned.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/repository.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/repository.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/repository.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,477 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+__version__='$Revision: 1.14 $'[11:-2]
+
+import datetime
+import time
+from random import randint
+
+import persistent
+from BTrees.OOBTree import OOBTree
+from BTrees.OIBTree import OIBTree
+
+import zope.interface
+
+from zope.app import datetimeutils
+from zope.app import zapi
+
+from zope.app.annotation.interfaces import IAnnotations
+
+from zope.app.versioncontrol.history import VersionHistory
+from zope.app.versioncontrol.interfaces import VersionControlError
+from zope.app.versioncontrol.interfaces import IVersionable, IVersioned
+from zope.app.versioncontrol.interfaces import IVersionControl
+from zope.app.versioncontrol.interfaces import CHECKED_IN, CHECKED_OUT
+from zope.app.versioncontrol.interfaces import ACTION_CHECKIN, ACTION_CHECKOUT
+from zope.app.versioncontrol.interfaces import ACTION_UNCHECKOUT, ACTION_UPDATE
+from zope.app.versioncontrol import nonversioned
+from zope.app.versioncontrol import utility
+
+
+VERSION_INFO_KEY = "%s.%s" % (utility.__name__, utility.VersionInfo.__name__)
+
+
+class Repository(persistent.Persistent):
+ """The repository implementation manages the actual data of versions
+ and version histories. It does not handle user interface issues."""
+
+ zope.interface.implements(IVersionControl)
+
+ def __init__(self):
+ # These keep track of symbolic label and branch names that
+ # have been used to ensure that they don't collide.
+ self._branches = OIBTree()
+ self._branches['mainline'] = 1
+ self._labels = OIBTree()
+
+ self._histories = OOBTree()
+ self._created = datetime.datetime.utcnow()
+
+ def createVersionHistory(self):
+ """Internal: create a new version history for a resource."""
+ # When one creates the first version in a version history, neither
+ # the version or version history yet have a _p_jar, which causes
+ # copy operations to fail. To work around that, we share our _p_jar.
+ history_id = None
+ while history_id is None or self._histories.has_key(history_id):
+ history_id = str(randint(1, 9999999999))
+ history = VersionHistory(history_id)
+ history.__parent__ = self
+ self._histories[history_id] = history
+ return history
+
+ def getVersionHistory(self, history_id):
+ """Internal: return a version history given a version history id."""
+ return self._histories[history_id]
+
+ def replaceState(self, obj, new_state):
+ """Internal: replace the state of a persistent object.
+ """
+ non_versioned = nonversioned.getNonVersionedData(obj)
+ # XXX There ought to be some way to do this more cleanly.
+ # This fills the __dict__ of the old object with new state.
+ # The other way to achieve the desired effect is to replace
+ # the object in its container, but this method preserves the
+ # identity of the object.
+ if obj.__class__ is not new_state.__class__:
+ raise VersionControlError(
+ "The class of the versioned object has changed. %s != %s"
+ % (repr(obj.__class__, new_state.__class__)))
+ obj._p_changed = 1
+ obj.__dict__.clear()
+ obj.__dict__.update(new_state.__dict__)
+ if non_versioned:
+ # Restore the non-versioned data into the new state.
+ nonversioned.restoreNonVersionedData(obj, non_versioned)
+ return obj
+
+ #####################################################################
+ # This is the implementation of the public version control interface.
+ #####################################################################
+
+ def isResourceUpToDate(self, object, require_branch=False):
+ info = self.getVersionInfo(object)
+ history = self.getVersionHistory(info.history_id)
+ branch = 'mainline'
+ if info.sticky:
+ if info.sticky[0] == 'B':
+ branch = info.sticky[1]
+ elif require_branch:
+ # The object is updated to a particular version
+ # rather than a branch. The caller
+ # requires a branch.
+ return False
+ return history.isLatestVersion(info.version_id, branch)
+
+ def isResourceChanged(self, object):
+ # Return true if the state of a resource has changed in a transaction
+ # *after* the version bookkeeping was saved. Note that this method is
+ # not appropriate for detecting changes within a transaction!
+ info = self.getVersionInfo(object)
+ itime = getattr(info, '_p_mtime', None)
+ if itime is None:
+ return False
+ mtime = utility._findModificationTime(object)
+ if mtime is None:
+ return False
+ return mtime > itime
+
+ def getVersionInfo(self, object):
+ # Return the VersionInfo associated with the given object.
+ #
+ # The VersionInfo object contains version control bookkeeping
+ # information. If the object is not under version control, a
+ # VersionControlError will be raised.
+ #
+ if IVersioned.providedBy(object):
+ annotations = IAnnotations(object)
+ return annotations[VERSION_INFO_KEY]
+ raise VersionControlError(
+ "Object is not under version control.")
+
+ def applyVersionControl(self, object, message=None):
+ if IVersioned.providedBy(object):
+ raise VersionControlError(
+ 'The resource is already under version control.'
+ )
+ if not IVersionable.providedBy(object):
+ raise VersionControlError(
+ 'This resource cannot be put under version control.'
+ )
+
+ # Need to check the parent to see if the container of the object
+ # being put under version control is itself a version-controlled
+ # object. If so, we need to use the branch id of the container.
+ branch = 'mainline'
+ parent = getattr(object, '__parent__', None)
+ if parent is None:
+ p_info = None
+ else:
+ p_info = self.getVersionInfo(parent)
+ if p_info is not None:
+ sticky = p_info.sticky
+ if sticky and sticky[0] == 'B':
+ branch = sticky[1]
+
+ # Create a new version history and initial version object.
+ history = self.createVersionHistory()
+ version = history.createVersion(object, branch)
+
+ history_id = history.__name__
+ version_id = version.__name__
+
+ # Add bookkeeping information to the version controlled object.
+ declare_versioned(object)
+ info = utility.VersionInfo(history_id, version_id, CHECKED_IN)
+ annotations = IAnnotations(object)
+ annotations[VERSION_INFO_KEY] = info
+ if branch != 'mainline':
+ info.sticky = ('B', branch)
+
+ # Save an audit record of the action being performed.
+ history.addLogEntry(version_id,
+ ACTION_CHECKIN,
+ zapi.getPath(object),
+ message is None and 'Initial checkin.' or message
+ )
+
+ def checkoutResource(self, object):
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_IN:
+ raise VersionControlError(
+ 'The selected resource is already checked out.'
+ )
+
+ if info.sticky and info.sticky[0] != 'B':
+ raise VersionControlError(
+ 'The selected resource has been updated to a particular '
+ 'version, label or date. The resource must be updated to '
+ 'the mainline or a branch before it may be checked out.'
+ )
+
+ if not self.isResourceUpToDate(object):
+ raise VersionControlError(
+ 'The selected resource is not up to date!'
+ )
+
+ history = self.getVersionHistory(info.history_id)
+ ob_path = zapi.getPath(object)
+
+ # Save an audit record of the action being performed.
+ history.addLogEntry(info.version_id,
+ ACTION_CHECKOUT,
+ ob_path
+ )
+
+ # Update bookkeeping information.
+ info.status = CHECKED_OUT
+ info.touch()
+
+ def checkinResource(self, object, message=''):
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_OUT:
+ raise VersionControlError(
+ 'The selected resource is not checked out.'
+ )
+
+ if info.sticky and info.sticky[0] != 'B':
+ raise VersionControlError(
+ 'The selected resource has been updated to a particular '
+ 'version, label or date. The resource must be updated to '
+ 'the mainline or a branch before it may be checked in.'
+ )
+
+ if not self.isResourceUpToDate(object):
+ raise VersionControlError(
+ 'The selected resource is not up to date!'
+ )
+
+ history = self.getVersionHistory(info.history_id)
+ ob_path = zapi.getPath(object)
+
+ branch = 'mainline'
+ if info.sticky is not None and info.sticky[0] == 'B':
+ branch = info.sticky[1]
+
+ version = history.createVersion(object, branch)
+
+ # Save an audit record of the action being performed.
+ history.addLogEntry(version.__name__,
+ ACTION_CHECKIN,
+ ob_path,
+ message
+ )
+
+ # Update bookkeeping information.
+ info.version_id = version.__name__
+ info.status = CHECKED_IN
+ info.touch()
+
+ def uncheckoutResource(self, object):
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_OUT:
+ raise VersionControlError(
+ 'The selected resource is not checked out.'
+ )
+
+ history = self.getVersionHistory(info.history_id)
+ ob_path = zapi.getPath(object)
+
+ version = history.getVersionById(info.version_id)
+
+ # Save an audit record of the action being performed.
+ history.addLogEntry(info.version_id,
+ ACTION_UNCHECKOUT,
+ ob_path
+ )
+
+ # Replace the state of the object with a reverted state.
+ self.replaceState(object, version.copyState())
+
+ # Update bookkeeping information.
+ info = utility.VersionInfo(info.history_id, info.version_id,
+ CHECKED_IN)
+ annotations = IAnnotations(object)
+ annotations[VERSION_INFO_KEY] = info
+
+ def updateResource(self, object, selector=None):
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_IN:
+ raise VersionControlError(
+ 'The selected resource must be checked in to be updated.'
+ )
+
+ history = self.getVersionHistory(info.history_id)
+ version = None
+ sticky = info.sticky
+
+ if not selector:
+ # If selector is null, update to the latest version taking any
+ # sticky attrs into account (branch, date). Note that the sticky
+ # tag could also be a date or version id. We don't bother checking
+ # for those, since in both cases we do nothing (because we'll
+ # always be up to date until the sticky tag changes).
+ if sticky and sticky[0] == 'L':
+ # A label sticky tag, so update to that label (since it is
+ # possible, but unlikely, that the label has been moved).
+ version = history.getVersionByLabel(sticky[1])
+ elif sticky and sticky[0] == 'B':
+ # A branch sticky tag. Update to latest version on branch.
+ version = history.getLatestVersion(selector)
+ else:
+ # Update to mainline, forgetting any date or version id
+ # sticky tag that was previously associated with the object.
+ version = history.getLatestVersion('mainline')
+ sticky = None
+ else:
+ # If the selector is non-null, we find the version specified
+ # and update the sticky tag. Later we'll check the version we
+ # found and decide whether we really need to update the object.
+ if history.hasVersionId(selector):
+ version = history.getVersionById(selector)
+ sticky = ('V', selector)
+
+ elif self._labels.has_key(selector):
+ version = history.getVersionByLabel(selector)
+ sticky = ('L', selector)
+
+ elif self._branches.has_key(selector):
+ version = history.getLatestVersion(selector)
+ if selector == 'mainline':
+ sticky = None
+ else:
+ sticky = ('B', selector)
+ else:
+ try: date = DateTime(selector)
+ except:
+ raise VersionControlError(
+ 'Invalid version selector: %s' % selector
+ )
+ else:
+ timestamp = date.timeTime()
+ sticky = ('D', timestamp)
+ # Fix!
+ branch = history.findBranchId(info.version_id)
+ version = history.getVersionByDate(branch, timestamp)
+
+ # If the state of the resource really needs to be changed, do the
+ # update and make a log entry for the update.
+ version_id = version and version.__name__ or info.version_id
+ if version and (version_id != info.version_id):
+ self.replaceState(object, version.copyState())
+ declare_versioned(object)
+
+ history.addLogEntry(version_id,
+ ACTION_UPDATE,
+ zapi.getPath(object)
+ )
+
+ # Update bookkeeping information.
+ info = utility.VersionInfo(info.history_id, version_id, CHECKED_IN)
+ if sticky is not None:
+ info.sticky = sticky
+ annotations = IAnnotations(object)
+ annotations[VERSION_INFO_KEY] = info
+
+ def labelResource(self, object, label, force=0):
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_IN:
+ raise VersionControlError(
+ 'The selected resource must be checked in to be labeled.'
+ )
+
+ # Make sure that labels and branch ids do not collide.
+ if self._branches.has_key(label) or label == 'mainline':
+ raise VersionControlError(
+ 'The label value given is already in use as an activity id.'
+ )
+ if not self._labels.has_key(label):
+ self._labels[label] = 1
+
+ history = self.getVersionHistory(info.history_id)
+ history.labelVersion(info.version_id, label, force)
+
+ def makeActivity(self, object, branch_id):
+ # Note - this is not part of the official version control API yet.
+ # It is here to allow unit testing of the architectural aspects
+ # that are already in place to support activities in the future.
+
+ info = self.getVersionInfo(object)
+ if info.status != CHECKED_IN:
+ raise VersionControlError(
+ 'The selected resource must be checked in.'
+ )
+
+ branch_id = branch_id or None
+
+ # Make sure that activity ids and labels do not collide.
+ if self._labels.has_key(branch_id) or branch_id == 'mainline':
+ raise VersionControlError(
+ 'The value given is already in use as a version label.'
+ )
+
+ if not self._branches.has_key(branch_id):
+ self._branches[branch_id] = 1
+
+ history = self.getVersionHistory(info.history_id)
+
+ if history._branches.has_key(branch_id):
+ raise VersionControlError(
+ 'The resource is already associated with the given activity.'
+ )
+
+ history.createBranch(branch_id, info.version_id)
+ return object
+
+ def getVersionOfResource(self, history_id, selector):
+ history = self.getVersionHistory(history_id)
+ sticky = None
+
+ if not selector or selector == 'mainline':
+ version = history.getLatestVersion('mainline')
+ else:
+ if history.hasVersionId(selector):
+ version = history.getVersionById(selector)
+ sticky = ('V', selector)
+
+ elif self._labels.has_key(selector):
+ version = history.getVersionByLabel(selector)
+ sticky = ('L', selector)
+
+ elif self._branches.has_key(selector):
+ version = history.getLatestVersion(selector)
+ sticky = ('B', selector)
+ else:
+ try:
+ timestamp = datetimeutils.time(selector)
+ except datetimeutils.DateTimeError:
+ raise VersionControlError(
+ 'Invalid version selector: %s' % selector
+ )
+ else:
+ sticky = ('D', timestamp)
+ version = history.getVersionByDate('mainline', timestamp)
+
+ object = version.copyState()
+ declare_versioned(object)
+
+ info = utility.VersionInfo(history_id, version.__name__, CHECKED_IN)
+ if sticky is not None:
+ info.sticky = sticky
+ annotations = IAnnotations(object)
+ annotations[VERSION_INFO_KEY] = info
+
+ return object
+
+ def getVersionIds(self, object):
+ info = self.getVersionInfo(object)
+ history = self.getVersionHistory(info.history_id)
+ return history.getVersionIds()
+
+ def getLabelsForResource(self, object):
+ info = self.getVersionInfo(object)
+ history = self.getVersionHistory(info.history_id)
+ return history.getLabels()
+
+ def getLogEntries(self, object):
+ info = self.getVersionInfo(object)
+ history = self.getVersionHistory(info.history_id)
+ return history.getLogEntries()
+
+
+def declare_versioned(object):
+ """Apply bookkeeping needed to recognize an object version controlled."""
+ ifaces = zope.interface.directlyProvidedBy(object)
+ ifaces += IVersioned
+ zope.interface.directlyProvides(object, *ifaces)
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/repository.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/tests.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/tests.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,60 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Version control tests
+
+$Id$
+"""
+import sys
+import unittest
+
+from zope.component.tests.placelesssetup import PlacelessSetup
+from zope.testing import doctest
+from transaction import abort
+
+class FakeModule:
+ def __init__(self, dict):
+ self.__dict = dict
+ def __getattr__(self, name):
+ try:
+ return self.__dict[name]
+ except KeyError:
+ raise AttributeError, name
+
+name = 'zope.app.versioncontrol.README'
+
+ps = PlacelessSetup()
+
+def setUp(test):
+ ps.setUp()
+ dict = test.globs
+ dict.clear()
+ dict['__name__'] = name
+ sys.modules[name] = FakeModule(dict)
+
+def tearDown(test):
+ del sys.modules[name]
+ abort()
+ db = test.globs.get('db')
+ if db is not None:
+ db.close()
+ ps.tearDown()
+
+def test_suite():
+ return doctest.DocFileSuite('README.txt',
+ setUp=setUp, tearDown=tearDown,
+ )
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/utility.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/utility.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/utility.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,104 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import time
+
+import persistent
+
+from ZODB.serialize import referencesf
+from ZODB.TimeStamp import TimeStamp
+
+import zope.security.management
+
+import zope.app.versioncontrol.interfaces
+
+
+def isAVersionableResource(obj):
+ """ True if an object is versionable.
+
+ To qualify, the object must be persistent (have its own db record), and
+ must not have an true attribute named '__non_versionable__'.
+ """
+ return zope.app.versioncontrol.interfaces.IVersionable.providedBy(obj)
+
+
+class VersionInfo(persistent.Persistent):
+ """Container for bookkeeping information.
+
+ The bookkeeping information can be read (but not changed) by
+ restricted code.
+ """
+
+ def __init__(self, history_id, version_id, status):
+ self.history_id = history_id
+ self.version_id = version_id
+ self.status = status
+ self.touch()
+
+ sticky = None
+
+ def branchName(self):
+ if self.sticky is not None and self.sticky[0] == 'B':
+ return self.sticky[1]
+ return 'mainline'
+
+ def touch(self):
+ self.user_id = _findUserId()
+ self.timestamp = time.time()
+
+
+def _findUserId():
+ interaction = zope.security.management.getInteraction()
+ return interaction.participations[0].principal.id
+
+def _findModificationTime(object):
+ """Find the last modification time for a version-controlled object.
+ The modification time reflects the latest modification time of
+ the object or any of its persistent subobjects that are not
+ themselves version-controlled objects. Note that this will
+ return None if the object has no modification time."""
+
+ mtime = getattr(object, '_p_mtime', None)
+ if mtime is None:
+ return None
+
+ latest = mtime
+ conn = object._p_jar
+ load = conn._storage.load
+ version = conn._version
+ refs = referencesf
+
+ oids=[object._p_oid]
+ done_oids={}
+ done=done_oids.has_key
+ first = 1
+
+ while oids:
+ oid=oids[0]
+ del oids[0]
+ if done(oid):
+ continue
+ done_oids[oid]=1
+ try: p, serial = load(oid, version)
+ except: pass # invalid reference!
+ else:
+ if first is not None:
+ first = None
+ else:
+ if p.find('U\x0b__vc_info__') == -1:
+ mtime = TimeStamp(serial).timeTime()
+ if mtime > latest:
+ latest = mtime
+ refs(p, oids)
+
+ return latest
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/utility.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope3/trunk/src/zope/app/versioncontrol/version.py
===================================================================
--- Zope3/trunk/src/zope/app/versioncontrol/version.py 2004-09-11 16:58:09 UTC (rev 27497)
+++ Zope3/trunk/src/zope/app/versioncontrol/version.py 2004-09-11 17:08:11 UTC (rev 27498)
@@ -0,0 +1,101 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+
+
+import tempfile
+import time
+from cStringIO import StringIO
+from cPickle import Pickler, Unpickler
+
+import persistent
+
+from BTrees.OOBTree import OOBTree
+
+import zope.app.location
+
+from zope.app.versioncontrol.interfaces import VersionControlError
+from zope.app.versioncontrol.interfaces import INonVersionedData
+
+
+def cloneByPickle(obj, ignore_list=()):
+ """Makes a copy of a ZODB object, loading ghosts as needed.
+
+ Ignores specified objects along the way, replacing them with None
+ in the copy.
+ """
+ ignore_dict = {}
+ for o in ignore_list:
+ ignore_dict[id(o)] = o
+ ids = {"ignored": object()}
+
+ def persistent_id(ob):
+ if ignore_dict.has_key(id(ob)):
+ return 'ignored'
+ if (zope.app.location.ILocation.providedBy(object)
+ and not zope.app.location.inside(ob, obj)):
+ myid = id(ob)
+ ids[myid] = ob
+ return myid
+ if getattr(ob, '_p_changed', 0) is None:
+ ob._p_changed = 0
+ return None
+
+ stream = StringIO()
+ p = Pickler(stream, 1)
+ p.persistent_id = persistent_id
+ p.dump(obj)
+ stream.seek(0)
+ u = Unpickler(stream)
+ u.persistent_load = ids.get
+ return u.load()
+
+
+class Version(persistent.Persistent, zope.app.location.Location):
+ """A Version is a resource that contains a copy of a particular state
+ (content and dead properties) of a version-controlled resource. A
+ version is created by checking in a checked-out resource. The state
+ of a version of a version-controlled resource never changes."""
+
+ def __init__(self, version_id):
+ self.__name__ = version_id
+ self.date_created = time.time()
+ self._data = None
+
+ # These attributes are set by the createVersion method of the version
+ # history at the time the version is created. The branch is the name
+ # of the branch on which the version was created. The prev attribute
+ # is the version id of the predecessor to this version. The next attr
+ # is a sequence of version ids of the successors to this version.
+ branch = 'mainline'
+ prev = None
+ next = ()
+
+ def saveState(self, obj):
+ """Save the state of `obj` as the state for this version of
+ a version-controlled resource."""
+ self._data = self.stateCopy(obj)
+
+ def copyState(self):
+ """Return an independent deep copy of the state of the version."""
+ return self.stateCopy(self._data)
+
+ def stateCopy(self, obj):
+ """Get a deep copy of the state of an object.
+
+ Breaks any database identity references.
+ """
+ ignore = INonVersionedData(obj).listNonVersionedObjects()
+ res = cloneByPickle(obj, ignore)
+ INonVersionedData(res).removeNonVersionedData()
+ return res
Property changes on: Zope3/trunk/src/zope/app/versioncontrol/version.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Zope3-Checkins
mailing list