[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