[Checkins] SVN: zc.vault/trunk/src/zc/vault/ Initial checkin. A subversion-like repository stored in the ZODB, complex but powerful and useful for many low-level tasks.

Gary Poster gary at zope.com
Tue Aug 15 16:58:29 EDT 2006


Log message for revision 69538:
  Initial checkin.  A subversion-like repository stored in the ZODB, complex but powerful and useful for many low-level tasks.
  

Changed:
  A   zc.vault/trunk/src/zc/vault/README.txt
  A   zc.vault/trunk/src/zc/vault/__init__.py
  A   zc.vault/trunk/src/zc/vault/catalog.py
  A   zc.vault/trunk/src/zc/vault/catalog.txt
  A   zc.vault/trunk/src/zc/vault/catalog.zcml
  A   zc.vault/trunk/src/zc/vault/configure.zcml
  A   zc.vault/trunk/src/zc/vault/core.py
  A   zc.vault/trunk/src/zc/vault/i18n.py
  A   zc.vault/trunk/src/zc/vault/interfaces.py
  A   zc.vault/trunk/src/zc/vault/keyref.py
  A   zc.vault/trunk/src/zc/vault/objectlog.py
  A   zc.vault/trunk/src/zc/vault/objectlog.txt
  A   zc.vault/trunk/src/zc/vault/objectlog.zcml
  A   zc.vault/trunk/src/zc/vault/tests.py
  A   zc.vault/trunk/src/zc/vault/traversal.py
  A   zc.vault/trunk/src/zc/vault/traversal.txt
  A   zc.vault/trunk/src/zc/vault/traversal.zcml
  A   zc.vault/trunk/src/zc/vault/vault.py
  A   zc.vault/trunk/src/zc/vault/versions.py
  A   zc.vault/trunk/src/zc/vault/versions.txt
  A   zc.vault/trunk/src/zc/vault/versions.zcml

-=-
Added: zc.vault/trunk/src/zc/vault/README.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/README.txt	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/README.txt	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,2489 @@
+=====
+Vault
+=====
+
+Vaults model versioned containers.  A single revision of a vault is
+typically viewed and (if not yet frozen) manipulated as an "inventory".
+Inventories actually manipulate lower-level objects called manifests
+that are only touched on in this document.  Inventories are the primary
+API.
+
+Inventories *model* containers, but are not traditional mappings:
+containment is external to the actual objects in the inventory.  You
+must query the inventory to discover the hierarchy, rather than the
+objects themselves.  For instance, if you put an object in an inventory
+and want to treat it as a versioned folder, you don't put children in
+the object, but in the inventory node that wraps the object.  This will
+be demonstrated repeatedly and in-depth below.
+
+Vaults only contain versioned, frozen manifests, accessed as
+inventories.  Working inventories can be made from any inventory in a
+vault.  They can then be modified, and committed themselves in the
+vault. Committing an inventory freezes it and all objects it
+"contains".
+
+Let's look at an example.  Vaults store manifests, so when you first
+create one it is empty.  Vaults have a basic sequence API, so a `len`
+will return `0`.
+
+    >>> from zc.vault.vault import Vault, Inventory
+    >>> from zc.vault.core import Manifest
+    >>> from zc.vault import interfaces
+    >>> from zope.interface.verify import verifyObject
+    >>> v = Vault()
+    >>> len(v)
+    0
+    >>> verifyObject(interfaces.IVault, v)
+    True
+
+The last inventory--the -1 index--is the current one.  A shorthand to this
+inventory is the inventory attribute.
+
+    >>> v.inventory # None
+
+Vaults and inventories must have a database connection in order to store their
+data.  We'll assume we have a ZODB folder named "app" in which we can store
+our information.  This is set up in tests.py when this file is run as a test.
+
+    >>> app['vault'] = v
+
+Creating an initial working inventory requires us to merely instantiate it.
+Usually we pass a versioned inventory on which to base the new inventory, but
+without that we at least pass the vault.
+
+    >>> i = Inventory(vault=v)
+    >>> verifyObject(interfaces.IInventory, i)
+    True
+
+Technically, what we have done is create a manifest--the core API for managing
+the contents--and wrapped an inventory API around it.
+
+    >>> verifyObject(interfaces.IManifest, i.manifest)
+    True
+
+We could have created the manifest explicitly instead.
+
+    >>> manifest = Manifest(vault=v)
+    >>> verifyObject(interfaces.IManifest, manifest)
+    True
+    >>> i = Inventory(manifest)
+    >>> verifyObject(interfaces.IInventory, i)
+    True
+
+Inventories--or at least the manifests on which they rely--must be
+stored somewhere in the database before being committed. They provide
+zope.app.location.interfaces.ILocation so that they can be stored in
+standard Zope containers as they are being developed.
+
+    >>> app['inventory'] = i
+
+Inventories have contents that can seem to directly contain objects.  They have
+a mapping API, and follow the IInventoryContents interface.
+
+    >>> verifyObject(interfaces.IInventoryContents, i.contents)
+    True
+    >>> len(i.contents.keys())
+    0
+    >>> len(i.contents.values())
+    0
+    >>> len(i.contents.items())
+    0
+    >>> list(i.contents)
+    []
+    >>> i.contents.get('mydemo') # None
+    >>> 'mydemo' in i
+    False
+    >>> i.contents['mydemo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'mydemo'
+    >>> del i.contents['mydemo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'mydemo'
+
+(ADVANCED SIDE NOTE: feel free to ignore)
+
+The contents object is an API convenience to wrap a relationship.
+Relationships connect a token to various pieces of information.  The
+token for all inventory contents (the top node) is stored on the vault
+as the top_token attribute, and lower levels get unique tokens that
+represent a given location in a vault across inventories.
+
+Contents and items (seen below) essentially get all their data from the
+relationships and the associated manifest that holds them.
+
+    >>> verifyObject(interfaces.IRelationship, i.contents.relationship)
+    True
+    >>> i.contents.relationship.token == i.vault.top_token
+    True
+    >>> verifyObject(interfaces.IRelationshipContainment,
+    ...              i.contents.relationship.containment)
+    True
+    >>> i.contents.relationship.object # None, because contents.
+
+(end ADVANCED SIDE NOTE)
+
+Because it is often convenient to use tokens as a globally unique identifier
+of a particular object, all inventory items have a "token" attribute.
+
+    >>> i.contents.token
+    1234567
+
+Unlike typical Zope 3 containment as defined in zope.app.container, this
+containment does not affect the __parent__ or __name__ of the object.
+
+All objects stored in an inventory must be None, or be adaptable to
+zope.app.keyreference.interfaces.IKeyReference.  In standard Zope 3,
+this includes any instance of a class that extends
+persistent.Persistent.
+
+All non-None objects must also be adaptable to
+zc.copyversion.interfaces.IVersionable.
+
+Here, we create an object, add it to the application, and try to add it to
+an inventory.
+
+    >>> import persistent
+    >>> from zope.app.container.contained import Contained
+    >>> class Demo(persistent.Persistent, Contained):
+    ...     def __repr__(self):
+    ...         return "<%s %r>" % (self.__class__.__name__, self.__name__)
+    ...
+    >>> app['d1'] = Demo()
+    >>> i.contents['mydemo'] = app['d1']
+    Traceback (most recent call last):
+    ...
+    ValueError: can only place versionable objects in vault, or None
+
+This error occurs because committing an inventory must version itself
+and version all of its contained objects, so that looking at an
+historical inventory displays the objects as they were at the time of
+commit.  Here's a simple demo adapter for the Demo objects.  We also
+declare that Demo is IVersionable, an important marker.
+
+    >>> import pytz
+    >>> import datetime
+    >>> from zope import interface, component, event
+    >>> from zc.copyversion.interfaces import (
+    ...     IVersioning, ObjectVersionedEvent, IVersionable)
+    >>> from zc.copyversion.versioning import method
+    >>> class DemoVersioningAdapter(object):
+    ...     interface.implements(IVersioning)
+    ...     component.adapts(Demo)
+    ...     def __init__(self, context):
+    ...         self.context = context
+    ...     @property
+    ...     def _z_versioned(self):
+    ...         return (getattr(self.context, '_z__version_timestamp', None)
+    ...                 is not None)
+    ...     @property
+    ...     def _z_version_timestamp(self):
+    ...         return getattr(self.context, '_z__version_timestamp', None)
+    ...     @method
+    ...     def _z_version(self):
+    ...         self.context._z__version_timestamp = datetime.datetime.now(
+    ...             pytz.utc)
+    ...         event.notify(ObjectVersionedEvent(self))
+    ...
+    >>> component.provideAdapter(DemoVersioningAdapter)
+    >>> interface.classImplements(Demo, IVersionable)
+
+As an aside, it's worth noting that the manifest objects provide
+IVersioning natively, so they can already be queried for the versioning
+status and timestamp without adaptation.  When a manifest is versioned,
+all "contained" objects should be versioned as well.
+
+It's not versioned now--and neither is our demo instance.
+
+    >>> manifest._z_versioned
+    False
+    >>> IVersioning(app['d1'])._z_versioned
+    False
+
+Now that Demo instances are versionable we can add the object to the inventory.
+That means adding and removing objects.  Here we add one.
+
+    >>> i.contents['mydemo'] = app['d1']
+    >>> i.contents['mydemo']
+    <Demo u'd1'>
+    >>> i.__parent__ is app
+    True
+    >>> i.contents.__parent__ is i
+    True
+    >>> i.contents.get('mydemo')
+    <Demo u'd1'>
+    >>> list(i.contents.keys())
+    ['mydemo']
+    >>> i.contents.values()
+    [<Demo u'd1'>]
+    >>> i.contents.items()
+    [('mydemo', <Demo u'd1'>)]
+    >>> list(i.contents)
+    ['mydemo']
+    >>> 'mydemo' in i.contents
+    True
+
+Now our effective hierarchy simply looks like this::
+
+                     (top node)
+                         |
+                      'mydemo'
+                   (<Demo u'd1'>)
+
+We will update this hierarchy as we proceed.
+
+Adding an object fires a (special to the package!) IObjectAdded event.
+This event is not from the standard lifecycleevents package because
+that one has a different connotation--for instance, as noted before,
+putting an object in an inventory does not set the __parent__ or
+__name__ (unless it does not already have a location, in which case it
+is put in a possibly temporary "held" container, discussed below).
+
+    >>> interfaces.IObjectAdded.providedBy(events[-1])
+    True
+    >>> isinstance(events[-1].object, int)
+    True
+    >>> i.manifest.get(events[-1].object).object is app['d1']
+    True
+    >>> events[-1].mapping is i.contents.relationship.containment
+    True
+    >>> events[-1].key
+    'mydemo'
+
+Now we remove the object.
+
+    >>> del i.contents['mydemo']
+    >>> len(i.contents.keys())
+    0
+    >>> len(i.contents.values())
+    0
+    >>> len(i.contents.items())
+    0
+    >>> list(i.contents)
+    []
+    >>> i.contents.get('mydemo') # None
+    >>> 'mydemo' in i.contents
+    False
+    >>> i.contents['mydemo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'mydemo'
+    >>> del i.contents['mydemo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'mydemo'
+
+Removing an object fires a special IObjectRemoved event (again, not from
+lifecycleevents).
+
+    >>> interfaces.IObjectRemoved.providedBy(events[-1])
+    True
+    >>> isinstance(events[-1].object, int)
+    True
+    >>> i.manifest.get(events[-1].object).object is app['d1']
+    True
+    >>> events[-1].mapping is i.contents.relationship.containment
+    True
+    >>> events[-1].key
+    'mydemo'
+
+In addition to a mapping API, the inventory contents support an ordered
+container API very similar to the ordered container in
+zope.app.container.ordered.  The ordered nature of the contents mean that
+iterating is on the basis of the order in which objects were added, by default
+(earliest first); and that the inventory supports an "updateOrder" method.
+The method takes an iterable of names in the container: the new order will be
+the given order.  If the set of given names differs at all with the current
+set of keys, the method will raise ValueError.
+
+    >>> i.contents.updateOrder(())
+    >>> i.contents.updateOrder(('foo',))
+    Traceback (most recent call last):
+    ...
+    ValueError: Incompatible key set.
+    >>> i.contents['donald'] = app['d1']
+    >>> app['b1'] = Demo()
+    >>> i.contents['barbara'] = app['b1']
+    >>> app['c1'] = Demo()
+    >>> app['a1'] = Demo()
+    >>> i.contents['cathy'] = app['c1']
+    >>> i.contents['abe'] = app['a1']
+    >>> list(i.contents.keys())
+    ['donald', 'barbara', 'cathy', 'abe']
+    >>> i.contents.values()
+    [<Demo u'd1'>, <Demo u'b1'>, <Demo u'c1'>, <Demo u'a1'>]
+    >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+    [('donald', <Demo u'd1'>), ('barbara', <Demo u'b1'>),
+     ('cathy', <Demo u'c1'>), ('abe', <Demo u'a1'>)]
+    >>> list(i.contents)
+    ['donald', 'barbara', 'cathy', 'abe']
+    >>> 'cathy' in i.contents
+    True
+    >>> i.contents.updateOrder(())
+    Traceback (most recent call last):
+    ...
+    ValueError: Incompatible key set.
+    >>> i.contents.updateOrder(('foo',))
+    Traceback (most recent call last):
+    ...
+    ValueError: Incompatible key set.
+    >>> i.contents.updateOrder(iter(('abe', 'barbara', 'cathy', 'donald')))
+    >>> list(i.contents.keys())
+    ['abe', 'barbara', 'cathy', 'donald']
+    >>> i.contents.values()
+    [<Demo u'a1'>, <Demo u'b1'>, <Demo u'c1'>, <Demo u'd1'>]
+    >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+    [('abe', <Demo u'a1'>), ('barbara', <Demo u'b1'>),
+     ('cathy', <Demo u'c1'>), ('donald', <Demo u'd1'>)]
+    >>> list(i.contents)
+    ['abe', 'barbara', 'cathy', 'donald']
+    >>> i.contents.updateOrder(('abe', 'cathy', 'donald', 'barbara', 'edward'))
+    Traceback (most recent call last):
+    ...
+    ValueError: Incompatible key set.
+    >>> list(i.contents)
+    ['abe', 'barbara', 'cathy', 'donald']
+    >>> del i.contents['cathy']
+    >>> list(i.contents.keys())
+    ['abe', 'barbara', 'donald']
+    >>> i.contents.values()
+    [<Demo u'a1'>, <Demo u'b1'>, <Demo u'd1'>]
+    >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+    [('abe', <Demo u'a1'>), ('barbara', <Demo u'b1'>), ('donald', <Demo u'd1'>)]
+    >>> list(i.contents)
+    ['abe', 'barbara', 'donald']
+    >>> i.contents.updateOrder(('barbara', 'abe', 'donald'))
+    >>> list(i.contents.keys())
+    ['barbara', 'abe', 'donald']
+    >>> i.contents.values()
+    [<Demo u'b1'>, <Demo u'a1'>, <Demo u'd1'>]
+
+Now our _`hierarchy` looks like this::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+
+Reordering a container fires an event.
+
+    >>> interfaces.IOrderChanged.providedBy(events[-1])
+    True
+    >>> events[-1].object is i.contents.relationship.containment
+    True
+    >>> events[-1].old_keys
+    ('abe', 'barbara', 'donald')
+
+In some circumstances it's easier set the new order from a set of tokens.  In
+that case the "updateOrderFromTokens" method is useful.
+
+    >>> def getToken(key):
+    ...     return i.contents(k).token
+
+    >>> new_order = [getToken(k) for k in ('abe', 'donald', 'barbara')]
+    >>> i.contents.updateOrderFromTokens(new_order)
+    >>> list(i.contents.keys())
+    ['abe', 'donald', 'barbara']
+
+Just like "updateOrder", an event is fired.
+
+    >>> interfaces.IOrderChanged.providedBy(events[-1])
+    True
+    >>> events[-1].object is i.contents.relationship.containment
+    True
+    >>> events[-1].old_keys
+    ('barbara', 'abe', 'donald')
+
+It's just as easy to put them back so that the `hierarchy`_ still looks the
+same as it did at the end of the previous example.
+
+    >>> new_order = [getToken(k) for k in ('barbara', 'abe', 'donald')]
+    >>> i.contents.updateOrderFromTokens(new_order)
+    >>> list(i.contents.keys())
+    ['barbara', 'abe', 'donald']
+
+As noted in the introduction to this document, the versioned hierarchy
+is kept external from the objects themselves.  This means that objects
+that are not containers themselves can still be branch
+nodes--containers, of a sort--within an inventory.  In fact, until a
+reasonable use case emerges for the pattern, the author discourages the
+use of true containers within a vault as branch nodes: two dimensions
+of "containerish" behavior is too confusing.
+
+In order to get an object that can act as a container for one of the objects
+in the inventory, one calls the inventory contents: "i.contents('abe')".  This
+returns an IInventoryItem, if the key exists.  It raises a KeyError for a
+missing key by default, but can take a default.
+
+    >>> i.contents['abe']
+    <Demo u'a1'>
+    >>> item = i.contents('abe')
+    >>> verifyObject(interfaces.IInventoryItem, item)
+    True
+    >>> i.contents('foo')
+    Traceback (most recent call last):
+    ...
+    KeyError: 'foo'
+    >>> i.contents('foo', None) # None
+
+IInventoryItems extend IInventoryContents to add an 'object' attribute, which
+is the object they represent. Like IInventoryContents, a mapping interface
+allows one to manipulate the hierarchy beneath the top level. For instance,
+here we effectively put the 'cathy' demo object in the container space of the
+'abe' demo object.
+
+    >>> item.object
+    <Demo u'a1'>
+    >>> item.name
+    'abe'
+    >>> item.parent.relationship is i.contents.relationship
+    True
+    >>> item.__parent__ is item.inventory
+    True
+    >>> list(item.values())
+    []
+    >>> list(item.keys())
+    []
+    >>> list(item.items())
+    []
+    >>> list(item)
+    []
+    >>> item.get('foo') # None
+    >>> item['foo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'foo'
+    >>> item('foo')
+    Traceback (most recent call last):
+    ...
+    KeyError: 'foo'
+    >>> item['catherine'] = app['c1']
+    >>> item['catherine']
+    <Demo u'c1'>
+    >>> item.get('catherine')
+    <Demo u'c1'>
+    >>> list(item.keys())
+    ['catherine']
+    >>> list(item.values())
+    [<Demo u'c1'>]
+    >>> list(item.items())
+    [('catherine', <Demo u'c1'>)]
+    >>> catherine = item('catherine')
+    >>> catherine.object
+    <Demo u'c1'>
+    >>> catherine.name
+    'catherine'
+    >>> catherine.parent.name
+    'abe'
+    >>> catherine.parent.object
+    <Demo u'a1'>
+    >>> list(catherine.keys())
+    []
+
+Now our hierarchy looks like this::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+
+It's worthwhile noting that the same object can be in multiple places in an
+inventory.  This does not duplicate the hierarchy, or keep changes in sync.
+If desired, this policy should be performed in code that uses the vault;
+similarly if a vault should only contain an object in one location at a time,
+this should be enforced in code that uses a vault.
+
+    >>> i.contents('abe')('catherine')['anna'] = app['a1']
+    >>> i.contents('abe')('catherine').items()
+    [('anna', <Demo u'a1'>)]
+    >>> i.contents('abe')('catherine')('anna').parent.parent.object
+    <Demo u'a1'>
+
+Now our hierarchy looks like this::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+Even though a1 contains c1 contains a1, this does not constitute a cycle: the
+hierarchy is separate from the objects.
+
+InventoryItems and InventoryContents are currently created on the fly, and
+not persisted.  They should be compared with "==", not "is".  They represent
+a persistent core data object that provides zc.vault.interfaces.IRelationship.
+The IRelationship itself is hidden from the majority of this discussion and
+only introduced at the end of the document.  But in any case...
+
+    >>> i.contents('abe') is i.contents('abe')
+    False
+    >>> i.contents('abe') == i.contents('abe')
+    True
+    >>> i.contents is i.contents
+    False
+    >>> i.contents == i.contents
+    True
+    >>> i.contents == None
+    False
+    >>> i.contents('abe') == None
+    False
+
+Comparing inventories will also compare their contents:
+
+    >>> i == None
+    False
+    >>> i == i
+    True
+    >>> i != i
+    False
+
+Another important characteristic of inventory items is that they continue to
+have the right information even as objects around them are changed--for
+instance, if an object's parent is changed from one part of the hierarchy to
+another (see `moveTo`, below), an item generated before the move will still
+reflect the change correctly.
+
+It's worth noting that, thanks to the wonder of the zc.shortcut code, views can
+exist for the object and also, from a proxy, have access to the InventoryItem's
+information: this needs to be elaborated (TODO).
+
+Now we'll try to commit.
+
+    >>> v.commit(i) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ConflictError: <zc.vault.core.Manifest object at ...>
+
+Conflicts?  We don't need no stinking conflicts!  We didn't even merge!  Where
+did this come from?
+
+The default vault takes a very strict approach to keeping track of conflicts:
+for instance, if you add something and then delete it in the same inventory,
+it will regard this as an "orphan conflict": a change that happened in this
+inventory that will not be committed.  You must explicitly say that it is
+OK for these orphaned changes to be lost.  Let's look at the orphans.
+
+    >>> orphans = list(i.iterOrphanConflicts())
+    >>> sorted(repr(item.object) for item in orphans)
+    ["<Demo u'c1'>", "<Demo u'd1'>"]
+    >>> orphans[0].parent # None
+    >>> orphans[0].name # None
+
+Ah yes--you can see that we deleted these objects above: we deleted "mydemo"
+(d1) and cathy (c1).  We'll just tell the inventory that it is ok to not
+include them.  If vault clients want to have more automation so that deletions
+automatically resolve, then they have the tools to do so.  After the
+resolution, iterOrphanConflicts will then be empty, and iterOrphanResolutions
+will include the objects.
+
+    >>> for o in orphans:
+    ...     o.resolveOrphanConflict()
+    ...
+    >>> len(list(i.iterOrphanConflicts()))
+    0
+    >>> sorted(repr(item.object) for item in i.iterOrphanResolutions())
+    ["<Demo u'c1'>", "<Demo u'd1'>"]
+
+Now when we commit, all objects will be versioned, and we will receive events
+for the versioning and the committing.  The events list represents recent
+events; when this document is run as a test, it is populated by listening for
+all events and attaching them to the list.
+
+    >>> v.commit(i)
+    >>> interfaces.IManifestCommitted.providedBy(events[-1])
+    True
+    >>> events[-1].object is manifest
+    True
+    >>> manifest.__parent__ is v
+    True
+    >>> IVersioning(app['a1'])._z_versioned
+    True
+    >>> IVersioning(app['b1'])._z_versioned
+    True
+    >>> IVersioning(app['c1'])._z_versioned
+    True
+    >>> IVersioning(app['d1'])._z_versioned
+    True
+    >>> manifest._z_versioned
+    True
+    >>> v.manifest is manifest
+    True
+    >>> len(v)
+    1
+
+After the versioning, the inventory enforces the versioning: no more changes
+can be made.
+
+    >>> i.contents['foo'] = Demo()
+    Traceback (most recent call last):
+    ...
+    VersionedError
+    >>> i.contents.updateOrder(())
+    Traceback (most recent call last):
+    ...
+    VersionedError
+    >>> i.contents('abe')('catherine')['foo'] = Demo()
+    Traceback (most recent call last):
+    ...
+    VersionedError
+
+    >>> v.manifest._z_versioned
+    True
+
+Enforcing the versioning of the inventory's objects is the responsibility of
+other configuration.
+
+The manifest now has an __name__ which is the string of its index.  This is
+of very limited usefulness, but with the right traverser might still allow
+items in the held container to be traversed to.
+
+    >>> i.manifest.__name__
+    u'0'
+
+After every commit, the vault should be able to determine the previous and
+next versions of every relationship.  Since this is the first commit, previous
+will be None, but we'll check it now anyway, building a function that checks
+the most recent manifest of the vault.
+
+    >>> def checkManifest(m):
+    ...     v = m.vault
+    ...     for r in m:
+    ...         p = v.getPrevious(r)
+    ...         assert (p is None or
+    ...                 r.__parent__.vault is not v or
+    ...                 p.__parent__.vault is not v or
+    ...                 v.getNext(p) is r)
+    ...
+    >>> checkManifest(v.manifest)
+
+Creating a new working inventory requires a new manifest, based on the old
+manifest.
+
+For better or worse, the package offers four approaches to this.  We
+can create a new working inventory by specifying a vault, from which
+the most recent manifest will be selected, and "mutable=True";
+
+    >>> i = Inventory(vault=v, mutable=True)
+    >>> manifest = i.manifest
+    >>> manifest._z_versioned
+    False
+
+by specifying an inventory, from which its manifest will be
+extracted, and "mutable=True";
+
+    >>> i = Inventory(inventory=v.inventory, mutable=True)
+    >>> manifest = i.manifest
+    >>> manifest._z_versioned
+    False
+
+by specifying a versioned manifest and "mutable=True";
+
+    >>> i = Inventory(v.manifest, mutable=True)
+    >>> manifest = i.manifest
+    >>> manifest._z_versioned
+    False
+
+or by specifying a mutable manifest.
+
+    >>> i = Inventory(Manifest(v.manifest))
+    >>> i.manifest._z_versioned
+    False
+
+These multiple spellings should be reexamined at a later date, and may have
+a deprecation period.  The last spelling--an explicit pasing of a manifest to
+an inventory--is the most likely to remain stable, because it clearly allows
+instantiation of the inventory wrapper for a working manifest or a versioned
+manifest.
+
+Note that, as mentioned above, the inventory is just an API wrapper around the
+manifest: therefore, changes to inventories that share a manifest will be
+shared among them.
+
+    >>> i_extra = Inventory(i.manifest)
+    >>> manifest._z_versioned
+    False
+
+In any case, we now have an inventory that has the same contents as the
+original.
+
+    >>> i.contents.keys() == v.inventory.contents.keys()
+    True
+    >>> i.contents['barbara'] is v.inventory.contents['barbara']
+    True
+    >>> i.contents['abe'] is v.inventory.contents['abe']
+    True
+    >>> i.contents['donald'] is v.inventory.contents['donald']
+    True
+    >>> i.contents('abe')['catherine'] is v.inventory.contents('abe')['catherine']
+    True
+    >>> i.contents('abe')('catherine')['anna'] is \
+    ... v.inventory.contents('abe')('catherine')['anna']
+    True
+
+We can now manipulate the new inventory as we did the old one.
+
+    >>> app['d2'] = Demo()
+    >>> i.contents['donald'] = app['d2']
+    >>> i.contents['donald'] is v.inventory.contents['donald']
+    False
+
+Now our hierarchy looks like this::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd2'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+Now we can observe our local changes.  One way to do this is to examine
+the results of iterChangedItems.
+
+    >>> len(list(i.iterChangedItems()))
+    1
+    >>> iter(i.iterChangedItems()).next() == i.contents('donald')
+    True
+
+Another is to look at each inventory item.  The items specify the type of
+information in the item: whether it is from the 'base', the 'local' changes,
+or a few other options we'll see when we examine merges.
+
+    >>> i.contents('abe').type
+    'base'
+    >>> i.contents('donald').type
+    'local'
+
+This will be true whether or not the change is returned to the original value
+by hand.
+
+    >>> i.contents['donald'] = app['d1']
+    >>> v.inventory.contents['donald'] is i.contents['donald']
+    True
+
+However, unchanged local copies are not included in the iterChangedItems
+results; they are also discarded on commit, as we will see below.
+
+    >>> len(list(i.iterChangedItems()))
+    0
+
+Now our hierarchy looks like this again::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+Each inventory item represents a single collection of data that stores an
+object and its effective hierarchy.  Therefore, changing either (or both) will
+generate a local inventory item.
+
+    >>> app['e1'] = Demo()
+    >>> i.contents('barbara').type
+    'base'
+    >>> i.contents('barbara')['edna'] = app['e1']
+    >>> i.contents('barbara').type
+    'local'
+    >>> i.contents['barbara'] is v.inventory.contents['barbara']
+    True
+    >>> len(list(i.iterChangedItems()))
+    2
+
+Those are two changes: one new node (edna) and one changed node (barbara got a
+new child).
+
+Now our hierarchy looks like this ("*" indicates a changed node)::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'*  'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+                 /            |
+                /             |
+             'edna'*     'catherine'
+         <Demo u'e1'>    <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+Modifying the collection of the top level contents means that we have a change
+as well: even though the inventory does not keep track of a single object at
+the top of the hierarchy, it does keep track of containment at the top level.
+
+    >>> i.contents.type
+    'base'
+    >>> app['f1'] = Demo()
+    >>> i.contents['fred'] = app['f1']
+    >>> i.contents.type
+    'local'
+    >>> len(list(i.iterChangedItems()))
+    4
+
+That's four changes: edna, barbara, fred, and the top node.
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+                               (top node)*
+                              /   /  \  \
+                          ----   /    \  ---------
+                         /      |      |          \
+                'barbara'*    'abe'   'donald'     'fred'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>  <Demo u'f1'>
+                 /            |
+                /             |
+             'edna'*     'catherine'
+         <Demo u'e1'>    <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+You can actually examine the base from the changed item--and even switch back.
+The `base_item` attribute always returns an item with the original object and
+containment.  The `local_item` returns an item with local changes, or None if
+no changes have been made.  A `select` method allows you to switch the given
+item to look at one or the other by default.  The readonly `selected`
+attribute allows introspection.
+
+    >>> list(i.contents.keys())
+    ['barbara', 'abe', 'donald', 'fred']
+    >>> i.contents == i.contents.local_item
+    True
+    >>> list(i.contents('barbara').keys())
+    ['edna']
+    >>> i.contents('barbara') == i.contents('barbara').local_item
+    True
+    >>> i.contents('barbara').local_item.selected
+    True
+    >>> i.contents('barbara').base_item.selected
+    False
+    >>> len(i.contents('barbara').base_item.keys())
+    0
+    >>> list(i.contents.base_item.keys())
+    ['barbara', 'abe', 'donald']
+    >>> i.contents('barbara').base_item.select()
+    >>> len(list(i.iterChangedItems()))
+    3
+
+That's fred, the top level, /and/ edna: edna still is a change, even though
+she is inaccessible with the old version of barbara.  If we were to commit now,
+we would have to resolve the orphan, as shown above.
+
+    >>> v.commit(i) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ConflictError: <zc.vault.core.Manifest object at ...>
+    >>> list(item.object for item in i.iterOrphanConflicts())
+    [<Demo u'e1'>]
+
+Let's look around a little more and switch things back:
+
+    >>> i.contents('barbara').local_item.selected
+    False
+    >>> i.contents('barbara').base_item.selected
+    True
+    >>> len(i.contents('barbara').keys())
+    0
+    >>> i.contents('barbara') == i.contents('barbara').local_item
+    False
+    >>> i.contents('barbara') == i.contents('barbara').base_item
+    True
+    >>> i.contents('barbara').local_item.select()
+    >>> len(list(i.iterChangedItems()))
+    4
+    >>> i.contents('barbara').local_item.selected
+    True
+    >>> i.contents('barbara').base_item.selected
+    False
+    >>> list(i.contents('barbara').keys())
+    ['edna']
+
+The inventory has booleans to examine whether a base item or local item exists,
+as a convenience (and optimization opportunity).
+
+    >>> i.contents('fred').has_local
+    True
+    >>> i.contents('fred').has_base
+    False
+    >>> i.contents('abe')('catherine').has_local
+    False
+    >>> i.contents('abe')('catherine').has_base
+    True
+    >>> i.contents('barbara').has_local
+    True
+    >>> i.contents('barbara').has_base
+    True
+
+It also has four other similar properties, `has_updated`, `has_suggested`,
+`has_modified`, and `has_merged`, which we will examine later.
+
+Before we commit we are going to make one more change to the inventory.  We'll
+make a change to "anna".  Notice how we spell this in the code: it this is the
+first object we have put in an inventory that does not already have a location
+in app.  When an inventory is asked to version an object without an ILocation,
+it stores it in a special folder on the manifest named "held".  Held objects
+are assigned names using the standard Zope 3 name chooser pattern and can be
+moved out even after being versioned.  In this case we will need to register a
+name chooser for our demo objects.  We'll use the standard one.
+
+    >>> from zope.app.container.contained import NameChooser
+    >>> from zope.app.container.interfaces import IWriteContainer
+    >>> component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+    >>> len(i.manifest.held)
+    0
+    >>> i.contents('abe')('catherine')['anna'] = Demo()
+    >>> len(i.manifest.held)
+    1
+    >>> i.manifest.held.values()[0] is i.contents('abe')('catherine')['anna']
+    True
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+                               (top node)*
+                              /   /  \  \
+                          ----   /    \  ---------
+                         /      |      |          \
+                'barbara'*    'abe'   'donald'     'fred'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>  <Demo u'f1'>
+                 /            |
+                /             |
+             'edna'*     'catherine'
+         <Demo u'e1'>    <Demo u'c1'>
+                              |
+                              |
+                           'anna'*
+                         <Demo ...>
+
+In our previous inventory commit, objects were versioned in place.  The vault
+code provides a hook to generate objects for committing to vault: it tries to
+adapt objects it wants to version to zc.vault.interfaces.IVersionFactory.
+This interface specifies any callable object.  Let's provide an example.
+
+The policy here is that if the object is in the inventories' held container,
+just return it, but otherwise "make a copy"--which for our demo just makes a
+new instance and slams the old one's name on it as an attribute.
+
+    >>> @interface.implementer(interfaces.IVersionFactory)
+    ... @component.adapter(interfaces.IVault)
+    ... def versionFactory(vault):
+    ...     def makeVersion(obj, manifest):
+    ...         if obj.__parent__ is manifest.held:
+    ...             return obj
+    ...         res = Demo()
+    ...         res.source_name = obj.__name__
+    ...         return res
+    ...     return makeVersion
+    ...
+    >>> component.provideAdapter(versionFactory)
+
+Let's commit now, to show the results.  We'll discard the change to barbara.
+
+    >>> len(list(i.iterChangedItems()))
+    5
+    >>> i.contents('barbara')('edna').resolveOrphanConflict()
+    >>> i.contents('barbara').base_item.select()
+    >>> len(list(i.iterChangedItems()))
+    4
+
+Edna is included even though she is resolved.
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+                               (top node)*
+                              /   /  \  \
+                          ----   /    \  ---------
+                         /      |      |          \
+                'barbara'     'abe'   'donald'     'fred'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>  <Demo u'f1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'*
+                         <Demo ...>
+
+    >>> changed = dict(
+    ...     (getattr(item, 'name', None), item)
+    ...     for item in i.iterChangedItems())
+    >>> changed['anna'].parent.name
+    'catherine'
+    >>> changed['fred'].object
+    <Demo u'f1'>
+    >>> changed['edna'].object
+    <Demo u'e1'>
+    >>> list(changed[None].keys())
+    ['barbara', 'abe', 'donald', 'fred']
+    >>> old_objects = dict(
+    ...     (k, i.object) for k, i in changed.items() if k is not None)
+    >>> v.commit(i)
+    >>> checkManifest(v.manifest)
+    >>> len(v)
+    2
+    >>> v.manifest is i.manifest
+    True
+    >>> v.inventory == i
+    True
+
+We committed the addition of fred, but not the addition of edna.  Once an
+inventory is committed, unselected changes are discarded.  Also, as mentioned
+above, the data for local item for `donald` has been discarded, since it did
+not include any changes.
+
+    >>> i.contents.local_item == i.contents
+    True
+    >>> i.contents.type
+    'local'
+    >>> i.contents('barbara').local_item # None
+    >>> i.contents('barbara').type
+    'base'
+    >>> i.contents('donald').local_item # None
+    >>> i.contents('donald').type
+    'base'
+    >>> IVersioning(app['e1'])._z_versioned
+    False
+
+Our changes are a bit different than what we had when we began the commit,
+because of the version Factory.  The f1 is not versioned, because we have made
+a copy instead.
+
+    >>> IVersioning(app['f1'])._z_versioned
+    False
+    >>> new_changed = dict(
+    ...     (getattr(item, 'name', None), item)
+    ...     for item in i.iterChangedItems())
+    >>> new_changed['anna'].parent.name
+    'catherine'
+    >>> new_changed['anna'].object is old_objects['anna']
+    True
+    >>> new_changed['fred'].object is old_objects['fred']
+    False
+    >>> new_changed['fred'].object is app['f1']
+    False
+    >>> new_changed['fred'].object.source_name
+    u'f1'
+    >>> IVersioning(new_changed['anna'].object)._z_versioned
+    True
+    >>> IVersioning(new_changed['fred'].object)._z_versioned
+    True
+
+Now that we have two versions in the vault, we can introduce two
+additional attributes of the inventories, contents, and items: `next` and
+`previous`.  These attributes let you time travel in the vault's history.
+
+We also look at similar attributes on the manifest, and at the vault's
+`getInventory` method.
+
+For instance, the current inventory's `previous` attribute points to the
+original inventory, and vice versa.
+
+    >>> i.previous == v.getInventory(0)
+    True
+    >>> i.manifest.previous is v[0]
+    True
+    >>> v.getInventory(0).next == i == v.inventory
+    True
+    >>> v[0].next is i.manifest is v.manifest
+    True
+    >>> i.next # None
+    >>> manifest.next # None
+    >>> v.getInventory(0).previous # None
+    >>> v[0].previous # None
+
+The same is true for inventory items.
+
+    >>> list(v.inventory.contents.previous.keys())
+    ['barbara', 'abe', 'donald']
+    >>> list(v.getInventory(0).contents.next.keys())
+    ['barbara', 'abe', 'donald', 'fred']
+    >>> v.inventory.contents.previous.next == v.inventory.contents
+    True
+    >>> v.inventory.contents('abe')('catherine')('anna').previous.object
+    <Demo u'a1'>
+    >>> (v.inventory.contents('abe').relationship is
+    ...  v.inventory.contents.previous('abe').relationship)
+    True
+
+Once you step to a previous or next item, further steps from the item remain
+in the previous or next inventory.
+
+    >>> v.inventory.contents('abe')('catherine')['anna'].__name__ == 'a1'
+    False
+    >>> v.inventory.contents.previous('abe')('catherine')['anna']
+    <Demo u'a1'>
+
+In addition, inventory items support `previous_version` and `next_version`.
+The difference between these and `previous` and `next` is that the `*_version`
+variants skip to the item that was different than the current item.  For
+instance, while the previous_version of the 'anna' is the old 'a1' object,
+just like the `previous` value, the previous_version of 'abe' is None, because
+it has no previous version.
+
+    >>> v.inventory.contents(
+    ...     'abe')('catherine')('anna').previous_version.object
+    <Demo u'a1'>
+    >>> v.inventory.contents('abe').previous_version # None
+
+These leverage the `getPrevious` and `getNext` methods on the vault, which work
+with relationships.
+
+The previous and next tools are even more interesting when tokens move: you
+can see positions change within the hierarchy.  Inventories have a `moveTo`
+method that can let the inventory follow the moves to maintain history.  We'll
+create a new inventory copy and demonstrate.  As we do, notice that
+inventory items obtained before the move correctly reflect the move, as
+described above.
+
+    >>> manifest = Manifest(v.manifest)
+    >>> del app['inventory']
+    >>> i = app['inventory'] = Inventory(manifest)
+    >>> item = i.contents('abe')('catherine')
+    >>> item.parent.name
+    'abe'
+    >>> i.contents('abe')('catherine').moveTo(i.contents('fred'))
+    >>> item.parent.name
+    'fred'
+    >>> len(i.contents('abe').keys())
+    0
+    >>> list(i.contents('fred').keys())
+    ['catherine']
+
+The change actually only affects the source and target of the move.
+
+    >>> changes = dict((getattr(item, 'name'), item)
+    ...                for item in i.iterChangedItems())
+    >>> len(changes)
+    2
+    >>> changes['fred'].values()
+    [<Demo u'c1'>]
+    >>> len(changes['abe'].keys())
+    0
+
+So now our hierarchy looks like this ("*" indicates a changed node)::
+
+                               (top node)
+                              /   /  \  \
+                          ----   /    \  ---------
+                         /      |      |          \
+                'barbara'     'abe'*  'donald'     'fred'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>  <Demo u'f1'>
+                                                       |
+                                                       |
+                                                  'catherine'
+                                                  <Demo u'c1'>
+                                                       |
+                                                       |
+                                                    'anna'
+                                                  <Demo ...>
+
+If you try to move parts of the hierarchy to someplace that has the same name,
+you will receive a ValueError unless you specify a name that does not
+conflict.
+
+    >>> i.contents('abe')['donald'] = app['d2']
+    >>> i.contents('donald').moveTo(i.contents('abe'))
+    Traceback (most recent call last):
+    ...
+    ValueError: Object with same name already exists in new location
+    >>> i.contents('donald').moveTo(i.contents('abe'), 'old_donald')
+    >>> i.contents('abe').items()
+    [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+                             (top node)*
+                              /  |   \
+                          ----   |     ----
+                         /       |         \
+                'barbara'     'abe'*        'fred'*
+            <Demo u'b1'>   <Demo u'a1'>     <Demo u'f1'>
+                           /         \             |
+                          /           \            |
+                     'donald'*    'old_donald'  'catherine'
+                   <Demo u'd2'>   <Demo u'd1'>  <Demo u'c1'>
+                                                   |
+                                                   |
+                                                 'anna'
+                                                <Demo ...>
+
+If you try to move part of the hierarchy to someplace within itself, you will
+also receive a ValueError.
+
+    >>> i.contents('fred').moveTo(i.contents('fred')('catherine')('anna'))
+    Traceback (most recent call last):
+    ...
+    ValueError: May not move item to within itself
+
+It is for this reason that the contents does not support the moveTo operation.
+
+    >>> hasattr(i.contents, 'moveTo')
+    False
+
+If you move an object to the same folder it is a silent noop, unless you are
+using the move as a rename operation and the new name conflicts.
+
+    >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'))
+    >>> i.contents('abe').items()
+    [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+    >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'), 'donald')
+    Traceback (most recent call last):
+    ...
+    ValueError: Object with same name already exists in new location
+    >>> i.contents('abe').items()
+    [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+    >>> i.contents('abe')('donald').moveTo(i.contents('abe'),
+    ...                                    'new_donald')
+    >>> i.contents('abe').items()
+    [('old_donald', <Demo u'd1'>), ('new_donald', <Demo u'd2'>)]
+
+Notice in the last part of the example above that the move within the folder
+also changed the order.
+
+It's also interesting to note that, with all these changes, we only have two
+additional changed items: the addition of new_donald, and the changed
+containment of the contents.  old_donald, for instance, is not considered to
+be changed; only its containers were.
+
+    >>> changes = dict((getattr(item, 'name', None), item)
+    ...                for item in i.iterChangedItems())
+    >>> len(changes)
+    4
+    >>> changes['fred'].items()
+    [('catherine', <Demo u'c1'>)]
+    >>> changes['abe'].items()
+    [('old_donald', <Demo u'd1'>), ('new_donald', <Demo u'd2'>)]
+    >>> changes['new_donald'].object
+    <Demo u'd2'>
+    >>> list(changes[None].keys())
+    ['barbara', 'abe', 'fred']
+
+Now that we have moved some objects that existed in previous inventories--
+catherine (containing anna) was moved from abe to fred, and donald was moved
+from the root contents to abe and renamed to 'old_donald'--we can examine
+the previous and previous_version pointers.
+
+    >>> i.contents('abe')('old_donald').previous.parent == i.previous.contents
+    True
+    >>> i.contents('abe')('old_donald').previous_version # None
+
+The previous_version is None because, as seen in the iterChangedItems example,
+donald didn't actually change--only its containers did.  previous_version does
+work for both local changes and changes in earlier inventories, though.
+
+    >>> list(i.contents('abe').keys())
+    ['old_donald', 'new_donald']
+    >>> list(i.contents('abe').previous.keys())
+    ['catherine']
+    >>> (i.contents('fred')('catherine')('anna').previous.inventory ==
+    ...  v.inventory)
+    True
+    >>> (i.contents('fred')('catherine')('anna').previous_version.inventory ==
+    ...  v.getInventory(0))
+    True
+
+The previous_version of anna is the first one that was committed in the
+initial inventory--it didn't change in this version, but in the most recently
+committed inventory, so the previous version is the very first one committed.
+
+By the way, notice that, while previous and previous_version point to the
+inventories from which the given item came, the historical, versioned
+inventories in the vault don't point to this working inventory in next or
+next_version because this inventory has not been committed yet.
+
+    >>> v.inventory.contents('abe').next # None
+    >>> v.inventory.contents('abe').next_version # None
+
+As mentioned above, only inventory items support `moveTo`, not the top-node
+inventory contents.  Both contents and inventory items support a `copyTo`
+method.  This is similar to moveTo but it creates new additional locations in
+the inventory for the same objects; the new locations don't maintain any
+history.  It is largely a short hand for doing "location1['foo'] =
+location2['foo']" for all objects in a part of the inventory.  The only
+difference is when copying between inventories, as we will see below.
+
+The basic `copyTo` machinery is very similar to `moveTo`.  We'll first copy
+catherine and anna to within the contents.
+
+    >>> i.contents('fred')('catherine').copyTo(i.contents)
+    >>> list(i.contents.keys())
+    ['barbara', 'abe', 'fred', 'catherine']
+    >>> list(i.contents('catherine').keys())
+    ['anna']
+    >>> i.contents['catherine'] is i.contents('fred')['catherine']
+    True
+    >>> (i.contents('catherine')('anna').object is
+    ...  i.contents('fred')('catherine')('anna').object)
+    True
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+                                (top node)*
+                       --------/  /   \   \-----------
+                      /          /     \              \
+                     /          /       \              \
+            'barbara'      'abe'*        'fred'*        'catherine'*
+        <Demo u'b1'>   <Demo u'a1'>     <Demo u'f1'>   <Demo u'c1'>
+                       /         \             |             |
+                      /           \            |             |
+              'new_donald'*   'old_donald'  'catherine'    'anna'*
+               <Demo u'd2'>   <Demo u'd1'>  <Demo u'c1'>   <Demo ...>
+                                               |
+                                               |
+                                             'anna'
+                                            <Demo ...>
+
+Now we have copied objects from one location to another.  The copies are unlike
+the originals because they do not have any history.
+
+    >>> i.contents('fred')('catherine')('anna').previous is None
+    False
+    >>> i.contents('catherine')('anna').previous is None
+    True
+
+However, they do know their copy source.
+
+    >>> (i.contents('catherine')('anna').copy_source ==
+    ...  i.contents('fred')('catherine')('anna'))
+    True
+
+As with `moveTo`, you may not override a name, but you may explicitly provide
+one.
+
+    >>> i.contents['anna'] = Demo()
+    >>> i.contents('catherine')('anna').copyTo(i.contents)
+    Traceback (most recent call last):
+    ...
+    ValueError: Object with same name already exists in new location
+    >>> i.contents('catherine')('anna').copyTo(i.contents, 'old_anna')
+    >>> list(i.contents.keys())
+    ['barbara', 'abe', 'fred', 'catherine', 'anna', 'old_anna']
+    >>> del i.contents['anna']
+    >>> del i.contents['old_anna']
+
+Unlike with `moveTo`, if you try to copy a part of the hierarchy on top of
+itself (same location, same name), the inventory will raise an error.
+
+    >>> i.contents('catherine')('anna').copyTo(i.contents('catherine'))
+    Traceback (most recent call last):
+    ...
+    ValueError: Object with same name already exists in new location
+
+You can actually copyTo a location in a completely different inventory, even
+from a separate vault.
+
+    >>> another = app['another'] = Vault()
+    >>> another_i = app['another_i'] = Inventory(vault=another)
+    >>> len(another_i.contents)
+    0
+    >>> i.contents('abe').copyTo(another_i.contents)
+    >>> another_i.contents['abe']
+    <Demo u'a1'>
+    >>> another_i.contents('abe')['new_donald']
+    <Demo u'd2'>
+    >>> another_i.contents('abe')['old_donald']
+    <Demo u'd1'>
+
+We haven't committed for awhile, so let's commit this third revision.  We did
+a lot of deletes, so let's just accept all of the orphan conflicts.
+
+    >>> for item in i.iterOrphanConflicts():
+    ...     item.resolveOrphanConflict()
+    ...
+    >>> v.commit(i)
+    >>> checkManifest(v.manifest)
+
+In a future revision of the zc.vault package, it may be possible to move and
+copy between inventories. At the time of writing, this use case is
+unnecessary, and doing so will have unspecified behavior.
+
+We have now discussed the core API for the vault system for basic use.  A
+number of other use cases are important, however:
+
+- revert to an older inventory;
+
+- merge concurrent changes;
+
+- track an object in a vault; and
+
+- traverse through a vault using URL or TALES paths.
+
+Reverting to an older inventory is fairly simple: use the 'commitFrom'
+method to copy and commit an older version into a new copy.  The same
+works with manifests.
+
+    >>> v.commitFrom(v[0])
+
+The data is now as it was in the old version.
+
+    >>> list(v.inventory.contents.keys())
+    ['barbara', 'abe', 'donald']
+
+Now our hierarchy looks like this again::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'   'abe'    'donald'
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+The `commitFrom` method will take any committed manifest from a vault that
+shares the same intids utility.  It creates a new manifest that duplicates the
+provided one.
+
+    >>> v.inventory.contents('abe')('catherine').previous.parent.name
+    'fred'
+    >>> v.manifest.previous is v[-2]
+    True
+    >>> v.manifest.base_source is v[-2]
+    True
+    >>> v.manifest.base_source is v[0]
+    False
+    >>> v[-2].base_source is v[-3]
+    True
+
+Note that this approach will cause an error:
+
+    >>> v.commit(Manifest(v[0])) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    OutOfDateError: <zc.vault.core.Manifest object at ...>
+
+Again, use `commitFrom` to revert.
+
+Now we come to the most complex vault use case: concurrent changes to a vault,
+merging inventories.  The vault design supports a number of features for these
+sorts of use cases.
+
+The basic merge story is that if one or more commits happen to a vault while
+an inventory from the vault is being worked on, so that the base of a working
+inventory is no longer the most recent committed inventory, and thus cannot
+be committed normally...
+
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> long_running.manifest.base_source is v.manifest
+    True
+    >>> short_running.contents['donald'] = app['d2']
+    >>> short_running.contents.items()
+    [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>), ('donald', <Demo u'd2'>)]
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> short_running.contents('barbara')['fred'] = app['f1']
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> long_running.manifest.base_source is v.manifest
+    False
+    >>> long_running.manifest.base_source is v.manifest.previous.previous
+    True
+    >>> long_running.contents['edna'] = app['e1']
+    >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE
+    [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>),
+     ('donald', <Demo u'd1'>), ('edna', <Demo u'e1'>)]
+    >>> v.commit(long_running) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    OutOfDateError: <zc.vault.core.Manifest object at ...>
+
+...then the inventory can be updated; and, if there are no problems with the
+update, then the inventory can be committed.
+
+short_running, and the head of the vault, looks like this now ("*" indicates a
+change from the previous version)::
+
+                         (top node)
+                         /    |    \
+                        /     |     \
+                'barbara'*  'abe'    'donald'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd2'>
+                |             |
+                |             |
+             'fred'*      'catherine'
+          <Demo u'f1'>    <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+long_running looks like this::
+
+                                (top node)*
+                         ------/  /   \  \----------
+                        /        /     \            \
+                'barbara'   'abe'    'donald'       'edna'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>  <Demo u'e1'>
+                              |
+                              |
+                         'catherine'
+                         <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+The contents node changed and 'edna' was added.
+
+By default, an update is to the current inventory of the inventory base's vault.
+
+Here's the update.  It will produce no conflicts, because the node changes do
+not overlap (review diagrams above).
+
+    >>> long_running.beginUpdate()
+    >>> long_running.updating
+    True
+
+Post-merge, long_running looks like this ('M' indicates a merged node)::
+
+                                (top node)*
+                         ------/  /   \  \----------
+                        /        /     \            \
+               'barbara'M   'abe'    'donald'M      'edna'*
+            <Demo u'b1'> <Demo u'a1'> <Demo u'd2'>  <Demo u'e1'>
+                 |            |
+                 |            |
+              'fred'M    'catherine'
+           <Demo u'f1'>  <Demo u'c1'>
+                              |
+                              |
+                           'anna'
+                         <Demo u'a1'>
+
+(ADVANCED)
+
+During an update, the local relationships may not be changed, even though they
+are not versioned.
+
+    >>> long_running.contents('edna').type
+    'local'
+    >>> long_running.contents('edna').relationship.object = Demo()
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot change local relationships while updating
+    >>> long_running.contents('edna').relationship.object
+    <Demo u'e1'>
+    >>> long_running.contents('edna').relationship._z_versioned
+    False
+    >>> long_running.manifest.getType(long_running.contents.relationship)
+    'local'
+    >>> long_running.contents.relationship.containment.updateOrder(
+    ...     ('abe', 'barbara', 'edna', 'donald'))
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot change local relationships while updating
+    >>> long_running.contents.relationship.containment.keys()
+    ('barbara', 'abe', 'donald', 'edna')
+
+When you change an item or contents, this is hidden by switching to a MODIFIED
+relationship, as seen below.
+
+(end ADVANCED)
+
+Now that we have updated, our `update_source` on the inventory shows the
+inventory used to do the update.
+
+    >>> long_running.manifest.base_source is v[-3]
+    True
+    >>> long_running.manifest.update_source is short_running.manifest
+    True
+
+What changes should the update reflect?  iterChangedItems takes an optional
+argument which can use an alternate base to calculate changes, so we can use
+that with the long_running.base to see the effective merges.
+
+    >>> changed = dict((getattr(item, 'name', None), item) for item in
+    ...                short_running.iterChangedItems(
+    ...                     long_running.manifest.base_source))
+    >>> changed['donald'].object.source_name
+    u'd2'
+    >>> changed['fred'].object.source_name
+    u'f1'
+    >>> list(changed['barbara'].keys())
+    ['fred']
+
+Our contents show these merged results.
+
+    >>> list(long_running.contents.keys())
+    ['barbara', 'abe', 'donald', 'edna']
+    >>> long_running.contents['donald'].source_name
+    u'd2'
+    >>> long_running.contents('barbara')['fred'].source_name
+    u'f1'
+
+You cannot update to another inventory until you `abortUpdate` or
+`completeUpdate`, as we discuss far below.
+
+    >>> long_running.beginUpdate(v[-2])
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot begin another update while updating
+
+We'll show `abortUpdate`, then redo the update.  A characteristic of
+abortUpdate is that it should revert all changes you made while updating.  For
+instance, we'll select another version of the contents and even add an item.
+The changes will all go away when we abort.
+
+    >>> len(list(long_running.iterChangedItems()))
+    5
+    >>> long_running.contents['fred'] = app['f1']
+    >>> list(long_running.contents.keys())
+    ['barbara', 'abe', 'donald', 'edna', 'fred']
+    >>> len(list(long_running.iterChangedItems()))
+    6
+    >>> long_running.abortUpdate()
+    >>> long_running.manifest.update_source # None
+    >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE
+    [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>),
+     ('donald', <Demo u'd1'>), ('edna', <Demo u'e1'>)]
+    >>> len(list(long_running.iterChangedItems()))
+    2
+    >>> long_running.beginUpdate()
+    >>> list(long_running.contents.keys())
+    ['barbara', 'abe', 'donald', 'edna']
+    >>> long_running.contents['donald'].source_name
+    u'd2'
+    >>> long_running.contents('barbara')['fred'].source_name
+    u'f1'
+
+Now we'll look around more at the state of things.  We can use
+iterChangedItems to get a list of all changed and updated.  As already seen in
+the examples, `update_source` on the inventory shows the inventory used to do
+the update.
+
+    >>> updated = {}
+    >>> changed = {}
+    >>> for item in long_running.iterChangedItems():
+    ...     name = getattr(item, 'name', None)
+    ...     if item.type == interfaces.LOCAL:
+    ...         changed[name] = item
+    ...     else:
+    ...         assert item.type == interfaces.UPDATED
+    ...         updated[name] = item
+    ...
+    >>> len(updated)
+    3
+    >>> updated['donald'].object.source_name
+    u'd2'
+    >>> updated['fred'].object.source_name
+    u'f1'
+    >>> list(updated['barbara'].keys())
+    ['fred']
+    >>> len(changed)
+    2
+    >>> list(changed[None].keys())
+    ['barbara', 'abe', 'donald', 'edna']
+    >>> changed['edna'].object
+    <Demo u'e1'>
+
+The `has_updated` and `updated_item` attributes, which only come into effect
+when an inventory is in the middle of an update, let you examine the changes
+from a more local perspective.
+
+    >>> long_running.contents('donald').has_local
+    False
+    >>> long_running.contents('donald').has_updated
+    True
+    >>> (long_running.contents('donald').updated_item.relationship is
+    ...  long_running.contents('donald').relationship)
+    True
+
+There are three kinds of problems that can prevent a post-merge commit: item
+conflicts, orphans, and parent conflicts.  Item conflicts are item updates
+that conflicted with local changes and that the system could not merge (more
+on that below). Orphans are accepted item changes (local or updated) that are
+not accessible from the top contents, and so will be lost.  Parent conflicts
+are items that were moved to one location in the source and another location
+in the local changes, and so now have two parents: an illegal state because it
+makes future merges and sane historical analysis difficult.
+
+These three kinds of problem can be analyzed with
+`iterUpdateConflicts`, `iterOrphanConflicts`, and `iterParentConflicts`,
+respectively.  We have already seen iterOrphanConflicts.  In our current merge,
+we have none of these problems, and we can commit (or completeUpdate)
+successfully.
+
+    >>> list(long_running.iterUpdateConflicts())
+    []
+    >>> list(long_running.iterOrphanConflicts())
+    []
+    >>> list(long_running.iterParentConflicts())
+    []
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+We had a lot of discussion between the most important points here, so to
+review, all we had to do in the simple case was this::
+
+    long_running.beginUpdate()
+    v.commit(long_running)
+
+We could have rejected some of the updates and local changes, which might
+have made things more interesting; and the two steps let you analyze the update
+changes to tweak things as desired.  But the simplest case allows a simple
+spelling.
+
+Now let's explore the possible merging problems.  The first, and arguably most
+complex, is item conflict.  An item conflict is easy to provoke.  We can do it
+by manipulating the containment or the object of an item.  Here we'll
+manipulate the containment order of the root.
+
+    >>> list(v.inventory.contents.keys())
+    ['barbara', 'abe', 'donald', 'edna']
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> short_running.contents.updateOrder(
+    ...     ('abe', 'barbara', 'edna', 'donald'))
+    >>> long_running.contents.updateOrder(
+    ...     ('abe', 'barbara', 'donald', 'edna'))
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> long_running.beginUpdate()
+    >>> v.commit(long_running)
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot complete update with conflicts
+    >>> conflicts = list(long_running.iterUpdateConflicts())
+    >>> len(conflicts)
+    1
+    >>> conflict = conflicts[0]
+    >>> conflict.type
+    'local'
+    >>> list(conflict.keys())
+    ['abe', 'barbara', 'donald', 'edna']
+    >>> conflict.is_update_conflict
+    True
+    >>> conflict.selected
+    True
+    >>> conflict.has_updated
+    True
+    >>> list(conflict.updated_item.keys())
+    ['abe', 'barbara', 'edna', 'donald']
+
+As you can see, we have the tools to find out the conflicts and examine them.
+To resolve this conflict, we merely need to use the `resolveUpdateConflict`
+method.  We can select the desired one we want, or even create a new one and
+modify it, before or after marking it resolved.
+
+Let's create a new one.  All you have to do is start changing the item, and a
+new one is created.  You are not allowed to directly modify local changes when
+you are updating, so that the system can revert to them; but you may create
+'modified' versions (that will be discarded if the update is aborted).
+
+    >>> len(list(conflict.iterModifiedItems()))
+    0
+    >>> conflict.has_modified
+    False
+    >>> conflict.selected
+    True
+    >>> conflict.type
+    'local'
+    >>> list(conflict.keys())
+    ['abe', 'barbara', 'donald', 'edna']
+    >>> conflict.updateOrder(['abe', 'donald', 'barbara', 'edna'])
+    >>> len(list(conflict.iterModifiedItems()))
+    1
+    >>> conflict.has_modified
+    True
+    >>> conflict.selected
+    True
+    >>> conflict.type
+    'modified'
+    >>> conflict.copy_source.type
+    'local'
+    >>> conflict.copy_source == conflict.local_item
+    True
+    >>> conflict == list(conflict.iterModifiedItems())[0]
+    True
+    >>> list(conflict.local_item.keys())
+    ['abe', 'barbara', 'donald', 'edna']
+    >>> list(conflict.keys())
+    ['abe', 'donald', 'barbara', 'edna']
+    >>> list(conflict.updated_item.keys())
+    ['abe', 'barbara', 'edna', 'donald']
+
+Now we're going to resolve it.
+
+    >>> conflict.resolveUpdateConflict()
+    >>> conflict.is_update_conflict
+    False
+    >>> len(list(long_running.iterUpdateConflicts()))
+    0
+    >>> resolved = list(long_running.iterUpdateResolutions())
+    >>> len(resolved)
+    1
+    >>> resolved[0] == conflict
+    True
+
+Now if we called abortUpdate, the local_item would look the way it did before
+the update, because we modified a separate object.  Let's commit, though.
+
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+Our hierarchy looks like this now::
+
+                                (top node)*
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'M      'barbara'M   'edna'*
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                 |                          |
+                 |                          |
+            'catherine'                 'fred'M
+           <Demo u'c1'>                 <Demo u'f1'>
+                 |
+                 |
+               'anna'
+            <Demo u'a1'>
+
+The vault code allows for adapters to try and suggest merges.  For instance, a
+simple merge might have a policy that one version with an object change and
+another version with a containment change can be merged simply.  This uses
+some APIs we haven't talked about yet: if there is a core.txt in this
+directory, you're in luck; otherwise, hope for help in interfaces.py and
+bother Gary for docs (sorry).
+
+    >>> from zc.vault.core import Relationship
+    >>> @component.adapter(interfaces.IVault)
+    ... @interface.implementer(interfaces.IConflictResolver)
+    ... def factory(vault):
+    ...     def resolver(manifest, local, updated, base):
+    ...         if local.object is not base.object:
+    ...             if updated.object is base.object:
+    ...                 object = local.object
+    ...             else:
+    ...                 return
+    ...         else:
+    ...             object = updated.object
+    ...         if local.containment != base.containment:
+    ...             if updated.containment != base.containment:
+    ...                 return
+    ...             else:
+    ...                 containment = local.containment
+    ...         else:
+    ...             containment = updated.containment
+    ...         suggested = Relationship(local.token, object, containment)
+    ...         manifest.addSuggested(suggested)
+    ...         manifest.select(suggested)
+    ...         manifest.resolveUpdateConflict(local.token)
+    ...     return resolver
+    ...
+    >>> component.provideAdapter(factory)
+
+Now if we merge changes that this policy can handle, we'll have smooth updates.
+
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> app['c2'] = Demo()
+    >>> short_running.contents('abe')['catherine'] = app['c2']
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> long_running.contents('abe')('catherine')['fred'] = app['f1']
+    >>> long_running.beginUpdate()
+    >>> cath = long_running.contents('abe')('catherine')
+    >>> cath.has_suggested
+    True
+    >>> cath.type
+    'suggested'
+    >>> cath.has_updated
+    True
+    >>> cath.selected
+    True
+    >>> cath.has_local
+    True
+    >>> suggestedItems = list(cath.iterSuggestedItems())
+    >>> len(suggestedItems)
+    1
+    >>> suggestedItems[0] == cath
+    True
+    >>> cath.object.source_name
+    u'c2'
+    >>> list(cath.keys())
+    ['anna', 'fred']
+    >>> cath.local_item.object
+    <Demo u'c1'>
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+This means we automatically merged this... ::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                 |                          |
+                 |                          |
+            'catherine'*                 'fred'
+           <Demo u'c2'>                 <Demo u'f1'>
+                 |
+                 |
+               'anna'
+            <Demo u'a1'>
+
+...with this (that would normally produce a conflict with the 'catherine'
+node)... ::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                 |                          |
+                 |                          |
+            'catherine'*                'fred'
+           <Demo u'c1'>                 <Demo u'f1'>
+            /        \
+           /          \
+        'anna'        'fred'*
+     <Demo u'a1'>    <Demo u'f1'>
+
+...to produce this::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                 |                          |
+                 |                          |
+            'catherine'*                'fred'
+           <Demo u'c2'>                 <Demo u'f1'>
+            /        \
+           /          \
+        'anna'        'fred'*
+     <Demo u'a1'>    <Demo u'f1'>
+
+This concludes our tour of item conflicts.  We are left with orphans and
+parent conflicts.
+
+As mentioned above, orphans are accepted, changed items, typically from the
+update or local changes, that are inaccessible from the root of the inventory.
+For example, consider the following.
+
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> list(short_running.contents('abe').keys())
+    ['catherine']
+    >>> list(short_running.contents('abe')('catherine').keys())
+    ['anna', 'fred']
+    >>> del short_running.contents('abe')['catherine']
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> long_running.contents('abe')('catherine')['anna'] = Demo()
+    >>> long_running.beginUpdate()
+    >>> v.commit(long_running)
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot complete update with conflicts
+    >>> orphans =list(long_running.iterOrphanConflicts())
+    >>> len(orphans)
+    1
+    >>> orphan = orphans[0]
+    >>> orphan.parent.name
+    'catherine'
+    >>> orphan.selected
+    True
+    >>> orphan.type
+    'local'
+    >>> orphan.parent.selected
+    True
+    >>> orphan.parent.type
+    'base'
+    >>> orphan.parent.parent.type
+    'base'
+    >>> orphan.parent.parent.selected
+    False
+    >>> orphan.parent.parent.selected_item.type
+    'updated'
+
+To reiterate in a diagram, the short_running inventory deleted the
+'catherine' branch::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                                            |
+                                            |
+                                         'fred'
+                                      <Demo u'f1'>
+
+However, the long running branch made a change to an object that had
+been removed ('anna')::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                 |                          |
+                 |                          |
+            'catherine'                 'fred'
+           <Demo u'c2'>                 <Demo u'f1'>
+            /        \
+           /          \
+        'anna'*       'fred'
+     <Demo ...>     <Demo u'f1'>
+
+So, given the orphan, you can discover the old version of the node that let the
+change occur, and thus the change that hid the orphan.
+
+To resolve an orphan, as seen before, you can `resolveOrphanConflict`, or
+somehow change the tree so that the orphan is within the tree again (using
+`moveTo`).  We'll just resolve it.  Note that resolving keeps it selected: it
+just stops the complaining.
+
+    >>> orphan.selected
+    True
+    >>> orphan.resolveOrphanConflict()
+    >>> orphan.selected
+    True
+    >>> len(list(long_running.iterOrphanConflicts()))
+    0
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+The same happens if the change occurs because of a reversal--the long_running
+inventory performs the delete.
+
+It also can happen if the user explicitly selects a choice that eliminates an
+accepted change, even outside of a merge, as we have seen above.
+
+Parent conflicts are the last sort of conflict.
+
+Our hierarchy now looks like this::
+
+                                (top node)
+                     ----------/  /   \  \----------
+                    /            /     \            \
+               'abe'     'donald'       'barbara'    'edna'
+            <Demo u'a1'> <Demo u'd2'>  <Demo u'b1'> <Demo u'e1'>
+                                            |
+                                            |
+                                         'fred'
+                                      <Demo u'f1'>
+
+The short_running version will be changed to look like this::
+
+                           (top node)
+                     ------/   |    \-------
+                    /          |            \
+               'abe'        'barbara'*      'edna'
+            <Demo u'a1'>   <Demo u'b1'>  <Demo u'e1'>
+                            /      \
+                           /        \
+                        'fred'     'donald'
+                   <Demo u'f1'>   <Demo u'd2'>
+
+The long_running version will look like this. ::
+
+                           (top node)
+                     ------/   |    \-------
+                    /          |            \
+               'abe'        'barbara'      'edna'
+            <Demo u'a1'>   <Demo u'b1'>  <Demo u'e1'>
+                               |
+                               |
+                             'fred'*
+                           <Demo u'f1'>
+                               |
+                               |
+                            'donald'
+                          <Demo u'd2'>
+
+Post-merge the tree looks like this::
+
+                           (top node)
+                     ------/   |    \-------
+                    /          |            \
+               'abe'        'barbara'*      'edna'
+            <Demo u'a1'>   <Demo u'b1'>  <Demo u'e1'>
+                            /      \
+                           /        \
+                        'fred'*    'donald'
+                   <Demo u'f1'>   <Demo u'd2'>
+                        |
+                        |
+                     'donald'
+                   <Demo u'd2'>
+
+The problem is Donald.  It is one token in two or more places: a parent
+conflict.
+
+    >>> short_running = Inventory(Manifest(v.manifest))
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> short_running.contents('donald').moveTo(
+    ...     short_running.contents('barbara'))
+    >>> v.commit(short_running)
+    >>> checkManifest(v.manifest)
+    >>> long_running.contents('donald').moveTo(
+    ...     long_running.contents('barbara')('fred'))
+    >>> long_running.beginUpdate()
+    >>> conflicts = list(long_running.iterParentConflicts())
+    >>> v.commit(long_running)
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot complete update with conflicts
+    >>> conflicts = list(long_running.iterParentConflicts())
+    >>> len(conflicts)
+    1
+    >>> conflict = conflicts[0]
+    >>> conflict.name
+    Traceback (most recent call last):
+    ...
+    ParentConflictError
+    >>> conflict.parent
+    Traceback (most recent call last):
+    ...
+    ParentConflictError
+    >>> selected = list(conflict.iterSelectedParents())
+    >>> len(selected)
+    2
+    >>> sorted((s.type, s.name) for s in selected)
+    [('local', 'fred'), ('updated', 'barbara')]
+    >>> all = dict((s.type, s) for s in conflict.iterParents())
+    >>> len(all)
+    3
+    >>> sorted(all)
+    ['base', 'local', 'updated']
+
+You can provoke these just by accepting a previous version, outside of merges.
+For instance, we can now make a three-way parent conflict by selecting the
+root node.
+
+    >>> all['base'].select()
+    >>> selected = list(conflict.iterSelectedParents())
+    >>> len(selected)
+    3
+
+Now if we resolve the original problem by rejecting the local change,
+we'll still have a problem, because of accepting the baseParent.
+
+    >>> all['local'].base_item.select()
+    >>> selected = list(conflict.iterSelectedParents())
+    >>> len(selected)
+    2
+    >>> v.commit(long_running)
+    Traceback (most recent call last):
+    ...
+    UpdateError: cannot complete update with conflicts
+    >>> all['base'].local_item.select()
+    >>> len(list(long_running.iterParentConflicts()))
+    0
+
+Now our hierarchy looks like short_running again::
+
+                           (top node)
+                     ------/   |    \-------
+                    /          |            \
+               'abe'        'barbara'      'edna'
+            <Demo u'a1'>   <Demo u'b1'>  <Demo u'e1'>
+                            /      \
+                           /        \
+                        'fred'     'donald'
+                   <Demo u'f1'>   <Demo u'd2'>
+
+We can't check this in because there are no effective changes between this
+and the last checkin.
+
+    >>> v.commit(long_running) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    NoChangesError: <zc.vault.core.Manifest object at ...>
+
+So actually, we'll reinstate the local change, reject the short_running
+change (the placement within barbara), and commit.
+
+    >>> all['local'].select()
+    >>> all['updated'].base_item.select()
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+Note that even though we selected the base_item, the relationship generated by
+completing the update is actually local because it is a change from the
+previous updated source.
+
+    >>> v.inventory.contents('barbara').type
+    'local'
+
+There is actually a fourth kind of error: having child nodes in selected
+relationships for which there are no selected relationships.  The code tries to
+disallow this, so it should not be encountered.
+
+Next, we will talk about using vaults to create and manage branches.
+The simple basics of this are that you can commit an inventory based on one
+vault into a fresh vault, and you can then update across the two vaults.  To
+create a vault that can have merged manifests, you must share the internal
+'intids' attribute.  The `createBranch` method is sugar for doing that and then
+(by default) committing the most recent manifest of the current vault as the first
+revision of the branch.
+
+    >>> branch = app['branch'] = v.createBranch()
+    >>> bi = Inventory(Manifest(branch.manifest))
+    >>> branch_start_inventory = v.inventory
+    >>> bi.contents['george'] = Demo()
+    >>> branch.commit(bi)
+    >>> checkManifest(branch.manifest)
+    >>> i = Inventory(Manifest(v.manifest))
+    >>> i.contents['barbara'] = app['b2'] = Demo()
+    >>> v.commit(i)
+    >>> checkManifest(v.manifest)
+    >>> i.contents['barbara'].source_name
+    u'b2'
+    >>> bi = Inventory(Manifest(branch.manifest))
+    >>> bi.contents('barbara')['henry'] = app['h1'] = Demo()
+    >>> branch.commit(bi)
+    >>> checkManifest(branch.manifest)
+
+Now we want to merge the mainline changes with the branch.
+
+    >>> bi = Inventory(Manifest(branch.manifest))
+    >>> (bi.manifest.base_source is bi.manifest.getBaseSource(branch) is
+    ...  branch.manifest)
+    True
+    >>> (bi.manifest.getBaseSource(v) is branch_start_inventory.manifest is
+    ...  v[-2])
+    True
+    >>> bi.beginUpdate(v.inventory)
+    >>> bi.contents['barbara'].source_name
+    u'b2'
+    >>> bi.contents('barbara')['henry'].source_name
+    u'h1'
+
+A smooth update.  But what happens if meanwhile someone changes the branch,
+before this is committed?  We use `completeUpdate`, and then update again on
+the branch.  `completeUpdate` moves all selected changes to be `local`,
+whatever the source, the same way commit does (in fact, commit uses
+completeUpdate).
+
+    >>> bi2 = Inventory(Manifest(branch.manifest))
+    >>> bi2.contents['edna'] = app['e2'] = Demo()
+    >>> branch.commit(bi2)
+    >>> checkManifest(branch.manifest)
+    >>> branch.commit(bi) # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    OutOfDateError: <zc.vault.core.Manifest object at ...>
+    >>> bi.completeUpdate()
+    >>> bi.beginUpdate()
+    >>> branch.commit(bi)
+    >>> checkManifest(branch.manifest)
+
+Once we have done this, the head of the branch is based on the head of the
+original vault, so we can immediately check in a branch inventory in the
+trunk inventory.
+
+    >>> v.commit(Inventory(Manifest(branch.manifest)))
+    >>> checkManifest(v.manifest)
+
+Finally, cherry-picking changes is possible as well, though it can
+cause normal updates to be confused.  `beginCollectionUpdate` takes an
+iterable of items (such as is produced by iterChangedItems) and applies
+the update with the usual conflict and examination approaches we've
+seen above.  `completeUpdate` can then accept the changes for
+additional updates.
+
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> discarded = Inventory(Manifest(v.manifest))
+    >>> discarded.contents['ignatius'] = app['i1'] = Demo()
+    >>> discarded.contents['jacobus'] = app['j1'] = Demo()
+    >>> long_running.beginCollectionUpdate((discarded.contents('ignatius'),))
+    >>> len(list(long_running.iterOrphanConflicts()))
+    1
+    >>> o = iter(long_running.iterOrphanConflicts()).next()
+    >>> o.selected
+    True
+    >>> o.name # None
+    >>> o.parent # None
+    >>> o.object
+    <Demo u'i1'>
+    >>> o.moveTo(long_running.contents, 'ignatius')
+    >>> len(list(long_running.iterOrphanConflicts()))
+    0
+    >>> long_running.contents['ignatius']
+    <Demo u'i1'>
+    >>> long_running.contents('ignatius')['jacobus'] = app['j1']
+    >>> list(long_running.contents('ignatius').keys())
+    ['jacobus']
+    >>> long_running.contents('ignatius')('jacobus').selected
+    True
+    >>> list(discarded.contents('ignatius').keys())
+    []
+    >>> v.commit(long_running)
+    >>> checkManifest(v.manifest)
+
+The code will stop you if you try to add a set of relationships that result in
+the manifest having keys that don't map to values--or more precisely, child
+tokens that don't have matching selected relationships.  For instance, consider
+this.
+
+    >>> long_running = Inventory(Manifest(v.manifest))
+    >>> discarded = Inventory(Manifest(v.manifest))
+    >>> discarded.contents['katrina'] = app['k1'] = Demo()
+    >>> discarded.contents('katrina')['loyola'] = app['l1'] = Demo()
+    >>> long_running.beginCollectionUpdate((discarded.contents('katrina'),))
+    ... # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ValueError: cannot update from a set that includes children tokens...
+
+It is disallowed because the katrina node includes the 'loyola' node, but we
+didn't include the matching 'loyola' item.
+
+If you include both, the merge will proceed as usual.
+
+    >>> long_running.beginCollectionUpdate(
+    ...     (discarded.contents('katrina'),
+    ...      discarded.contents('katrina')('loyola')))
+    >>> long_running.updating
+    True
+    >>> len(list((long_running.iterOrphanConflicts())))
+    2
+    >>> orphans = dict((o.name, o) for o in long_running.iterOrphanConflicts())
+    >>> orphans[None].moveTo(long_running.contents, 'katrina')
+    >>> long_running.contents['katrina']
+    <Demo u'k1'>
+    >>> long_running.contents('katrina')['loyola']
+    <Demo u'l1'>
+
+The combination of `beginCollectionUpdate` and `iterChangedItems` can provide
+a powerful way to apply arbitrary changesets to a revision.
+
+Storing None
+============
+
+Sometimes you want to just make an empty node for organizational purposes.
+While normally stored objects must be versionable and adaptable to
+IKeyReference, None is a special case.  We can store None in any node.  Let's
+make a quick example.
+
+    >>> v = app['v'] = Vault()
+    >>> i = Inventory(vault=v)
+    >>> i.contents['foo'] = None
+    >>> i.contents('foo')['bar'] = None
+    >>> i.contents('foo')('bar')['baz'] = app['d1']
+    >>> i.contents['foo'] # None
+    >>> i.contents('foo')['bar'] # None
+    >>> i.contents('foo')('bar')['baz'] is app['d1']
+    True
+    >>> i.contents['bing'] = app['a1']
+    >>> i.contents['bing'] is app['a1']
+    True
+    >>> v.commit(i)
+    >>> i = Inventory(vault=v, mutable=True)
+    >>> i.contents['bing'] = None
+    >>> del i.contents('foo')['bar']
+    >>> i.contents['foo'] = app['d1']
+    >>> v.commit(i)
+    >>> v.inventory.contents.previous['bing'] is app['a1']
+    True
+    >>> v.inventory.contents.previous['foo'] is None
+    True
+
+Special "held" Containers
+=========================
+
+It is sometimes useful to specify a "held" container for all objects stored
+in a vault, overriding the "held" containers for each manifest as described
+above.  Vaults can be instantiated with specifying a held container.
+
+    >>> from zc.vault.core import HeldContainer
+    >>> held = app['held'] = HeldContainer()
+    >>> v = app['vault_held'] = Vault(held=held)
+    >>> i = Inventory(vault=v)
+    >>> o = i.contents['foo'] = Demo()
+    >>> o.__parent__ is held
+    True
+    >>> held[o.__name__] is o
+    True
+
+If you create a branch, by default it will use the same held container.
+
+    >>> v.commit(i)
+    >>> v2 = app['vault_held2'] = v.createBranch()
+    >>> i2 = Inventory(vault=v2, mutable=True)
+    >>> o2 = i2.contents['bar'] = Demo()
+    >>> o2.__parent__ is held
+    True
+    >>> held[o2.__name__] is o2
+    True
+
+You can also specify another held container when you create a branch.
+
+    >>> another_held = app['another_held'] = HeldContainer()
+    >>> v3 = app['vault_held3'] = v.createBranch(held=another_held)
+    >>> i3 = Inventory(vault=v3, mutable=True)
+    >>> o3 = i3.contents['baz'] = Demo()
+    >>> o3.__parent__ is another_held
+    True
+    >>> another_held[o3.__name__] is o3
+    True
+
+Committing the transaction
+==========================
+
+We'll make sure that all these changes can in fact be committed to the ZODB.
+
+    >>> import transaction
+    >>> transaction.commit()
+
+-----------
+
+...commit messages?  Could be added to event, so object log could use.
+
+Need commit datetime stamp, users.  Handled now by objectlog.
+
+Show traversal adapters that use zc.shortcut code...
+
+Talk about tokens.
+
+Then talk about use case of having a reference be updated to a given object
+within a vault...
+
+...a vault mirror that also keeps track of hierarchy?
+
+A special reference that knows both vault and token?
+


Property changes on: zc.vault/trunk/src/zc/vault/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.vault/trunk/src/zc/vault/__init__.py
===================================================================
--- zc.vault/trunk/src/zc/vault/__init__.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/__init__.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1 @@
+#

Added: zc.vault/trunk/src/zc/vault/catalog.py
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,221 @@
+from BTrees import IOBTree, IFBTree
+from zc.vault import interfaces, keyref
+from zope import component, interface
+import persistent
+import zope.app.container.interfaces
+import zope.app.intid.interfaces
+import zope.component.interfaces
+import zope.event
+import zope.lifecycleevent
+import zope.lifecycleevent.interfaces
+import zope.location
+
+# from a site, to get to the default package, the incantation is
+# site.getSiteManager()['default']
+
+def addLocalUtility(package, utility, interface=None,
+                    name='', name_in_container='', comment=u''):
+    chooser = zope.app.container.interfaces.INameChooser(package)
+    name_in_container = chooser.chooseName(name_in_container, utility)
+    zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(utility))
+    package[name_in_container] = utility
+    # really want IComponentRegistry, but that is not set up in Zope 3 ATM
+    registry = zope.component.interfaces.IComponentLookup(package)
+    registry.registerUtility(utility, interface, name, comment)
+
+HISTORICAL = 'zc.vault.historical'
+CURRENT = 'zc.vault.current'
+WORKING = 'zc.vault.working'
+# the following constant is not API
+REVERSE = 'zc.vault.working_reverse'
+
+class IRevisionReferences(interface.Interface):
+    historical = interface.Attribute(
+        '''an object with a standard mapping get method: stores
+        object intid -> set of historical manifest intids''')
+    current = interface.Attribute(
+        '''an object with a standard mapping get method: stores
+        object intid -> set of historical manifest intids''')
+    working = interface.Attribute(
+        '''an object with a standard mapping get method: stores
+        object intid -> set of historical manifest intids''')
+
+class RevisionReferencesMappings(persistent.Persistent):
+
+    def __init__(self):
+        self.references = IOBTree.IOBTree()
+
+    def _getrefs(self, key):
+        refs = self.references.get(key)
+        if refs is None:
+            refs = self.references[key] = IFBTree.IFTreeSet()
+        return refs
+
+    def add(self, key, value):
+        self._getrefs(key).insert(value)
+
+    def update(self, key, values):
+        self._getrefs(key).update(values)
+
+    def remove(self, key, value):
+        refs = self.references.get(key)
+        if refs is not None:
+            refs.remove(value) # raises KeyError when we desire
+            if not refs:
+                del self.references[key]
+        else:
+            raise KeyError("key and value pair does not exist")
+
+    def discard(self, key, value):
+        try:
+            self.remove(key, value)
+        except KeyError:
+            pass
+
+    def contains(self, key, value):
+        refs = self.references.get(key)
+        if refs is not None:
+            return value in refs
+        return False
+
+    def set(self, key, values):
+        refs = self.references.get(key)
+        vals = tuple(values)
+        if not vals:
+            if refs is not None:
+                # del
+                del self.references[key]
+        else:
+            if refs is None:
+                refs = self.references[key] = IFBTree.IFTreeSet()
+            else:
+                refs.clear()
+            refs.update(vals)
+
+    def get(self, key):
+        return self.references.get(key, ())
+
+class RevisionReferences(persistent.Persistent, zope.location.Location):
+
+    interface.implements(IRevisionReferences)
+
+    __parent__ = __name__ = None
+
+    def __init__(self):
+        self.historical = RevisionReferencesMappings()
+        self.current = RevisionReferencesMappings()
+        self.working = RevisionReferencesMappings()
+        self.reverse = RevisionReferencesMappings()
+
+def createRevisionReferences(package):
+    utility = RevisionReferences()
+    chooser = zope.app.container.interfaces.INameChooser(package)
+    name = chooser.chooseName('zc_vault_revision_references', utility)
+    zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(utility))
+    package[name] = utility
+    # really want IComponentRegistry, but that is not set up in Zope 3 ATM
+    registry = zope.component.interfaces.IComponentLookup(package)
+    registry.registerUtility(utility, IRevisionReferences, '', '')
+
+ at component.adapter(interfaces.IManifestCommitted)
+def makeReferences(ev):
+    refs = component.getUtility(IRevisionReferences)
+    intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+    man = ev.object
+    man_id = intids.register(man)
+    prev = man.previous
+    if prev is not None:
+        prev_id = intids.register(prev)
+        for rel in prev:
+            if rel.token is not man.vault.top_token:
+                o_id = intids.register(rel.object)
+                refs.current.discard(o_id, prev_id)
+                refs.historical.add(o_id, prev_id)
+    for o_id in refs.reverse.get(man_id):
+        refs.working.remove(o_id, man_id)
+    refs.reverse.set(man_id, ())
+    for rel in man:
+        if rel.token is not man.vault.top_token:
+            refs.current.add(intids.register(rel.object), man_id)
+
+ at component.adapter(interfaces.IUpdateCompleted)
+def updateCompleted(ev):
+    refs = component.getUtility(IRevisionReferences)
+    intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+    man = ev.object
+    man_id = intids.register(man)
+    for o_id in refs.reverse.get(man_id):
+        refs.working.remove(o_id, man_id)
+    refs.reverse.set(man_id, ())
+    for rel in man.iterSelections():
+        if rel.token is not man.vault.top_token:
+            o_id = intids.register(rel.object)
+            refs.working.add(o_id, man_id)
+            refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IUpdateAborted)
+def updateAborted(ev):
+    updateCompleted(ev)
+
+ at component.adapter(
+    interfaces.IManifest, zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def manifestCreated(man, ev):
+    refs = component.getUtility(IRevisionReferences)
+    intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+    man = ev.object
+    man_id = intids.register(man)
+    for rel in man:
+        if rel.token is not man.vault.top_token:
+            o_id = intids.register(rel.object)
+            refs.working.add(o_id, man_id)
+            refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IRelationshipSelected)
+def relationshipSelected(ev):
+    rel = ev.object
+    if rel.token is not rel.__parent__.vault.top_token:
+        man = ev.manifest
+        refs = component.getUtility(IRevisionReferences)
+        intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        man_id = intids.register(man)
+        o_id = intids.register(rel.object)
+        refs.working.add(o_id, man_id)
+        refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IRelationshipDeselected)
+def relationshipDeselected(ev):
+    rel = ev.object
+    if rel.token is not rel.__parent__.vault.top_token:
+        man = ev.manifest
+        for other in man:
+            if other.object is rel.object:
+                break
+        else:
+            refs = component.getUtility(IRevisionReferences)
+            intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+            man_id = intids.register(man)
+            o_id = intids.register(rel.object)
+            refs.working.remove(o_id, man_id)
+            refs.reverse.remove(man_id, o_id)
+
+ at component.adapter(interfaces.IObjectChanged)
+def objectChanged(ev):
+    rel = ev.object
+    if rel.token is not rel.__parent__.vault.top_token:
+        man = rel.__parent__
+        if man is not None and man.isSelected(rel):
+            refs = component.getUtility(IRevisionReferences)
+            intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+            man_id = intids.register(man)
+            o_id = intids.register(rel.object)
+            previous = ev.previous
+            if previous is not None:
+                for other in man:
+                    if other.object is previous:
+                        break
+                else:
+                    p_id = intids.register(previous)
+                    refs.working.remove(p_id, man_id)
+                    refs.reverse.remove(man_id, p_id)
+            refs.working.add(o_id, man_id)
+            refs.reverse.add(man_id, o_id)

Added: zc.vault/trunk/src/zc/vault/catalog.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.txt	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.txt	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,196 @@
+When wanting to index objects in a vault, we have identified at least three
+scenarios.
+
+- You may want to find unique objects irrespective of their effective locations
+  within a vault.
+
+- You may want to index the objects within a vault, tracking the most recent
+  manifest revision, treating identical objects in different vault locations as
+  separate searchable entities.
+
+- As a variation of the second story, you may want to search through all past
+  vault revisions, not just the most recent one.
+
+The way the catalog.py module approaches this is to provide three
+extrinsicreferences-like mapping objects to historical revisions, current
+revisions, and uncommitted versions, respectively, that use them (a fourth
+similar data structure is included for bookkeeping, but is not part of the
+advertised API).  Then it provides a number of subscribers to keep these
+data structures up-to-date.
+
+Let's run the code through its paces.  First we want to run the code that adds
+the references.  It is a function that expects a package.
+
+    >>> from zc.vault import catalog
+    >>> sm = app.getSiteManager()
+    >>> package = sm['default']
+    >>> catalog.createRevisionReferences(package)
+
+Now we should be able to get the utilities.  Note that the
+catalog module provides constants for the utility names.
+
+    >>> from zope import component
+    >>> refs = component.getUtility(catalog.IRevisionReferences)
+
+Now we can actually create a vault and watch the references change.
+
+    >>> from zc.vault.vault import Vault, Inventory
+    >>> v = Vault()
+    >>> app['vault'] = v
+    >>> i = Inventory(vault=v)
+    >>> import persistent
+    >>> from zope.app.container.contained import Contained
+    >>> from zc.copyversion.versioning import Versioning
+    >>> class Demo(persistent.Persistent, Contained, Versioning):
+    ...     def __repr__(self):
+    ...         return "<%s %r>" % (self.__class__.__name__, self.__name__)
+    ...
+    >>> i.contents[u'able'] = app['a1'] = Demo()
+
+Initially, an object is added to a working inventory (or manifest).
+The refs put a relationship in the working category.
+
+    >>> from zope.app.intid.interfaces import IIntIds
+    >>> intids = component.getUtility(IIntIds)
+    >>> working = refs.working.get(intids.getId(app['a1']))
+    >>> len(working)
+    1
+    >>> list(working) == [intids.getId(i.manifest)]
+    True
+    >>> len(refs.current.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.historical.get(intids.getId(app['a1'])))
+    0
+
+Once we commit the inventory, the relationship moves to the current category.
+
+    >>> v.commit(i)
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.historical.get(intids.getId(app['a1'])))
+    0
+    >>> current = refs.current.get(intids.getId(app['a1']))
+    >>> len(current)
+    1
+    >>> list(current) == [intids.getId(i.manifest)]
+    True
+
+If we create a new inventory, the object is in both working and current.
+
+    >>> i = Inventory(vault=v, mutable=True)
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    1
+    >>> working = refs.working.get(intids.getId(app['a1']))
+    >>> len(working)
+    1
+    >>> list(working) == [intids.getId(i.manifest)]
+    True
+    >>> current = refs.current.get(intids.getId(app['a1']))
+    >>> len(current)
+    1
+    >>> list(current) == [intids.getId(v.manifest)]
+    True
+    >>> len(refs.historical.get(intids.getId(app['a1'])))
+    0
+
+Commit that one, and we have one in historical and one in current.
+
+    >>> i.contents[u'disiri'] = app['d1'] = Demo() # so there's a change
+    >>> v.commit(i)
+    >>> historical = refs.historical.get(intids.getId(app['a1']))
+    >>> len(historical)
+    1
+    >>> list(historical) == [intids.getId(v[0])]
+    True
+    >>> current = refs.current.get(intids.getId(app['a1']))
+    >>> len(current)
+    1
+    >>> list(current) == [intids.getId(v.manifest)]
+    True
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+
+We'll make another two inventories.
+
+    >>> i = Inventory(vault=v, mutable=True)
+    >>> i2 = Inventory(vault=v, mutable=True)
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    2
+
+Now one of them will replace 'a1' ('able') with another object.
+
+    >>> i.contents['able'] = app['a2'] = Demo()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    1
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    1
+
+Now we'll commit the first inventory.
+
+    >>> v.commit(i)
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    1
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    0
+    >>> len(refs.current.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.current.get(intids.getId(app['a2'])))
+    1
+    >>> len(refs.historical.get(intids.getId(app['a1'])))
+    2
+
+If we update i2 (do a merge with the checked-in version) then the references
+correctly change.
+
+    >>> i2.beginUpdate()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    1
+
+If we select a different relationship then the references also correctly
+change.
+
+    >>> i2.contents('able').base_item.select()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    1
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    0
+    >>> i2.contents('able').updated_item.select()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    1
+
+If we abort then the references correctly change.
+
+    >>> i2.abortUpdate()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    1
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    0
+    >>> i2.beginUpdate()
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    1
+
+Now we'll commit i2 and once again check the references.
+
+    >>> i2.contents[u'toug'] = app['t1'] = Demo() # so there's a change
+    >>> v.commit(i2)
+    >>> len(refs.working.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.working.get(intids.getId(app['a2'])))
+    0
+    >>> len(refs.current.get(intids.getId(app['a1'])))
+    0
+    >>> len(refs.current.get(intids.getId(app['a2'])))
+    1
+    >>> len(refs.historical.get(intids.getId(app['a1'])))
+    2
+    >>> len(refs.historical.get(intids.getId(app['a2'])))
+    1
+
+The end.  Further tests would make sure that selected orphans were
+maintained in working copies.


Property changes on: zc.vault/trunk/src/zc/vault/catalog.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.vault/trunk/src/zc/vault/catalog.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.zcml	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.zcml	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="zc.vault">
+
+  <subscriber handler=".catalog.makeReferences" />
+
+  <subscriber handler=".catalog.updateCompleted" />
+
+  <subscriber handler=".catalog.updateAborted" />
+
+  <subscriber handler=".catalog.manifestCreated" />
+
+  <subscriber handler=".catalog.relationshipSelected" />
+
+  <subscriber handler=".catalog.relationshipDeselected" />
+
+  <subscriber handler=".catalog.objectChanged" />
+
+</configure>

Added: zc.vault/trunk/src/zc/vault/configure.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/configure.zcml	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/configure.zcml	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="zc.task">
+
+  <class class=".core.Mapping">
+    <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+    <require permission="zope.View"
+        interface="zope.interface.common.mapping.IEnumerableMapping" />
+    <require permission="zope.View"
+        attributes="getKey __eq__ __ne__" />
+    <require permission="zope.ManageContent"
+        interface="zope.interface.common.mapping.IWriteMapping" />
+    <require permission="zope.ManageContent"
+        attributes="updateOrder updateOrderFromTokens" />
+  </class>
+
+  <class class=".keyref.Token">
+    <allow interface="zope.app.keyreference.interfaces.IKeyReference"
+        attributes="identifiers" />
+  </class>
+
+  <class class=".keyref._top_token_">
+    <require like_class=".keyref.Token" />
+  </class>
+
+  <class class=".core.Relationship">
+    <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+    <require like_class=".keyref.Token" />
+    <require permission="zope.View"
+        attributes="token object containment children copy_source" />
+    <require permission="zope.ManageContent"
+        set_attributes="object" />
+  </class>
+
+  <class class=".core.HeldContainer">
+    <require permission="zope.View"
+        interface="zope.app.container.interfaces.IReadContainer" />
+    <require permission="zope.ManageContent"
+        interface="zope.app.container.interfaces.IWriteContainer" />
+  </class>
+
+  <class class=".core.Manifest">
+    <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+    <require permission="zope.View"
+        attributes="held vault_index getBaseSources getBaseSource base_source
+                    vault merged_sources update_source update_base updating
+                    iterChanges get getType isSelected getBase getLocal
+                    getUpdated iterSuggested iterModified iterMerged
+                    iterSelectedParents iterParents isLinked
+                    iterUpdateConflicts iterUpdateResolutions isUpdateConflict
+                    iterOrphanConflicts iterOrphanResolutions isOrphan
+                    isOrphanConflict iterParentConflicts iterAll __iter__
+                    iterUnchangedOrphans previous next isOption" />
+    <require permission="zope.ManageContent"
+        attributes="addLocal addModified beginUpdate beginCollectionUpdate
+                    completeUpdate approveRelationshipChange abortUpdate select
+                    resolveUpdateConflict resolveOrphanConflict
+                    undoOrphanConflictResolution" />
+    <!-- addSuggested and reindex should be forbidden or another permission;
+         currently forbidden -->
+    <require permission="zope.ManageContent"
+        set_attributes="vault_index vault" />
+  </class>
+
+  <class class=".core.Vault">
+    <require permission="zope.View"
+        attributes="intids getPrevious getNext __len__ __getitem__ __iter__
+                    manifest" />
+    <require permission="zope.ManageContent"
+        attributes="commit commitOverride" />
+  </class>
+
+  <class class=".vault.InventoryContents">
+    <require permission="zope.View"
+        interface="zope.interface.common.mapping.IEnumerableMapping" />
+    <require permission="zope.View"
+        attributes="getKey __eq__ __ne__ __call__ previous next
+                    previous_version next_version type selected
+                    is_update_conflict has_base has_local has_updated
+                    has_suggested has_modified has_merged base_item local_item
+                    updated_item iterSuggestedItems iterModifiedItems
+                    iterMergedItems copy_source selected_item
+                    relationship inventory" />
+    <require permission="zope.ManageContent"
+        interface="zope.interface.common.mapping.IWriteMapping" />
+    <require permission="zope.ManageContent"
+        attributes="updateOrder updateOrderFromTokens makeMutable select
+                    resolveUpdateConflict" />
+  </class>
+
+  <class class=".vault.InventoryItem">
+    <require like_class=".vault.InventoryContents" />
+    <require permission="zope.View"
+        attributes="is_orphan is_orphan_conflict is_parent_conflict parent
+                    name iterSelectedParents iterParents object" />
+    <require permission="zope.ManageContent"
+        attributes="resolveOrphanConflict moveTo copyTo"
+        set_attributes="object" />
+   </class>
+
+  <class class=".vault.Inventory">
+    <require permission="zope.View"
+        attributes="__eq__ __ne__ vault contents manifest iterUpdateConflicts
+                    iterUpdateResolutions iterOrphanConflicts
+                    iterOrphanResolutions iterUnchangedOrphans
+                    iterParentConflicts __iter__ updating merged_sources
+                    update_source iterChangedItems getItemFromToken previous
+                    next" />
+    <require permission="zope.ManageContent"
+        attributes="beginUpdate completeUpdate abortUpdate
+                    beginCollectionUpdate"
+        set_attributes="vault" />
+   </class>
+
+  <class class=".vault.Vault">
+    <require like_class=".core.Vault" />
+    <require permission="zope.View"
+        attributes="getInventory inventory" />
+  </class>
+
+</configure>

Added: zc.vault/trunk/src/zc/vault/core.py
===================================================================
--- zc.vault/trunk/src/zc/vault/core.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/core.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,1318 @@
+
+from ZODB.interfaces import IConnection
+from BTrees import IOBTree, OIBTree, OOBTree, IFBTree, Length
+import persistent
+from zope import interface, component, event
+import zope.lifecycleevent
+import zope.location
+import zope.location.interfaces
+import zope.app.container.contained
+from zope.app.container.interfaces import INameChooser
+import zope.app.intid
+import zope.app.intid.interfaces
+from zc.relationship import index
+from zc.copyversion import versioning
+import zc.copyversion.interfaces
+import zope.app.keyreference.interfaces
+# import rwproperty
+from zc.copyversion import rwproperty
+
+from zc.vault import interfaces, keyref
+
+# vault -> relationships -> relationship -> mapping
+#                        -> held
+
+# XXX Missing relationships (child token without matching selected
+# relationship) is handled but not very gracefully.  Especially a problem with
+# beginCollectionUpdate.  Other situations are addressed in reindex.
+
+class approvalmethod(object):
+
+    def __init__(self, reindex=False):
+        self.reindex = reindex
+
+    def __call__(self, func):
+        self.func = func
+        return self.wrapper
+
+    def wrapper(self, wself, *args, **kwargs):
+        manifest = None
+        rel = wself.__parent__
+        if rel is not None:
+            manifest = rel.__parent__
+            if manifest is not None:
+                if rel.token is None:
+                    raise ValueError(
+                        'cannot change containment without token on '
+                        'relationship')
+                manifest.checkRelationshipChange(rel)
+        self.func(wself, *args, **kwargs)
+        if self.reindex and manifest is not None:
+            manifest.reindex(rel)
+
+class Mapping(persistent.Persistent, versioning.Versioning):
+
+    interface.implements(interfaces.IRelationshipContainment)
+
+    __parent__ = None
+
+    def __init__(self, data=None):
+        self._forward = OIBTree.OIBTree()
+        self._reverse = IOBTree.IOBTree()
+        self._length = Length.Length()
+        self._order = ()
+        if data is not None:
+            self.update(data)
+
+    def __len__(self):
+        return self._length.value
+
+    def __getitem__(self, key):
+        return self._forward[key]
+
+    def get(self, key, default=None):
+        return self._forward.get(key, default)
+
+    def items(self):
+        return [(k, self._forward[k]) for k in self._order]
+
+    def keys(self):
+        return self._order
+
+    def values(self):
+        return [self._forward[k] for k in self._order]
+
+    @property
+    def valuesBTree(self): # for optimization
+        return self._reverse
+
+    @property
+    def keysBTree(self): # for symmetry :-)
+        return self._forward
+
+    def __iter__(self):
+        return iter(self._order)
+
+    def __contains__(self, key):
+        return key in self._forward
+
+    has_key = __contains__
+
+    def getKey(self, value, default=None):
+        if not isinstance(value, int):
+            return default
+        return self._reverse.get(value, default)
+
+    @versioning.method
+    @approvalmethod(reindex=True)
+    def __delitem__(self, key):
+        self._delitem(key)
+
+    def _delitem(self, key):
+        old = self._forward.pop(key)
+        order = list(self._order)
+        order.remove(key)
+        self._order = tuple(order)
+        self._reverse.pop(old)
+        self._length.change(-1)
+        event.notify(interfaces.ObjectRemoved(old, self, key))
+
+    @versioning.method
+    @approvalmethod(reindex=True)
+    def __setitem__(self, key, value):
+        self._setitem(key, value)
+
+    def _setitem(self, key, value):
+        bad = False
+        if isinstance(key, basestring):
+            try:
+                unicode(key)
+            except UnicodeError:
+                bad = True
+        else:
+            bad = True
+        if bad: 
+            raise TypeError("'%s' is invalid, the key must be an "
+                            "ascii or unicode string" % key)
+        if len(key) == 0:
+            raise ValueError("The key cannot be an empty string")
+        if not isinstance(value, int):
+            raise ValueError("The value must be an integer")
+        old_key = self._reverse.get(value)
+        if old_key is not None:
+            if old_key != key:
+                raise ValueError(
+                    'name mapping can only contain unique values')
+            # else noop
+        else:
+            old_value = self._forward.get(key)
+            if old_value is None:
+                self._order += (key,)
+                self._length.change(1)
+            else:
+                old_old_key = self._reverse.pop(old_value)
+                assert old_old_key == key
+            self._forward[key] = value
+            self._reverse[value] = key
+            if old_value is not None:
+                event.notify(interfaces.ObjectRemoved(old_value, self, key))
+            event.notify(interfaces.ObjectAdded(value, self, key))
+
+    @versioning.method
+    @approvalmethod(reindex=True)
+    def update(self, data):
+        if getattr(data, 'keys', None) is not None:
+            data = [(k, data[k]) for k in data.keys()]
+        if len(self):
+            # since duplication of values is disallowed, we need to remove
+            # any current overlapped values so we don't get spurious errors.
+            keys = set()
+            probs = []
+            for k, v in data:
+                keys.add(k)
+                old_k = self._reverse.get(v)
+                if old_k is not None and old_k != k:
+                    probs.append((old_k, v))
+            for k, v in probs:
+                if k not in keys:
+                    raise ValueError(
+                        'name mapping can only contain unique values', v)
+            for k, v in probs:
+                self._delitem[k]
+        for k, v in data:
+            self._setitem(k, v)
+
+    @versioning.method
+    @approvalmethod(reindex=False)
+    def updateOrder(self, order):
+        order = tuple(order)
+        if self._order != order:
+            if len(order) != len(self):
+                raise ValueError('Incompatible key set.')
+            for k in order:
+                if k not in self._forward:
+                    raise ValueError('Incompatible key set.')
+            old_order = self._order
+            self._order = order
+            event.notify(interfaces.OrderChanged(self, old_order))
+
+    def __eq__(self, other):
+        return self is other or (
+            (interfaces.IBidirectionalNameMapping.providedBy(other) and
+             self.keys() == other.keys() and self.values() == other.values()))
+
+    def __ne__(self, other):
+        return not (self == other)
+
+class Relationship(keyref.Token, versioning.Versioning):
+    interface.implements(interfaces.IRelationship)
+
+    _token = _copy_source = None
+
+    def __init__(self, token=None, object=None, containment=None,
+                 relationship=None, source_manifest=None):
+        if source_manifest is not None:
+            if relationship is None:
+                raise ValueError(
+                    'source_inventory must be accompanied with relationship')
+        if relationship is not None:
+            if (source_manifest is None and
+                relationship.__parent__ is not None):
+                tmp = getattr(
+                    relationship.__parent__, '__parent__', None)
+                if interfaces.IManifest.providedBy(tmp):
+                    source_manifest = tmp
+            if source_manifest is not None:
+                self._copy_source = (relationship, source_manifest)
+            if object is not None or containment is not None:
+                raise ValueError(
+                    'cannot specify relationship with object or containment')
+            object = relationship.object
+            containment = relationship.containment
+        if token is not None:
+            if not isinstance(token, int):
+                raise ValueError('token must be int')
+            self._token = token
+        self._object = object
+        self._containment = Mapping(containment)
+        self._containment.__parent__ = self
+
+    @property
+    def copy_source(self): # None or tuple of (relationship, inventory)
+        return self._copy_source
+
+    @versioning.method
+    def _z_version(self):
+        if self.token is None:
+            raise zc.copyversion.interfaces.VersioningError(
+                'Cannot version without a token')
+        vault = self.__parent__.vault
+        if not self.containment._z_versioned:
+            prev = vault.getPrevious(self)
+            if prev is not None:
+                if prev.containment == self.containment:
+                    assert prev.containment._z_versioned
+                    self._containment = prev.containment
+            if not self._containment._z_versioned:
+                self.containment._z_version()
+        if self._object is not None:
+            obj_v = zc.copyversion.interfaces.IVersioning(self.object)
+            if not obj_v._z_versioned:
+                factory = interfaces.IVersionFactory(vault, None)
+                if factory is not None:
+                    res = factory(self.object, self.__parent__)
+                    if res is not self.object:
+                        self.object = res
+                        obj_v = zc.copyversion.interfaces.IVersioning(res)
+                if not obj_v._z_versioned:
+                    obj_v._z_version()
+        super(Relationship, self)._z_version()
+
+    @property
+    def token(self):
+        return self._token
+    @rwproperty.setproperty
+    def token(self, value):
+        if self._token is None:
+            self._token = value
+        elif not isinstance(value, int):
+            raise ValueError('token must be int')
+        else:
+            self._token = value
+
+    @property
+    def object(self):
+        return self._object
+    @versioning.setproperty
+    def object(self, value):
+        if self.token is None and self.__parent__ is not None:
+            raise ValueError('cannot change object without token')
+        if self.token == self.__parent__.vault.top_token:
+            raise ValueError('cannot set object of top token')
+        if (value is not None and
+            not zc.copyversion.interfaces.IVersionable.providedBy(value)):
+            raise ValueError(
+                'can only place versionable objects in vault, or None')
+        if self.__parent__ is not None:
+            self.__parent__.checkRelationshipChange(self)
+        if value is not self._object:
+            old = self._object
+            self._object = value
+            if (self.__parent__ is not None and
+                self.__parent__.getType(self) is not None):
+                self.__parent__.reindex(self)
+                event.notify(interfaces.ObjectChanged(self, old))
+
+    @property
+    def containment(self):
+        return self._containment
+
+    @property
+    def children(self):
+        return self.containment.valuesBTree
+
+def localDump(obj, index, cache):
+    # NOTE: a reference to this function is persisted!
+    return index.__parent__.vault.intids.register(obj)
+
+def localLoad(token, index, cache):
+    # NOTE: a reference to this function is persisted!
+    return index.__parent__.vault.intids.getObject(token)
+
+class Manifest(persistent.Persistent, versioning.Versioning,
+               zope.app.container.contained.Contained):
+
+    interface.implements(interfaces.IManifest)
+
+    _updateSource = _updateBase = None
+
+    def __init__(self, base=None, vault=None):
+        if vault is None:
+            if base is None:
+                raise ValueError('must provide base or vault')
+            vault = base.vault
+        elif base is not None:
+            if base.vault is not vault and base.getBaseSource(vault) is None:
+                raise ValueError(
+                    "explicitly passed vault must have a base in base_source.")
+        else: # vault but not base
+            base = vault.manifest
+        if base is not None and not base._z_versioned:
+            raise ValueError('base must be versioned')
+        self.__parent__ = self._vault = vault
+        self._index = index.Index(
+            ({'element':interfaces.IRelationship['token'],
+              'dump': None, 'load': None, 'btree': IOBTree},
+             interfaces.IRelationship['object'],
+             {'element':interfaces.IRelationship['children'],
+              'multiple': True, 'dump': None, 'load': None,
+              'name': 'child', 'btree': IOBTree}),
+            index.TransposingTransitiveQueriesFactory('token', 'child'),
+            localDump, localLoad)
+        self._index.__parent__ = self
+        self._selections = IFBTree.IFTreeSet()
+        self._oldSelections = IFBTree.IFTreeSet()
+        self._conflicts = IFBTree.IFTreeSet()
+        self._resolutions = IFBTree.IFTreeSet()
+        self._orphanResolutions = IFBTree.IFTreeSet()
+        self._oldOrphanResolutions = IFBTree.IFTreeSet()
+        self._updated = IFBTree.IFTreeSet()
+        self._local = IFBTree.IFTreeSet()
+        self._suggested = IFBTree.IFTreeSet()
+        self._modified = IFBTree.IFTreeSet()
+        self._bases = IOBTree.IOBTree()
+        if base:
+            self._indexBases(base.getBaseSources(), base, True)
+        if vault.held is None:
+            self._held = HeldContainer()
+            zope.location.locate(self._held, self, "held")
+        else:
+            self._held = vault.held
+
+    @property
+    def held(self):
+        return self._held
+
+    def _indexBases(self, bases, base=None, select=False):
+        intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        bases = dict((intids.register(b.vault), b) for b in bases)
+        if base is not None:
+            bid = intids.register(base.vault)
+            bases[bid] = base
+        else:
+            bid = None
+        assert not self._bases, (
+            'programmer error: _indexBases should not be called with '
+            'populated _bases')
+        for iid, b in bases.items():
+            select_this = select and iid==bid
+            base_set = IFBTree.IFTreeSet()
+            data = (base_set, b)
+            register = self.vault.intids.register
+            for rel in b:
+                rid = register(rel)
+                base_set.insert(rid)
+                if select_this:
+                    self._selections.insert(rid)
+                    event.notify(interfaces.RelationshipSelected(rel, self))
+                self._index.index_doc(rid, rel)
+            self._bases[iid] = data
+
+    versioning.makeProperty('vault_index')
+
+    def getBaseSources(self):
+        return tuple(data[1] for data in self._bases.values())
+
+    def getBaseSource(self, vault):
+        intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        iid = intids.queryId(vault)
+        if iid is not None:
+            data = self._bases.get(iid)
+            if data is not None:
+                return data[1]
+
+    @property
+    def base_source(self):
+        return self.getBaseSource(self.vault)
+
+    _vault = None
+    @property
+    def vault(self):
+        return self._vault
+    @versioning.setproperty
+    def vault(self, value):
+        if self.updating:
+            raise interfaces.UpdateError('Cannot change vault while updating')
+        if value is not self._vault:
+            old = self._vault
+            s = set(old.intids.getObject(t) for t in self._selections)
+            bases = tuple(self.getBaseSources())
+            self._selections.clear()
+            l = set(old.intids.getObject(t) for t in self._local)
+            self._local.clear()
+            self._index.clear()
+            self._bases.clear()
+            self._vault = value
+            self._indexBases(bases)
+            for r in l:
+                self._add(r, self._local, True)
+            self._selections.update(value.intids.register(r) for r in s)
+            event.notify(interfaces.VaultChanged(self, old))
+
+    @property
+    def merged_sources(self):
+        v = self.vault
+        return tuple(b for b in self.getBaseSources() if b.vault is not v)
+
+    @property
+    def update_source(self):
+        return self._updateSource
+
+    @property
+    def update_base(self):
+        return self._updateBase
+
+    @property
+    def updating(self):
+        return self._updateSource is not None
+
+    @versioning.method
+    def _z_version(self):
+        if self.updating:
+            raise zc.copyversion.interfaces.VersioningError(
+                'cannot version during update')
+        if (list(self.iterParentConflicts()) or
+            list(self.iterOrphanConflicts())):
+            raise zc.copyversion.interfaces.VersioningError(
+                'cannot version with conflicts')
+        selections = set(self._iterLinked())
+        b = base = self.base_source
+        for r in list(self._local):
+            if r not in selections:
+                self._local.remove(r)
+                self._index.unindex_doc(r)
+            else:
+                rel = self.vault.intids.getObject(r)
+                if base is not None:
+                    b = base.get(rel.token)
+                if (b is None or
+                    b.object is not rel.object or
+                    b.containment != rel.containment):
+                    if not rel._z_versioned:
+                        rel._z_version()
+                else:
+                    selections.remove(r)
+                    self._local.remove(r)
+                    selections.add(self.vault.intids.getId(b))
+                    self._index.unindex_doc(r)
+        self._selections.clear()
+        self._selections.update(selections)
+        super(Manifest, self)._z_version()
+
+    def _locateObject(self, relationship, force=False):
+        if not force:
+            for child in relationship.children:
+                if self.get(child) is None:
+                    raise ValueError(
+                        'child tokens must have selected relationships')
+        if relationship.token == self.vault.top_token:
+            assert relationship.object is None
+            return
+        obj = relationship.object
+        if obj is not None and getattr(obj, '__parent__', None) is None:
+            if zope.location.interfaces.ILocation.providedBy(obj):
+                dest = self.held
+                dest[INameChooser(dest).chooseName('', obj)] = obj
+            else:
+                obj.__parent__ = self.vault
+
+    def _add(self, relationship, set, force=False):
+        self._locateObject(relationship, force)
+        if relationship.__parent__ is not self:
+            if relationship.__parent__ is None:
+                relationship.__parent__ = self
+            else:
+                raise ValueError(
+                    'cannot add relationship already in another set')
+        iid = self.vault.intids.register(relationship)
+        set.insert(iid)
+        self._index.index_doc(iid, relationship)
+
+    @versioning.method
+    def addLocal(self, relationship):
+        if self.updating:
+            raise interfaces.UpdateError(
+                'cannot add local relationships during update')
+        if self.getLocal(relationship.token) is not None:
+            raise ValueError(
+                'cannot add a second local relationship for the same token')
+        self._add(relationship, self._local)
+        event.notify(interfaces.LocalRelationshipAdded(relationship))
+        if len(self._index.findRelationshipTokenSet(
+            self._index.tokenizeQuery({'token': relationship.token}))) == 1:
+            self.select(relationship)
+
+    @versioning.method
+    def addModified(self, relationship):
+        if not self.updating:
+            raise interfaces.UpdateError(
+                'can only add modified relationships during update')
+        self._add(relationship, self._modified)
+        event.notify(interfaces.ModifiedRelationshipAdded(relationship))
+        if len(self._index.findRelationshipTokenSet(
+            self._index.tokenizeQuery({'token': relationship.token}))) == 1:
+            self.select(relationship)
+
+    @versioning.method
+    def addSuggested(self, relationship):
+        if not self.updating:
+            raise interfaces.UpdateError(
+                'can only add suggested relationships during update')
+        if len(self._index.findRelationshipTokenSet(
+               {'token': relationship.token})) == 0:
+            raise ValueError('cannot add suggested relationship for new token')
+        self._add(relationship, self._suggested)
+        event.notify(interfaces.SuggestedRelationshipAdded(relationship))
+
+    @versioning.method
+    def beginUpdate(self, source=None, base=None):
+        if self.updating:
+            raise interfaces.UpdateError(
+                'cannot begin another update while updating')
+        if source is None:
+            source = self.vault.manifest
+        if not interfaces.IManifest.providedBy(source):
+            raise ValueError('source must be manifest')
+        if source.vault.intids is not self.vault.intids:
+            raise ValueError('source must share intids')
+        if base is None:
+            if self.base_source is None or source.vault != self.vault:
+                myBase = self.getBaseSource(source.vault)
+                otherBase = source.getBaseSource(self.vault)
+                if myBase is None:
+                    if otherBase is None:
+                        # argh.  Need to walk over all bases and find any
+                        # shared ones.  Then pick the most recent one.
+                        for b in self.getBaseSources():
+                            if b.vault == self.vault:
+                                continue
+                            o = source.getBaseSource(b.vault)
+                            if o is not None:
+                                # we found one!
+                                if (o._z_version_timestamp >
+                                    b._z_version_timestamp):
+                                    b = o
+                                if base is None or (
+                                    b._z_version_timestamp >
+                                    base._z_version_timestamp):
+                                    base = b
+                        if base is None:
+                            raise ValueError('no shared previous manifest')
+                    else:
+                        base = otherBase
+                elif (otherBase is None or
+                      otherBase._z_version_timestamp <=
+                      myBase._z_version_timestamp):
+                    base = myBase
+                else:
+                    base = otherBase
+            else:
+                base = self.base_source
+
+        if base is source:
+            raise ValueError('base is source')
+        elif base._z_version_timestamp > source._z_version_timestamp:
+            raise NotImplementedError(
+                "don't know how to merge to older source")
+        if not interfaces.IManifest.providedBy(base):
+            raise ValueError('base must be manifest')
+        if not source._z_versioned or not base._z_versioned:
+            raise ValueError('manifests must be versioned')
+        intids = self.vault.intids
+        self._oldSelections.update(self._selections)
+        self._oldOrphanResolutions.update(self._orphanResolutions)
+        self._updateSource = source
+        self._updateBase = base
+        to_be_resolved = []
+        for s in source:
+            b = base.get(s.token)
+            source_changed = (b is None or s.object is not b.object or 
+                              s.containment != b.containment)
+            l = self.get(s.token)
+            if l is None: # even if base is non-None, that change is elsewhere
+                local_changed = False
+            elif b is None:
+                local_changed = True
+            else:
+                local_changed = l is not b and (
+                    l.object is not b.object or l.containment != b.containment)
+            if source_changed:
+                iid = intids.register(s)
+                self._updated.insert(iid)
+                self._index.index_doc(iid, s)
+                if local_changed:
+                    self._conflicts.insert(s.token)
+                    if l is not s and (l.object is not s.object or
+                                       l.containment != s.containment):
+                        # material difference.  Give resolver a chance.
+                        to_be_resolved.append((l, s, b))
+                    else:
+                        # we'll use the merged version by default
+                        self.select(s)
+                        self._resolutions.insert(s.token)
+                else:
+                    self.select(s)
+        if to_be_resolved:
+            resolver = interfaces.IConflictResolver(self.vault, None)
+            if resolver is not None:
+                for l, s, b in to_be_resolved:
+                    resolver(self, l, s, b)
+        event.notify(interfaces.UpdateBegun(self, source, base))
+
+    @versioning.method
+    def beginCollectionUpdate(self, source):
+        if self.updating:
+            raise interfaces.UpdateError(
+                'cannot begin another update while updating')
+        source = set(source)
+        token_to_source = dict((r.token, r) for r in source)
+        if len(token_to_source) < len(source):
+            raise ValueError(
+                'cannot provide more than one update relationship for the '
+                'same source')
+        for rel in source:
+            if rel.__parent__.vault.intids is not self.vault.intids:
+                raise ValueError('sources must share intids')
+            for child in rel.children:
+                if (token_to_source.get(child) is None and 
+                    self.get(child) is None):
+                    raise ValueError(
+                        'cannot update from a set that includes children '
+                        'tokens without matching relationships in which the '
+                        'child is the token')
+        intids = self.vault.intids
+        self._oldSelections.update(self._selections)
+        self._oldOrphanResolutions.update(self._orphanResolutions)
+        tmp_source = set()
+        for rel in source:
+            if not rel._z_versioned:
+                if rel.__parent__ is not None and rel.__parent__ is not self:
+                    rel = Relationship(rel.token, relationship=rel)
+                    rel.__parent__ = self
+                    event.notify(
+                        zope.lifecycleevent.ObjectCreatedEvent(rel))
+                self._add(rel, self._updated, force=True)
+            else:
+                iid = intids.register(rel)
+                self._updated.insert(iid)
+                self._locateObject(rel, force=True)
+                self._index.index_doc(iid, rel)
+            tmp_source.add(rel)
+            local = self.getLocal(rel.token)
+            if local is not None:
+                self._conflicts.insert(rel.token)
+                if (local.object is rel.object and 
+                      local.containment == rel.containment):
+                    self._resolutions.insert(rel.token)
+                else:
+                    resolver = component.queryMultiAdapter(
+                        (local, rel, None), interfaces.IConflictResolver)
+                    if resolver is not None:
+                        resolver(self)
+            else:
+                self.select(rel)
+        self._updateSource = frozenset(tmp_source)
+        assert not self._getChildErrors()
+        event.notify(interfaces.UpdateBegun(self, source, None))
+
+    def _selectionsFilter(self, relchain, query, index, cache):
+        return relchain[-1] in self._selections
+
+    def _iterLinked(self):
+        for p in self._index.findRelationshipTokenChains(
+            {'token': self.vault.top_token}, filter=self._selectionsFilter):
+            yield p[-1]
+
+    def completeUpdate(self):
+        source = self._updateSource
+        if source is None:
+            raise interfaces.UpdateError('not updating')
+        if (list(self.iterUpdateConflicts()) or
+            list(self.iterParentConflicts()) or
+            list(self.iterOrphanConflicts())):
+            raise interfaces.UpdateError(
+                'cannot complete update with conflicts')
+        assert not self._getChildErrors(), 'children without relationships!'
+        manifest = interfaces.IManifest.providedBy(source)
+        # assemble the contents of what will be the new bases
+        intids = self.vault.intids
+        selected = set(self._iterLinked())
+        base = self._updateBase
+        self._updateSource = self._updateBase = None
+        self._selections.clear()
+        self._selections.update(selected)
+        self._local.clear()
+        self._index.clear()
+        self._updated.clear()
+        self._modified.clear()
+        self._suggested.clear()
+        self._conflicts.clear()
+        self._resolutions.clear()
+        self._orphanResolutions.clear()
+        self._oldOrphanResolutions.clear()
+        self._oldSelections.clear()
+        bases = self.getBaseSources()
+        self._bases.clear()
+        if manifest:
+            global_intids = component.getUtility(
+                zope.app.intid.interfaces.IIntIds)
+            bases = dict((global_intids.register(b.vault), b) for b in bases)
+            for b in source.getBaseSources():
+                iid = global_intids.register(b.vault)
+                o = bases.get(iid)
+                if o is None or o._z_version_timestamp < b._z_version_timestamp:
+                    bases[iid] = b
+            self._indexBases(bases.values(), source)
+            existing = IFBTree.multiunion(
+                [data[0] for data in self._bases.values()])
+            for i in selected:
+                orig = rel = intids.getObject(i)
+                if rel._z_versioned:
+                    create_local = False
+                    source_rel = source.get(rel.token)
+                    if source_rel is rel:
+                        create_local = True
+                    elif source_rel is not None:
+                        base_rel = base.get(rel.token)
+                        if (base_rel is None or
+                            source_rel._z_version_timestamp >
+                            base_rel._z_version_timestamp):
+                            create_local = True
+                    if create_local:
+                        rel = Relationship(
+                            rel.token, relationship=rel, source_manifest=self)
+                        rel.__parent__ = self
+                        event.notify(
+                            zope.lifecycleevent.ObjectCreatedEvent(rel))
+                    else:
+                        continue
+                self._add(rel, self._local, True)
+                event.notify(interfaces.LocalRelationshipAdded(rel))
+                if orig is not rel:
+                    self._selections.remove(i)
+                    self.select(rel)
+        else:
+            self._indexBases(bases)
+            existing = IFBTree.multiunion(
+                [data[0] for data in self._bases.values()])
+            for i in selected:
+                if i not in existing:
+                    rel = intids.getObject(i)
+                    if rel._z_versioned:
+                        rel = Relationship(
+                            rel.token, relationship=rel, source_manifest=self)
+                        rel.__parent__ = self
+                        event.notify(
+                            zope.lifecycleevent.ObjectCreatedEvent(rel))
+                    self._add(rel, self._local, True)
+                    event.notify(interfaces.LocalRelationshipAdded(rel))
+        assert not (list(self.iterUpdateConflicts()) or
+                    list(self.iterParentConflicts()) or
+                    list(self.iterOrphanConflicts()) or
+                    self._getChildErrors())
+        event.notify(interfaces.UpdateCompleted(self, source, base))
+
+    def checkRelationshipChange(self, relationship):
+        reltype = self.getType(relationship)
+        if self.updating and reltype == interfaces.LOCAL:
+            raise interfaces.UpdateError(
+                'cannot change local relationships while updating')
+        if reltype in (interfaces.SUGGESTED, interfaces.UPDATED):
+            assert self.updating, (
+                'internal state error: the system should not allow suggested '
+                'or updated relationships when not updating')
+            raise TypeError(
+                'cannot change relationships when used as suggested or '
+                'updated values')
+
+    def abortUpdate(self):
+        if self._updateSource is None:
+            raise interfaces.UpdateError('not updating')
+        source = self._updateSource
+        base = self._updateBase
+        self._updateSource = self._updateBase = None
+        for s in (self._updated, self._modified, self._suggested):
+            for t in s:
+                self._index.unindex_doc(t)
+            s.clear()
+        self._conflicts.clear()
+        self._resolutions.clear()
+        self._orphanResolutions.clear()
+        self._orphanResolutions.update(self._oldOrphanResolutions)
+        self._oldOrphanResolutions.clear()
+        self._selections.clear()
+        self._selections.update(self._oldSelections)
+        self._oldSelections.clear()
+        event.notify(interfaces.UpdateAborted(self, source, base))
+
+    def iterChanges(self, base=None):
+        get = self.vault.intids.getObject
+        if base is None:
+            base = self.base_source
+        for t in self._selections:
+            rel = get(t)
+            if base is None:
+                yield rel
+            else:
+                b = base.get(rel.token)
+                if (b is None or
+                    b.object is not rel.object or
+                    b.containment != rel.containment):
+                    yield rel
+
+    @versioning.method
+    def reindex(self, relationship):
+        t = self.vault.intids.queryId(relationship)
+        if t is not None and (t in self._local or t in self._suggested or
+                              t in self._modified or t in self._updated):
+            self._locateObject(relationship)
+            self._index.index_doc(t, relationship)
+
+    def _getFromSet(self, token, set, default):
+        res = list(self._yieldFromSet(token, set))
+        if not res:
+            return default
+        assert len(res) == 1, 'internal error: too many in the same category'
+        return res[0]
+
+    def _yieldFromSet(self, token, set):
+        get = self.vault.intids.getObject
+        for t in self._index.findRelationshipTokenSet({'token': token}):
+            if t in set:
+                yield get(t)
+
+    def get(self, token, default=None):
+        # return the selected relationship
+        return self._getFromSet(token, self._selections, default)
+
+    def getType(self, relationship):
+        t = self.vault.intids.queryId(relationship)
+        if t is not None:
+            if t in self._local:
+                return interfaces.LOCAL
+            elif t in self._updated:
+                return interfaces.UPDATED
+            elif t in self._suggested:
+                return interfaces.SUGGESTED
+            elif t in self._modified:
+                return interfaces.MODIFIED
+            else:
+                intids = component.getUtility(
+                    zope.app.intid.interfaces.IIntIds)
+                iid = intids.queryId(relationship.__parent__.vault)
+                if iid is not None and iid in self._bases:
+                    iiset, rel_set = self._bases[iid]
+                    if t in iiset:
+                        return interfaces.BASE
+                    for bid, (iiset, rel_set) in self._bases.items():
+                        if bid == iid:
+                            continue
+                        if t in iiset:
+                            return interfaces.MERGED
+        return None
+
+    def isSelected(self, relationship):
+        t = self.vault.intids.queryId(relationship)
+        return t is not None and t in self._selections
+
+    @versioning.method
+    def select(self, relationship):
+        t = self.vault.intids.queryId(relationship)
+        if t is None or self.getType(relationship) is None:
+            raise ValueError('unknown relationship')
+        if t in self._selections:
+            return
+        rel_tokens = self._index.findRelationshipTokenSet(
+            self._index.tokenizeQuery({'token': relationship.token}))
+        for rel_t in rel_tokens:
+            if rel_t in self._selections:
+                self._selections.remove(rel_t)
+                event.notify(interfaces.RelationshipDeselected(
+                    self.vault.intids.getObject(rel_t), self))
+                break
+        self._selections.insert(t)
+        event.notify(interfaces.RelationshipSelected(relationship, self))
+
+    def getBase(self, token, default=None):
+        vault = self.base_source
+        for iiset, rel_set in self._bases.values():
+            if rel_set is vault:
+                return self._getFromSet(token, iiset, default)
+
+    def getLocal(self, token, default=None):
+        return self._getFromSet(token, self._local, default)
+
+    def getUpdated(self, token, default=None):
+        return self._getFromSet(token, self._updated, default)
+
+    def iterSuggested(self, token):
+        return self._yieldFromSet(token, self._suggested)
+
+    def iterModified(self, token):
+        return self._yieldFromSet(token, self._modified)
+
+    def iterMerged(self, token):
+        vault = self.vault
+        seen = set()
+        for iiset, rel_set in self._bases.values():
+            if rel_set is not vault:
+                for r in self._yieldFromSet(token, iiset):
+                    if r not in seen:
+                        yield r
+                        seen.add(r)
+
+    def _parents(self, token):
+        return self._index.findRelationshipTokenSet({'child': token})
+
+    def iterSelectedParents(self, token):
+        get = self.vault.intids.getObject
+        for iid in self._parents(token):
+            if iid in self._selections:
+                yield get(iid)
+
+    def iterParents(self, token):
+        get = self.vault.intids.getObject
+        return (get(iid) for iid in self._parents(token))
+
+    def getParent(self, token):
+        good = set()
+        orphaned = set()
+        unselected = set()
+        orphaned_unselected = set()
+        for iid in self._parents(token):
+            is_orphaned = self.isOrphan(iid)
+            if iid in self._selections:
+                if is_orphaned:
+                    orphaned.add(iid)
+                else:
+                    good.add(iid)
+            else:
+                if is_orphaned:
+                    orphaned_unselected.add(iid)
+                else:
+                    unselected.add(iid)
+        for s in (good, orphaned, unselected, orphaned_unselected):
+            if s:
+                if len(s) > 1:
+                    raise interfaces.ParentConflictError
+                return self.vault.intids.getObject(s.pop())
+
+    def isLinked(self, token, child):
+        return self._index.isLinked(
+            self._index.tokenizeQuery({'token': token}),
+            filter=self._selectionsFilter,
+            targetQuery=self._index.tokenizeQuery({'child': child}))
+
+    def iterUpdateConflicts(self):
+        # any proposed (not accepted) relationship that has both update and
+        # local for its token
+        if self._updateSource is None:
+            raise StopIteration
+        get = self.vault.intids.getObject
+        for t in self._conflicts:
+            if t not in self._resolutions:
+                rs = list(self._index.findRelationshipTokenSet({'token': t}))
+                for r in rs:
+                    if r in self._selections:
+                        yield get(r)
+                        break
+                else:
+                    assert 0, (
+                        'programmer error: no selected relationship found for '
+                        'conflicted token')
+
+    def iterUpdateResolutions(self):
+        if self._updateSource is None:
+            raise StopIteration
+        get = self.vault.intids.getObject
+        for t in self._resolutions:
+            assert t in self._conflicts
+            rs = list(self._index.findRelationshipTokenSet({'token': t}))
+            for r in rs:
+                if r in self._selections:
+                    yield get(r)
+                    break
+            else:
+                assert 0, (
+                    'programmer error: no selected relationship found for '
+                    'resolved token')
+
+    def isUpdateConflict(self, token):
+        return (token in self._conflicts and
+                token not in self._resolutions)
+
+    @versioning.method
+    def resolveUpdateConflict(self, token):
+        if not self.updating:
+            raise interfaces.UpdateError(
+                'can only resolve merge conflicts during update')
+        if token not in self._conflicts:
+            raise ValueError('token does not have merge conflict')
+        self._resolutions.insert(token)
+
+    def _iterOrphans(self, condition):
+        get = self.vault.intids.getObject
+        res = set(self._selections)
+        res.difference_update(self._iterLinked())
+        bases = IFBTree.multiunion([d[0] for d in self._bases.values()])
+        res.difference_update(bases)
+        for t in res:
+            tids = self._index.findValueTokenSet(t, 'token')
+            assert len(tids) == 1
+            tid = iter(tids).next()
+            if not condition(tid):
+                continue
+            yield get(t)
+
+    def iterOrphanConflicts(self):
+        return self._iterOrphans(lambda t: t not in self._orphanResolutions)
+
+    def iterOrphanResolutions(self):
+        return self._iterOrphans(lambda t: t in self._orphanResolutions)
+
+    def isOrphan(self, token):
+        return not (token == self.vault.top_token or
+                    self.isLinked(self.vault.top_token, token))
+
+    def isOrphanConflict(self, token):
+        return (self.isOrphan(token) and
+                self.getType(token) not in (interfaces.BASE, interfaces.MERGED)
+                and token not in self._orphanResolutions)
+
+    @versioning.method
+    def resolveOrphanConflict(self, token):
+        self._orphanResolutions.insert(token)
+
+    @versioning.method
+    def undoOrphanConflictResolution(self, token):
+        self._orphanResolutions.remove(token)
+
+    def iterParentConflicts(self):
+        get = self.vault.intids.getObject
+        seen = set()
+        for r in self._iterLinked():
+            if r in seen:
+                continue
+            seen.add(r)
+            ts = self._index.findValueTokenSet(r, 'token')
+            assert len(ts) == 1
+            t = iter(ts).next()
+            paths = list(self._index.findRelationshipTokenChains(
+                {'child': t}, filter=self._selectionsFilter,
+                targetQuery={'token': self.vault.top_token}))
+            if len(paths) > 1:
+                yield get(r)
+
+    def _getChildErrors(self):
+        parents = set()
+        children = set()
+        for rid in self._iterLinked():
+            parents.update(self._index.findValueTokenSet(rid, 'token'))
+            children.update(self._index.findValueTokenSet(rid, 'child'))
+        children.difference_update(parents)
+        return children # these are token ids
+
+    def iterAll(self): # XXX __iter__?
+        get = self.vault.intids.getObject
+        seen = set()
+        for s in (self._local, self._updated, self._suggested,
+                  self._modified):
+            for t in s:
+                if t not in seen:
+                    yield get(t)
+                    seen.add(t)
+        for iidset, rel_set in self._bases.values():
+            for t in iidset:
+                if t not in seen:
+                    yield get(t)
+                    seen.add(t)
+
+    def iterSelections(self): # XXX __iter__?
+        get = self.vault.intids.getObject
+        return (get(t) for t in self._selections)
+            
+
+    def __iter__(self): # XXX iterLinked?
+        get = self.vault.intids.getObject
+        return (get(t) for t in self._iterLinked())
+
+    def iterUnchangedOrphans(self):
+        get = self.vault.intids.getObject
+        res = set(self._selections)
+        res.difference_update(self._iterLinked())
+        bases = IFBTree.multiunion([d[0] for d in self._bases.values()])
+        res.intersection_update(bases)
+        return (get(t) for t in res)
+
+    @property
+    def previous(self):
+        i = self.vault_index
+        if i is not None and len(self.vault)-1 >= i and self.vault[i] is self:
+            if i > 0:
+                return self.vault[i-1]
+            return None
+        return self.base_source
+
+    @property
+    def next(self):
+        i = self.vault_index
+        if i is not None and len(self.vault) > i+1 and self.vault[i] is self:
+            return self.vault[i+1]
+        return None
+
+    def isOption(self, relationship): # XXX __contains__?
+        for rel in self._index.findRelationships(
+            self._index.tokenizeQuery(
+                {'token': relationship.token, 'object': relationship.object})):
+            if rel is relationship:
+                return True
+        return False
+
+class HeldContainer(zope.app.container.btree.BTreeContainer):
+    pass
+
+class Vault(persistent.Persistent, zope.app.container.contained.Contained):
+    interface.implements(interfaces.IVault)
+    def __init__(self, intids=None, held=None):
+        self._data = IOBTree.IOBTree()
+        if intids is None:
+            intids = zope.app.intid.IntIds()
+        self.intids = intids
+        if intids.__parent__ is None:
+            zope.location.locate(intids, self, 'intids')
+        self._next = OOBTree.OOBTree()
+        self.top_token = intids.register(keyref.top_token)
+        self._held = held
+
+    @property
+    def held(self):
+        return self._held
+
+    def createBranch(self, ix=-1, held=None):
+        # ...shugah, shugah...
+        # uh, that means that this is sugar for the "make sure you share the
+        # intids when you make a branch" issue.
+        if not isinstance(ix, int):
+            raise ValueError('ix must be int')
+        if held is None:
+            held = self.held
+        res = self.__class__(self.intids, held)
+        res.commit(Manifest(self[ix]))
+        return res
+
+    def getPrevious(self, relationship):
+        manifest = relationship.__parent__ # first one with this relationship
+        ix = manifest.vault_index
+        if ix > 0:
+            return manifest.vault[ix-1].get(relationship.token)
+        return None
+
+    def getNext(self, relationship):
+        return self._next.get(self.intids.getId(relationship))
+
+    def __len__(self):
+        if self._data:
+            return self._data.maxKey() + 1
+        else:
+            return 0
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            start, stop, stride = key.indices(len(self))
+            if stride==1:
+                return self._data.values(start, stop, excludemax=True)
+            else:
+                pos = start
+                res = []
+                while pos < stop:
+                    res.append(self._data[pos])
+                    pos += stride
+                return res
+        elif key < 0:
+            effective_key = len(self) + key
+            if effective_key < 0:
+                raise IndexError(key)
+            return self._data[effective_key]
+        elif key >= len(self):
+            raise IndexError(key)
+        else:
+            return self._data[key]
+
+    def __iter__(self):
+        return iter(self._data.values())
+
+    @property
+    def manifest(self):
+        if self._data:
+            return self._data[self._data.maxKey()]
+        return None
+
+    def commit(self, manifest):
+        if not interfaces.IManifest.providedBy(manifest):
+            raise ValueError('may only commit an IManifest')
+        current = self.manifest
+        if current is not None:
+            current_vaults = set(id(b.vault) for b in current.getBaseSources())
+            current_vaults.add(id(current.vault))
+            new_vaults = set(id(b.vault) for b in manifest.getBaseSources())
+            new_vaults.add(id(manifest.vault))
+            if not current_vaults & new_vaults:
+                raise ValueError(
+                    'may only commit a manifest with at least one shared base')
+            elif manifest.getBaseSource(self) is not current and (
+                not manifest.updating or (
+                    manifest.updating and
+                    manifest.update_source is not current)):
+                raise interfaces.OutOfDateError(manifest)
+        self._commit(manifest)
+
+    def _commit(self, manifest):
+        if manifest._z_versioned:
+            raise zc.copyversion.interfaces.VersionedError(manifest)
+        if manifest.get(self.top_token) is None:
+            raise ValueError(
+                'cannot commit a manifest without a top_token relationship')
+        if (self.manifest is not None and
+            not len(tuple(manifest.iterChanges(self.manifest)))):
+            raise interfaces.NoChangesError(manifest)
+        if manifest.updating:
+            manifest.completeUpdate()
+        elif (list(manifest.iterUpdateConflicts()) or
+              list(manifest.iterOrphanConflicts()) or
+              list(manifest.iterParentConflicts())):
+            raise interfaces.ConflictError(manifest)
+        manifest.vault = self
+        ix = len(self)
+        self._data[ix] = manifest
+        manifest.vault_index = ix
+        manifest._z_version()
+        for r in manifest:
+            if manifest.getLocal(r.token) is r:
+                p = self.getPrevious(r)
+                if p is not None and p.__parent__.vault is self:
+                    pid = self.intids.getId(p)
+                    self._next[pid] = r
+        if (manifest.__parent__ is None or
+            manifest.__name__ is None):
+            # so absoluteURL on objects in held "works" (needs traversers to
+            # really work)
+            zope.location.locate(manifest, self, unicode(manifest.vault_index))
+        event.notify(interfaces.ManifestCommitted(manifest))
+
+    def commitFrom(self, source):
+        if not interfaces.IManifest.providedBy(source):
+            raise ValueError('source must be manifest')
+        if source.vault.intids is not self.intids:
+            raise ValueError('source must share intids')
+        if not source._z_versioned:
+            raise ValueError('source must already be versioned')
+        res = Manifest(self.manifest)
+        base_rels = dict((r.token, r) for r in res.base_source)
+        for rel in source:
+            base_rel = base_rels.pop(rel.token, None)
+            if base_rel is not None and base_rel == rel:
+                res.select(rel)
+            else:
+                rel = Relationship(
+                    rel.token, relationship=rel, source_manifest=res)
+                rel.__parent__ = res
+                event.notify(
+                    zope.lifecycleevent.ObjectCreatedEvent(rel))
+                res._add(rel, res._local, True)
+                res.select(rel)
+                event.notify(interfaces.LocalRelationshipAdded(rel))
+        for rel in base_rels.values():
+            res.select(rel) # to make sure that any hidden local changes are
+            # ignored.  We don't need to resolve the orphans because base
+            # orphans are not regarded as conflicts
+        self._commit(res)

Added: zc.vault/trunk/src/zc/vault/i18n.py
===================================================================
--- zc.vault/trunk/src/zc/vault/i18n.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/i18n.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,33 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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
+#
+##############################################################################
+"""I18N support.
+
+This defines a `MessageFactory` for the I18N domain for the zc.vault
+package.  This is normally used with this import::
+
+  from i18n import MessageFactory as _
+
+The factory is then used normally.  Two examples::
+
+  text = _('some internationalized text')
+  text = _('helpful-descriptive-message-id', 'default text')
+"""
+__docformat__ = "reStructuredText"
+
+
+from zope import i18nmessageid
+
+MessageFactory = _ = i18nmessageid.MessageFactory("zc.vault")
+

Added: zc.vault/trunk/src/zc/vault/interfaces.py
===================================================================
--- zc.vault/trunk/src/zc/vault/interfaces.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/interfaces.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,770 @@
+from zope import interface
+import zope.interface.common.mapping
+import zope.interface.common.sequence
+import zope.app.container.interfaces
+import zope.component.interfaces
+
+#### CONSTANTS ####
+
+# see IManifest.getType for definitions
+LOCAL = 'local'
+BASE = 'base'
+UPDATED = 'updated'
+SUGGESTED = 'suggested'
+MODIFIED = 'modified'
+MERGED = 'merged'
+
+#### EXCEPTIONS ####
+
+class OutOfDateError(ValueError):
+    """Manifest to be committed is not based on the currently committed version
+    """
+
+class NoChangesError(ValueError):
+    """Manifest to be committed has no changes from the currently committed
+    version"""
+
+class ConflictError(ValueError):
+    """Manifest to be committed has unresolved conflicts"""
+
+#### EVENTS ####
+
+class IMappingEvent(zope.component.interfaces.IObjectEvent): # abstract
+
+    mapping = interface.Attribute(
+        'the affected mapping; __parent__ is relationship')
+
+    key = interface.Attribute('the key affected')
+
+class IObjectRemoved(IMappingEvent):
+    """Object was removed from mapping"""
+
+class IObjectAdded(IMappingEvent):
+    """Object was added to mapping"""
+
+class IOrderChanged(zope.component.interfaces.IObjectEvent):
+    """Object order changed; object is mapping"""
+
+    old_keys = interface.Attribute('A tuple of the old key order')
+
+class IManifestCommitted(zope.component.interfaces.IObjectEvent):
+    """Object is manifest."""
+
+class ILocalRelationshipAdded(zope.component.interfaces.IObjectEvent):
+    """Relationship added to manifest as a local version.
+    Relationship.__parent__ must be manifest."""
+
+class IModifiedRelationshipAdded(zope.component.interfaces.IObjectEvent):
+    """Relationship added to manifest as a modified version.
+    Relationship.__parent__ must be manifest."""
+
+class ISuggestedRelationshipAdded(zope.component.interfaces.IObjectEvent):
+    """Relationship added to manifest as a suggested version.
+    Relationship.__parent__ must be manifest."""
+
+class IUpdateEvent(zope.component.interfaces.IObjectEvent):
+
+    source = interface.Attribute(
+        '''source manifest (from beginUpdate) or collection
+        (from beginCollectionUpdate)''')
+
+    base = interface.Attribute(
+        '''the base manifest from which the update proceeds, or None.''')
+
+class IUpdateBegun(IUpdateEvent):
+    '''fired from beginUpdate or beginCollectionUpdate'''
+
+class IUpdateCompleted(IUpdateEvent):
+    ''
+
+class IUpdateAborted(IUpdateEvent):
+    ''
+
+class IObjectChanged(zope.component.interfaces.IObjectEvent):
+    previous = interface.Attribute('the previous object')
+
+class IVaultChanged(zope.component.interfaces.IObjectEvent):
+    previous = interface.Attribute('the previous vault')
+
+class IRelationshipSelected(zope.component.interfaces.IObjectEvent):
+    """relationship was selected"""
+    manifest = interface.Attribute(
+        'the manifest in which this relationship was selected')
+
+class IRelationshipDeselected(zope.component.interfaces.IObjectEvent):
+    """relationship was deselected"""
+    manifest = interface.Attribute(
+        'the manifest in which this relationship was deselected')
+
+class ObjectRemoved(zope.component.interfaces.ObjectEvent):
+    interface.implements(IObjectRemoved)
+    def __init__(self, obj, mapping, key):
+        super(ObjectRemoved, self).__init__(obj)
+        self.mapping = mapping
+        self.key = key
+
+class ObjectAdded(zope.component.interfaces.ObjectEvent):
+    interface.implements(IObjectAdded)
+    def __init__(self, obj, mapping, key):
+        super(ObjectAdded, self).__init__(obj)
+        self.mapping = mapping
+        self.key = key
+
+class OrderChanged(zope.component.interfaces.ObjectEvent):
+    interface.implements(IOrderChanged)
+    def __init__(self, obj, old_keys):
+        super(OrderChanged, self).__init__(obj)
+        self.old_keys = old_keys
+
+class ManifestCommitted(zope.component.interfaces.ObjectEvent):
+    interface.implements(IManifestCommitted)
+
+class LocalRelationshipAdded(zope.component.interfaces.ObjectEvent):
+    interface.implements(ILocalRelationshipAdded)
+
+class ModifiedRelationshipAdded(zope.component.interfaces.ObjectEvent):
+    interface.implements(IModifiedRelationshipAdded)
+
+class SuggestedRelationshipAdded(zope.component.interfaces.ObjectEvent):
+    interface.implements(ISuggestedRelationshipAdded)
+
+class AbstractUpdateEvent(zope.component.interfaces.ObjectEvent):
+    def __init__(self, obj, source, base):
+        super(AbstractUpdateEvent, self).__init__(obj)
+        self.source = source
+        self.base = base
+
+class UpdateBegun(AbstractUpdateEvent):
+    interface.implements(IUpdateBegun)
+
+class UpdateCompleted(AbstractUpdateEvent):
+    interface.implements(IUpdateCompleted)
+
+class UpdateAborted(AbstractUpdateEvent):
+    interface.implements(IUpdateAborted)
+
+class VaultChanged(zope.component.interfaces.ObjectEvent):
+    interface.implements(IVaultChanged)
+    def __init__(self, obj, previous):
+        super(VaultChanged, self).__init__(obj)
+        self.previous = previous
+
+class ObjectChanged(zope.component.interfaces.ObjectEvent):
+    interface.implements(IObjectChanged)
+    def __init__(self, obj, previous):
+        super(ObjectChanged, self).__init__(obj)
+        self.previous = previous
+
+class RelationshipSelected(zope.component.interfaces.ObjectEvent):
+    interface.implements(IRelationshipSelected)
+    def __init__(self, obj, manifest):
+        super(RelationshipSelected, self).__init__(obj)
+        self.manifest = manifest
+
+class RelationshipDeselected(zope.component.interfaces.ObjectEvent):
+    interface.implements(IRelationshipDeselected)
+    def __init__(self, obj, manifest):
+        super(RelationshipDeselected, self).__init__(obj)
+        self.manifest = manifest
+
+#### EXCEPTIONS ####
+
+class ParentConflictError(StandardError):
+    """the item has more than one selected parent"""
+
+class UpdateError(StandardError):
+    """Update-related operation cannot proceed"""
+
+#### BASIC INTERFACES ####
+
+class IVersionFactory(interface.Interface):
+    """looked up as a adapter of vault"""
+    def __call__(object, manifest):
+        """return the object to be stored"""
+
+class IConflictResolver(interface.Interface):
+    """looked up as a adapter of vault."""
+    def __call__(manifest, local, update, base):
+        """React to conflict between local and update as desired."""
+
+class IUniqueReference(interface.Interface):
+
+    identifiers = interface.Attribute(
+        """An iterable of identifiers for this object.
+        From most general to most specific.  Combination uniquely identifies
+        the object.""")
+
+    def __hash__():
+        """return a hash of the full set of identifiers."""
+
+    def __cmp__(other):
+        """Compare against other objects that provide IUniqueReference,
+        using the identifiers.""" # note that btrees do not support rich comps
+
+class IToken(IUniqueReference):
+    """An object used as a token for a manifest relationship"""
+
+class IBidirectionalNameMapping(zope.interface.common.mapping.IMapping):
+    """all keys are unicode, all values are adaptable to IKeyReference.
+
+    all values must be unique (no two keys may have the same value).
+
+    items, values, keys, and __iter__ returns in the specified order."""
+
+    def getKey(value, default=None):
+        """return key for value, or None"""
+
+    def updateOrder(order):
+        """Revise the order of keys, replacing the current ordering.
+
+        order is an iterable containing the set of existing keys in the new
+        order. `order` must contain ``len(keys())`` items and cannot contain
+        duplicate keys.
+
+        Raises ``TypeError`` if order is not iterable or any key is not
+        hashable.
+
+        Raises ``ValueError`` if order contains an invalid set of keys.
+        """
+
+class IRelationshipContainment(IBidirectionalNameMapping):
+    """If __parent__.__parent__ (manifest) exists, must call
+    manifest.approveRelationshipChange before making any changes, and should
+    call manifest.reindex after all changes except order changes.
+    """
+
+    __parent__ = interface.Attribute(
+        """The IRelationship of this containment before versioning (may be
+        reused for other relationships after versioning).""")
+
+class IRelationship(IUniqueReference):
+    """The relationship for mapping a token to its contents and its object.
+    Not mutable if can_modify is False."""
+
+    token = interface.Attribute(
+        """The token that this relationship maps""")
+
+    __parent__ = interface.Attribute(
+        """The manifest of this relationship before versioning (may be
+        reused for other manifests after being versioned)""")
+
+    object = interface.Attribute(
+        """the object that the token represents for this relationship.
+        if __parent__ exists (manifest), should call
+        manifest.approveRelationshipChange before making any changes, and
+        call manifest.reindex after change.""")
+
+    containment = interface.Attribute(
+        """The IRelationshipContainment, mapping names to contained tokens,
+        for this relationship.""")
+
+    children = interface.Attribute(
+        """readonly: the containment.values().  modify with the containment."""
+        )
+
+class IContained(IBidirectionalNameMapping):
+    """Abstract interface."""
+
+    previous = interface.Attribute(
+        """The IContained in the vault's previous inventory, or None if
+        it has no previous version.  May be equal to (have the same
+        relationship as) this IContained.""")
+
+    next = interface.Attribute(
+        """The IContained in the vault's next inventory, or None if
+        it has no next version.  May be equal to (have the same
+        relationship as) this IContained.""")
+
+    previous_version = interface.Attribute(
+        """The previous version of the IContained in the vault, or None if
+        it has no previous version.  Will never be equal to (have the same
+        relationship as) this IContained.""")
+
+    next_version = interface.Attribute(
+        """The next version of the IContained in the vault, or None if
+        it has no next version.  Will never be equal to (have the same
+        relationship as) this IContained.""")
+
+    __parent__ = interface.Attribute(
+        """the inventory to which this IContained belongs; same as inventory.
+        """)
+
+    inventory = interface.Attribute(
+        """the inventory to which this IContained belongs; same as __parent__.
+        """)
+
+    relationship = interface.Attribute(
+        """The relationship that models the containment and object information
+        for the token.""")
+
+    token = interface.Attribute(
+        """The token assigned to this IContained's relationship.
+
+        Synonym for .relationship.token""")
+
+    def __call__(name):
+        """return an IContained for the name, or raise Key Error"""
+
+    def makeMutable():
+        """convert this item to a mutable version if possible. XXX"""
+
+    type = interface.Attribute(
+        '''readonly; one of LOCAL, BASE, MERGED, UPDATED, SUGGESTED, MODIFIED.
+        see IManifest.getType (below) for definitions.''')
+
+    selected = interface.Attribute(
+        '''readonly boolean; whether this item is selected.
+        Only one item (relationship) may be selected at a time for a given
+        token in a given inventory''')
+
+    selected_item = interface.Attribute(
+        '''the selected version of this item''')
+
+    def select():
+        '''select this item, deselecting any other items for the same token'''
+
+    is_update_conflict = interface.Attribute(
+        '''whether this is an unresolved update conflict''')
+
+    def resolveUpdateConflict():
+        '''select this item and mark the update conflict as resolved.'''
+
+    has_base = interface.Attribute("whether item has a base version")
+
+    has_local = interface.Attribute("whether item has a local version")
+
+    has_updated = interface.Attribute("whether item has an updated version")
+
+    has_suggested = interface.Attribute(
+        "whether item has any suggested versions")
+
+    has_modified = interface.Attribute(
+        "whether item has any modified versions")
+
+    has_merged = interface.Attribute(
+        "whether item has any merged versions")
+
+    base_item = interface.Attribute('''the base item, or None''')
+
+    local_item = interface.Attribute('''the local item, or None''')
+
+    updated_item = interface.Attribute('''the updated item, or None''')
+
+    def iterSuggestedItems():
+        """iterate over suggested items"""
+
+    def iterModifiedItems():
+        """iterate over modified items"""
+
+    def iterMergedItems():
+        """iterate over merged items"""
+
+    def updateOrderFromTokens(order):
+        """Revise the order of keys, replacing the current ordering.
+
+        order is an iterable containing the set of tokens in the new order.
+        `order` must contain ``len(keys())`` items and cannot contain duplicate
+        values.
+
+        XXX what exceptions does this raise?
+        """
+
+class IInventoryContents(IContained):
+    """The top node of an inventory's hierarchy"""
+
+class IInventoryItem(IContained):
+
+    is_orphan = interface.Attribute(
+        '''whether this item cannot be reached from the top of the inventory's
+        hierarchy via selected relationships/items''')
+
+    is_orphan_conflict = interface.Attribute(
+        '''whether this is an orphan (see is_orphan) that is not BASE or
+        MERGED and not resolved.''')
+
+    def resolveOrphanConflict():
+        '''resolve the orphan conflict so that it no longer stops committing
+        or completing an update'''
+
+    is_parent_conflict = interface.Attribute(
+        '''whether this object has more than one selected parent''')
+
+    parent = interface.Attribute(
+        """The effective parent of the IContained.
+        Always another IContained, or None (for an IInventoryContents).
+        Will raise ParentConflictError if multiple selected parents.""")
+
+    name = interface.Attribute(
+        """The effective name of the IContained.
+        Always another IContained, or None (for an IInventoryContents).
+        Will raise ParentConflictError if multiple selected parents.""")
+
+    def iterSelectedParents():
+        '''iterate over all selected parents'''
+
+    def iterParents():
+        '''iterate over all parents'''
+
+    object = interface.Attribute(
+        """the object to which this IContained's token maps.  The
+        vault_contents for the vault's top_token""")
+
+    def copyTo(location, name=None):
+        """make a clone of this node and below in location.  Does not copy
+        actual object(s): just puts the same object(s) in an additional
+        location.
+
+        Location must be an IContained.  Copying to another inventory is
+        currently undefined.
+        """
+
+    def moveTo(location=None, name=None):
+        """move this object's tree to location.
+
+        Location must be an IMutableContained from the same vault_contents.
+        Not specifying location indicates the current location (merely a
+        rename)."""
+
+    copy_source = interface.Attribute(
+        '''the item representing the relationship and inventory from which this
+        item's relationship was created.''')
+
+class IInventory(interface.Interface):
+    """IMPORTANT: the top token in an IInventory (and IManifest) is always
+    zc.vault.keyref.top_token."""
+
+    manifest = interface.Attribute(
+        """The IManifest used by this inventory""")
+
+    def iterUpdateConflicts():
+        '''iterate over the unresolved items that have update conflicts'''
+
+    def iterUpdateResolutions():
+        '''iterate over the resolved items that have update conflicts'''
+
+    def iterOrphanConflicts():
+        '''iterate over the current unresolved orphan conflicts'''
+
+    def iterOrphanResolutions():
+        '''iterate over the current resolved orphan conflicts'''
+
+    def iterUnchangedOrphans():
+        '''iterate over the orphans that do not cause conflicts--the ones that
+        were not changed, so are either in the base or a merged inventory.'''
+
+    def iterParentConflicts():
+        '''iterate over the items that have multiple parents.
+        The only way to resolve these is by deleting the item in one of the
+        parents.'''
+
+    def __iter__():
+        '''iterates over all selected items, whether or not they are orphans.
+        '''
+
+    updating = interface.Attribute(
+        '''readonly boolean: whether inventory is updating''')
+
+    merged_sources = interface.Attribute(
+        '''a tuple of the merged sources for this item.''')
+
+    update_source = interface.Attribute(
+        '''the source currently used for an update, or None if not updating''')
+
+    def beginUpdate(inventory=None, previous=None):
+        """begin update.  Fails if already updating.  if inventory is None,
+        uses the current vault's most recent checkin.  if previous is None,
+        calculates most recent shared base.
+        """
+
+    def completeUpdate():
+        """complete update, moving update to merged.  Fails if any update,
+        orphan, or parent conflicts."""
+
+    def abortUpdate():
+        """returns to state before update, discarding all changes made during
+        the update."""
+
+    def beginCollectionUpdate(items):
+        """start an update based on just a collection of items"""
+
+    def iterChangedItems(source=None):
+        '''iterate over items that have changed from source.
+        if source is None, use previous.'''
+
+    def getItemFromToken(token, default=None):
+        """get an item for the token in this inventory, or return default."""
+
+    previous = interface.Attribute('the previous inventory in the vault')
+
+    next = interface.Attribute('the next inventory in the vault')
+
+class IVault(zope.app.container.interfaces.IContained,
+             zope.interface.common.sequence.IFiniteSequence):
+    """A read sequence of historical manifests.  The oldest is 0, and the
+    most recent is -1."""
+
+    manifest = interface.Attribute(
+        """The most recent committed manifest (self[-1])""")
+
+    def getPrevious(relationship):
+        '''return the previous version of the relationship (based on token)
+        or None'''
+
+    def getNext(relationship):
+        '''return the next version of the relationship (based on token)
+        or None'''
+
+    def commit(manifest):
+        """XXX"""
+
+    def commitFrom(manifest):
+        """XXX"""
+
+class IInventoryVault(IVault):
+    """commit and commitFrom also take inventories"""
+
+    inventory = interface.Attribute(
+        """The most recent committed inventory (self.getInventory(-1))""")
+
+    def getInventory(self, ix):
+        """return IInventory for relationship set at index ix"""
+
+class IManifest(interface.Interface):
+    """IMPORTANT: the top token in an IManifest (and IInventory) is always
+    zc.vault.keyref.top_token."""
+    # should manifests know all of the manifests
+    # they have generated (working copies)?  Maybe worth keeping track of?
+
+    vault_index = interface.Attribute(
+        'the index of the manifest in its vault')
+
+    held = interface.Attribute(
+        """Container of any related objects held because they were nowhere
+        else.
+        """)
+
+    def getBaseSources():
+        '''iterate over all bases (per vault)'''
+
+    def getBaseSource(vault):
+        '''get the base for the vault'''
+
+    base_source = interface.Attribute('''the base of the set''')
+
+    vault = interface.Attribute('the vault for this relationship set')
+
+    vault_index = interface.Attribute('the index of this set in the vault')
+
+    merged_sources = interface.Attribute(
+        '''a tuple of the non-base merged sources, as found in getBase.''')
+
+    update_source = interface.Attribute(
+        '''the manifest used for the upate''')
+
+    update_base = interface.Attribute(
+        '''the manifest for the shared ancestor that the two manifests share'''
+        )
+
+    updating = interface.Attribute(
+        '''boolean.  whether in middle of update.''')
+
+    base_source = interface.Attribute(
+        """the manifest used as a base for this one.""")
+
+    def addLocal(relationship):
+        '''add local copy except during update.  If no other relationship
+        exists for the token, select it.  If no relationship already exists
+        for the child tokens, disallow, raising ValueError.'''
+
+    def addModified(relationship):
+        '''add modified copies during update  If no other relationship
+        exists for the token, select it.  If no relationship already exists
+        for the child tokens, disallow, raising ValueError.'''
+
+    def addSuggested(relationship):
+        '''add suggested copies during update.  Another relationship must
+        already exist in the manifest for the relationship's token.'''
+
+    def checkRelationshipChange(relationship):
+        """raise errors if the relationship may not be changed.
+        UpdateError if the manifest is updating and the relationship is LOCAL;
+        TypeError (XXX?) if the relationship is SUGGESTED or UPDATED"""
+
+    def beginUpdate(source=None, base=None):
+        '''begin an update.  Calculates update conflicts, tries to resolve.
+        If source is None, uses vault's most recent manifest.  If base is None,
+        uses the most recent shared base between this manifest and the source,
+        if any.
+
+        if already updating, raise UpdateError.
+
+        update conflicts (different changes from the base both locally and in
+        the source) are given to an interfaces.IConflictResolver, if an
+        adapter to this interface is provided by the vault, to do with as it
+        will (typically including making suggestions and resolving).'''
+
+    def beginCollectionUpdate(source):
+        '''cherry-pick update interface: CURRENTLY IN FLUX'''
+
+    def completeUpdate():
+        '''moves update source to bases; turns off updating; discards unused
+        suggested, modified, and local relationships.
+
+        Newer versions of the bases of the update source will replace the
+        bases of this manifest. if a BASE or MERGED relationship (see getType
+        for definitions) is selected and its source is no longer a part of the
+        bases after the bases are replaced, a new (mutable) copy is created as
+        a local relationship.'''
+
+    def abortUpdate():
+        '''return manifest to state before beginUpdate.'''
+
+    def iterChanges(base=None):
+        ''''iterate over all selected relationships that differ from the base.
+        if base is not given, uses self.base_source'''
+
+    def reindex(relationship):
+        '''reindex the relationship after a change: used by relationships.'''
+
+    def get(token, default=None):
+        '''return the currently selected relationship for the token'''
+
+    def getType(relationship):
+        '''return type of relationship: one of BASE, LOCAL, UPDATED,
+        SUGGESTED, MODIFIED, MERGED (see constants in this file, above).
+
+        BASE relationships are those in the base_source (which is the manifest
+        from the current vault on which this manifest was based).  There will
+        only be zero or one BASE relationship for any given token in a
+        manifest.
+
+        LOCAL relationships are new relationships added (replacing or in
+        addition to BASE) when not updating.  They may not be modified while
+        the manifest is updating.  There will only be zero or one LOCAL
+        relationship for any given token in a manifest.
+
+        UPDATED relationships only are available in this manifest while
+        updating, and are relationships changed from the BASE of the same
+        token.  They may not be modified, even if they have not been versioned
+        (e.g., added via `beginCollectionUpdate`). There will only be zero or
+        one UPDATED relationship for any given token in a manifest.
+
+        SUGGESTED relationships only exist while updating, and are intended to
+        be relationships that an IConflictResolver created (although the
+        resolvers have free reign).  They may not be modified, even if they
+        have not been versioned.  There will be zero or more (unbounded)
+        SUGGESTED relationships for any given token in a manifest. All
+        MODIFIED relationships are discarded after an `abortUpdate`.
+
+        MODIFIED relationships only exist while updating, and are the only
+        relationships that may be modified while updating. There will be zero
+        or more (unbounded) MODIFIED relationships for any given token in a
+        manifest. Unselected MODIFIED relationships are discarded after an
+        `completeUpdate`, and all MODIFIED relationships are discarded after
+        an `abortUpdate`.
+
+        MERGED relationships are those in the manifests returned by
+        `getBaseSources` that are not the `base_source`: that is, typically
+        those in manifests that have been merged into this one.  There will
+        be zero or more MERGED relationships--no more than
+        `len(self.getBaseSources()) -1`--in a manifest for a given token.
+
+        '''
+
+    def isSelected(relationship):
+        '''bool whether relationship is selected'''
+
+    def select(relationship):
+        '''select the relationship for the given token.  There should always be
+        one and only one selected relationship for any given token known about
+        by the manifest.'''
+
+    def getBase(token, default=None):
+        '''Get the base relationship for the token, or default if None'''
+
+    def getLocal(token, default=None):
+        '''Get the local relationship for the token, or default if None'''
+
+    def getUpdated(token, default=None):
+        '''Get the updated relationship for the token, or default if None'''
+
+    def iterSuggested(token):
+        '''Iterate over suggested relationships for the token.'''
+
+    def iterModified(token):
+        '''Iterate over modified relationships for the token.'''
+
+    def iterMerged(token):
+        '''Iterate over merged relationships for the token.'''
+
+    def iterSelectedParents(token):
+        '''Iterate over selected parent for the token.  If there is more than
+        one, it is a parent conflict; if there are none and the token is not
+        the zc.vault.keyref.top_token, it is an orphan.'''
+
+    def iterParents(token):
+        '''iterate over all possible parents, selected and unselected, for the
+        token'''
+
+    def isLinked(token, child):
+        '''returns boolean, whether child token is transitively linked
+        beneath token using only selected relationships.'''
+
+    def iterUpdateConflicts():
+        '''iterate over unresolved update conflicts.'''
+
+    def iterUpdateResolutions():
+        '''iterate over resolved update conflicts.'''
+
+    def isUpdateConflict(token):
+        '''returns boolean, whether token is an unresolved update conflict.'''
+
+    def resolveUpdateConflict(token):
+        '''resolve the update conflict ("stop complaining, and use whatever is
+        selected")'''
+
+    def iterOrphanConflicts():
+        '''iterate over unresolved orphan conflicts--selected relationships
+        changed from the BASE and MERGED relationships.'''
+
+    def iterOrphanResolutions():
+        '''iterate over resolved orphan conflicts.'''
+
+    def isOrphan(token):
+        '''Whether token is an orphan.'''
+
+    def isOrphanConflict(token):
+        '''Whether token is an unresolved orphan token, as found in
+        iterOrphanConflicts'''
+
+    def resolveOrphanConflict(token):
+        '''resolve the orphan conflict'''
+
+    def undoOrphanConflictResolution(token):
+        '''undo orphan conflict resolution'''
+
+    def iterParentConflicts():
+        '''iterate over all selected relationships that have more than
+        one parent.'''
+
+    def iterAll():
+        '''iterate over all relationships known (of all types)'''
+
+    def iterSelections():
+        '''iterate over all selected relationships.'''
+
+    def __iter__():
+        '''iterate over linked, selected relationships: selected non-orphans.
+        '''
+
+    def iterUnchangedOrphans():
+        '''iterate over BASE and MERGED orphans (that do not cause conflicts)
+        '''
+
+    next = interface.Attribute('the next manifest in the vault')
+
+    previous = interface.Attribute(
+        "the previous manifest in the vault, or from a branch's source")
+
+    def isOption(relationship):
+        """boolean, whether relationship is known (in iterAll)"""

Added: zc.vault/trunk/src/zc/vault/keyref.py
===================================================================
--- zc.vault/trunk/src/zc/vault/keyref.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/keyref.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,74 @@
+import itertools
+import persistent
+from ZODB.interfaces import IConnection
+from zope import interface
+from zope.cachedescriptors.property import Lazy
+import zope.app.keyreference.interfaces
+import zope.app.container.contained
+from zc.vault import interfaces
+
+class AbstractUniqueReference(object):
+    interface.implements(
+        zope.app.keyreference.interfaces.IKeyReference,
+        interfaces.IUniqueReference)
+
+    __slots__ = ()
+
+    # must define identifiers and key_type_id
+
+    def __call__(self):
+        return self
+
+    def __hash__(self):
+        return hash(tuple(self.identifiers))
+
+    def __cmp__(self, other):
+        if interfaces.IUniqueReference.providedBy(other):
+            return cmp(tuple(self.identifiers), tuple(other.identifiers))
+        if zope.app.keyreference.interfaces.IKeyReference.providedBy(other):
+            assert self.key_type_id != other.key_type_id
+            return cmp(self.key_type_id, other.key_type_id)
+        raise ValueError(
+            "Can only compare against IUniqueIdentity and "
+            "IKeyReference objects")
+
+# XXX this is API; need to highlight in tests...
+def getPersistentIdentifiers(obj):
+    if obj._p_oid is None:
+        connection = IConnection(obj, None)
+        if connection is None:
+            raise zope.app.keyreference.interfaces.NotYet(obj)
+        connection.add(obj)
+    return (obj._p_jar.db().database_name, obj._p_oid)
+
+class Token(AbstractUniqueReference, persistent.Persistent,
+            zope.app.container.contained.Contained):
+
+    interface.implements(interfaces.IToken)
+
+    __slots__ = ()
+
+    key_type_id = 'zc.vault.keyref.Token'
+
+    @Lazy
+    def identifiers(self):
+        return (self.key_type_id,) + getPersistentIdentifiers(self)
+
+class _top_token_(AbstractUniqueReference): # creates singleton
+
+    interface.implements(interfaces.IToken)
+
+    __slots__ = ()
+
+    key_type_id = 'zc.vault.keyref.TopToken'
+
+    identifiers = (key_type_id,)
+
+    def __reduce__(self):
+        return _top_token, ()
+
+top_token = _top_token_()
+
+def _top_token():
+    return top_token
+_top_token.__safe_for_unpickling__ = True

Added: zc.vault/trunk/src/zc/vault/objectlog.py
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,176 @@
+from zope import interface, component, schema
+import zope.app.intid.interfaces
+import zope.lifecycleevent.interfaces
+import zope.location
+import zc.objectlog.interfaces
+from zc import objectlog
+import zc.copyversion.interfaces
+from zc.vault.i18n import _
+from zc.vault import interfaces
+
+class IManifestChangeset(interface.Interface):
+    update_source_intid = schema.Int(
+        title=_('Update Source'),
+        description=_('Will be None for collection update'),
+        required=False)
+    update_base_intid = schema.Int(
+        title=_('Update Base'),
+        description=_('Will be None for collection update'),
+        required=False)
+    vault_intid = schema.Int(
+        title=_('Vault'),
+        required=True)
+
+class ManifestChangesetAdapter(object):
+    interface.implements(IManifestChangeset)
+    component.adapts(interfaces.IManifest)
+    def __init__(self, man):
+        intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        self.vault_intid = intids.register(man.vault)
+        self.update_source_intid = self.update_base_intid = None
+        if interfaces.IManifest.providedBy(man.update_source):
+            self.update_source_intid = intids.register(man.update_source)
+            if interfaces.IManifest.providedBy(man.update_base):
+                self.update_base_intid = intids.register(man.update_base)
+
+class IRelationshipChangeset(interface.Interface):
+    items = schema.Tuple(
+        title=_('Items'), required=True)
+    object_intid = schema.Int(title=_('Object'), required=False)
+
+class RelationshipChangesetAdapter(object):
+    interface.implements(IRelationshipChangeset)
+    component.adapts(interfaces.IRelationship)
+    def __init__(self, relationship):
+        self.items = tuple((k, v) for k, v in relationship.containment.items())
+        if relationship.object is None:
+            self.object_intid = None
+        else:
+            intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+            self.object_intid = intids.register(relationship.object)
+
+ at component.adapter(
+    interfaces.IManifest, zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def createManifestLog(man, ev):
+    if not zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log = objectlog.Log(IManifestChangeset)
+        zope.location.locate(man.log, man, 'log')
+        interface.directlyProvides(man, zc.objectlog.interfaces.ILogging)
+    man.log(_('Created'))
+
+ at component.adapter(
+    interfaces.IRelationship,
+    zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def createRelationshipLog(rel, ev):
+    if not zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log = objectlog.Log(IRelationshipChangeset)
+        zope.location.locate(rel.log, rel, 'log')
+        interface.directlyProvides(rel, zc.objectlog.interfaces.ILogging)
+    rel.log(_('Created'))
+    rel.log(_('Created (end of transaction)'), defer=True, if_changed=True)
+
+ at component.adapter(interfaces.IObjectRemoved)
+def logRemoval(ev):
+    rel = ev.mapping.__parent__
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Child removed'))
+
+ at component.adapter(interfaces.IObjectAdded)
+def logAddition(ev):
+    rel = ev.mapping.__parent__
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Child added'))
+
+ at component.adapter(interfaces.IOrderChanged)
+def logOrderChanged(ev):
+    rel = ev.object.__parent__
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Child order changed'))
+
+ at component.adapter(interfaces.IManifestCommitted)
+def logCommit(ev):
+    man = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log(_('Committed'))
+
+ at component.adapter(
+    interfaces.IRelationship, zc.copyversion.interfaces.IObjectVersionedEvent)
+def logVersioning(rel, ev):
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Versioned'))
+
+ at component.adapter(interfaces.ILocalRelationshipAdded)
+def logNewLocal(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Added as local relationship'))
+
+ at component.adapter(interfaces.IModifiedRelationshipAdded)
+def logNewModified(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Added as modified relationship'))
+
+ at component.adapter(interfaces.ISuggestedRelationshipAdded)
+def logNewSuggested(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Added as suggested relationship'))
+
+ at component.adapter(interfaces.IUpdateBegun)
+def logUpdateBegun(ev):
+    man = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log(_('Update begun'))
+
+ at component.adapter(interfaces.IUpdateAborted)
+def logUpdateAborted(ev):
+    man = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log(_('Update aborted'))
+
+ at component.adapter(interfaces.IUpdateCompleted)
+def logUpdateCompleted(ev):
+    man = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log(_('Update completed'))
+
+ at component.adapter(interfaces.IVaultChanged)
+def logVaultChanged(ev):
+    man = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(man):
+        man.log(_('Vault changed'))
+
+# ADDITIONAL_SELECTION = _('Selected in additional manifest (${intid})')
+ at component.adapter(interfaces.IRelationshipSelected)
+def logSelection(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        if ev.manifest is rel.__parent__:
+            rel.log(_('Selected'))
+        # else:
+        #     intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        #     msg = i18n.Message(ADDITIONAL_SELECTION,
+        #                        mapping={'intid': intids.register(
+        #                            ev.manifest)})
+        #     rel.log(msg)
+
+# ADDITIONAL_DESELECTION = _('Deselected from additional manifest (${intid})')
+ at component.adapter(interfaces.IRelationshipDeselected)
+def logDeselection(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        if ev.manifest is rel.__parent__:
+            rel.log(_('Deselected'))
+        # else:
+        #     intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+        #     msg = i18n.Message(ADDITIONAL_DESELECTION,
+        #                        mapping={'intid': intids.register(
+        #                            ev.manifest)})
+        #     rel.log(msg)
+
+ at component.adapter(interfaces.IObjectChanged)
+def logObjectChanged(ev):
+    rel = ev.object
+    if zc.objectlog.interfaces.ILogging.providedBy(rel):
+        rel.log(_('Object changed'))

Added: zc.vault/trunk/src/zc/vault/objectlog.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.txt	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.txt	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,293 @@
+The objectlog module gives objectlogs to manifests and relationships and keeps
+track of most of the changes.  It is all handled with subscribers and two
+adapters.  These have already been installed for these examples; see
+objectlog.zcml for zcml that loads the subscribers, or tests.py for the code
+that installs the components when this file is run as a test.
+
+    >>> from zc.vault.vault import Vault, Inventory
+    >>> from zc.vault.core import Manifest
+    >>> from zc.vault import interfaces
+    >>> v = Vault()
+    >>> app['vault'] = v
+    >>> i = Inventory(vault=v)
+
+Initially, manifests do not have a log.  You must fire an
+ObjectCreatedEvent to get it installed.  However, creating a manifest
+with an inventory accomplishes this for you transparently, so you
+actually don't have to do anything.
+
+    >>> import zc.objectlog.interfaces
+    >>> man = i.manifest
+    >>> zc.objectlog.interfaces.ILogging.providedBy(man)
+    True
+
+This also makes a log entry recording the creation.
+
+    >>> len(man.log)
+    1
+    >>> man.log[0].summary
+    u'Created'
+
+In addition to the basic log entry data (summary, description, time
+stamp, principals), the log record keeps track of the update sources
+and update bases, for manifest-based (non-collection) updates.  For
+now, these are None.
+
+    >>> man.log[0].record.update_source_intid # None
+    >>> man.log[0].record.update_base_intid # None
+
+Merely accessing the inventory contents creates a top-level relationship, and
+this is logged.
+
+    >>> import persistent
+    >>> from zope.app.container.contained import Contained
+    >>> from zc.copyversion.versioning import Versioning
+    >>> class Demo(persistent.Persistent, Contained, Versioning):
+    ...     def __repr__(self):
+    ...         return "<%s %r>" % (self.__class__.__name__, self.__name__)
+    ...
+    >>> zc.objectlog.interfaces.ILogging.providedBy(i.contents.relationship)
+    True
+    >>> len(i.contents.relationship.log)
+    3
+    >>> i.contents.relationship.log[0].summary
+    u'Created'
+    >>> i.contents.relationship.log[1].summary
+    u'Added as local relationship'
+    >>> i.contents.relationship.log[2].summary
+    u'Selected'
+
+If we add an item, the top relationship will have another log entry and the
+new relationship will also have a log.
+
+    >>> app['d1'] = Demo()
+    >>> i.contents[u'donald'] = app['d1']
+    >>> len(i.contents.relationship.log)
+    4
+    >>> i.contents.relationship.log[3].summary
+    u'Child added'
+    >>> rel = i.contents('donald').relationship
+    >>> len(rel.log)
+    3
+    >>> rel.log[0].summary
+    u'Created'
+    >>> rel.log[1].summary
+    u'Added as local relationship'
+    >>> rel.log[2].summary
+    u'Selected'
+    >>> i.contents.relationship.log[2].summary
+    u'Selected'
+
+The log's record keeps track of the items and the object intid.
+
+    >>> from zope.app.intid.interfaces import IIntIds
+    >>> from zope import component, interface
+    >>> intids = component.getUtility(IIntIds)
+    >>> intids.getObject(rel.log[-1].record.object_intid) is app['d1']
+    True
+    >>> i.contents.relationship.log[-1].record.object_intid # None
+    >>> rel.log[-1].record.items
+    ()
+    >>> [key for key, val in i.contents.relationship.log[-1].record.items]
+    [u'donald']
+    >>> i.manifest.get(
+    ...     i.contents.relationship.log[-1].record.items[0][1]
+    ...     ).object is app['d1']
+    True
+
+Let's add a few more items, then reorder: the reordering should generate a new
+log message.
+
+    >>> app['e1'] = Demo()
+    >>> app['f1'] = Demo()
+    >>> i.contents[u'edward'] = app['e1']
+    >>> i.contents[u'fred'] = app['f1']
+    >>> len(i.contents.relationship.log)
+    6
+    >>> [key for key, val in i.contents.relationship.log[-1].record.items]
+    [u'donald', u'edward', u'fred']
+    >>> i.contents.updateOrder([u'donald', u'edward', u'fred']) # no change
+    >>> len(i.contents.relationship.log)
+    6
+    >>> i.contents.updateOrder([u'donald', u'fred', u'edward'])
+    >>> len(i.contents.relationship.log)
+    7
+    >>> i.contents.relationship.log[-1].summary
+    u'Child order changed'
+
+Now we'll delete one: another new message.
+
+    >>> del i.contents['donald']
+    >>> len(i.contents.relationship.log)
+    8
+    >>> i.contents.relationship.log[-1].summary
+    u'Child removed'
+
+When we commit, the manifest log gets a new entry.
+
+    >>> len(i.manifest.log)
+    1
+    >>> i.iterOrphanConflicts().next().resolveOrphanConflict()
+    >>> v.commit(i)
+    >>> len(i.manifest.log)
+    2
+    >>> i.manifest.log[-1].summary
+    u'Committed'
+
+The individual relationships record when they were versioned--at the same time
+as the commit.
+
+    >>> i.contents.relationship.log[-1].summary
+    u'Versioned'
+
+Other logs occur during updates, so we need to create a situation that needs
+an update, and that generates a suggestion.
+
+    >>> i = Inventory(vault=v, mutable=True)
+    >>> concurrent = Inventory(vault=v, mutable=True)
+    >>> concurrent.contents('fred')['denise'] = app['d1']
+    >>> concurrent.contents['gary'] = app['g1'] = Demo()
+    >>> v.commit(concurrent)
+
+    >>> from zope import event
+    >>> import zope.lifecycleevent
+    >>> from zc.vault import interfaces
+    >>> from zc.vault.core import Relationship
+    >>> @component.adapter(interfaces.IVault)
+    ... @interface.implementer(interfaces.IConflictResolver)
+    ... def factory(vault):
+    ...     def resolver(manifest, local, updated, base):
+    ...         if local.object is not base.object:
+    ...             if updated.object is base.object:
+    ...                 object = local.object
+    ...             else:
+    ...                 return
+    ...         else:
+    ...             object = updated.object
+    ...         if local.containment != base.containment:
+    ...             if updated.containment != base.containment:
+    ...                 return
+    ...             else:
+    ...                 containment = local.containment
+    ...         else:
+    ...             containment = updated.containment
+    ...         suggested = Relationship(local.token, object, containment)
+    ...         suggested.__parent__ = manifest
+    ...         event.notify(zope.lifecycleevent.ObjectCreatedEvent(
+    ...             suggested))
+    ...         manifest.addSuggested(suggested)
+    ...         manifest.select(suggested)
+    ...         manifest.resolveUpdateConflict(local.token)
+    ...     return resolver
+    ...
+    >>> component.provideAdapter(factory)
+
+While we are at it, changing an object generates a log entry.
+
+    >>> i.contents('fred').type == interfaces.BASE
+    True
+    >>> i.contents['fred'] = app['f2'] = Demo()
+    >>> i.contents('fred').type == interfaces.LOCAL
+    True
+    >>> len(i.contents('fred').relationship.log)
+    4
+    >>> i.contents('fred').relationship.log[-1].summary
+    u'Object changed'
+
+When we begin the update, we again get a log.
+
+    >>> i.beginUpdate()
+    >>> i.manifest.log[-1].summary
+    u'Update begun'
+
+So, the manifest keeps track of the update.  If it is based on a manifest, it
+also keeps track of which manifest was merged, and what the base was, using
+intids.
+
+    >>> (intids.getObject(i.manifest.log[-1].record.update_source_intid) is
+    ...  v.manifest)
+    True
+    >>> (intids.getObject(i.manifest.log[-1].record.update_base_intid) is
+    ...  v[-2])
+    True
+
+The suggested relationship has a log entry describing its entry into the
+system.
+
+    >>> i.contents('fred').type == interfaces.SUGGESTED
+    True
+    >>> i.contents('fred').relationship.log[-2].summary
+    u'Added as suggested relationship'
+    >>> i.contents('fred').relationship.log[-1].summary
+    u'Selected'
+
+If we select another relationship, the previously selected one gets a
+'Deselected' log entry.
+
+    >>> suggested = i.contents('fred')
+    >>> i.contents('fred').local_item.select()
+    >>> i.contents('fred').type == interfaces.LOCAL
+    True
+    >>> suggested.relationship.log[-1].summary
+    u'Deselected'
+    >>> i.contents('fred').relationship.log[-1].summary
+    u'Selected'
+
+If the update is aborted, it is logged.
+
+    >>> i.abortUpdate()
+    >>> i.manifest.log[-1].summary
+    u'Update aborted'
+    >>> i.manifest.log[-1].record.update_source_intid # None
+    >>> i.manifest.log[-1].record.update_base_intid # None
+
+Now we'll restart the update, add a modified relationship, and complete the
+update.
+
+    >>> i.beginUpdate()
+    >>> i.contents['edward'] = app['e2'] = Demo()
+    >>> i.contents('edward').relationship.log[-3].summary
+    u'Added as modified relationship'
+    >>> i.contents('edward').relationship.log[-2].summary
+    u'Selected'
+    >>> i.contents('edward').relationship.log[-1].summary
+    u'Object changed'
+    >>> i.completeUpdate()
+    >>> i.manifest.log[-1].summary
+    u'Update completed'
+
+Note that the edward and fred relationships that were modified and suggested,
+respectively, are now logged as local.
+
+    >>> i.contents('edward').relationship.log[-1].summary
+    u'Added as local relationship'
+    >>> i.contents('fred').relationship.log[-1].summary
+    u'Added as local relationship'
+
+The last log occurs when a manifest changes vaults.  Manifest log records keep
+track of vaults, so the changes are available in history.
+
+    >>> intids.getObject(i.manifest.log[-1].record.vault_intid) is v
+    True
+
+Now we'll make a branch and move i over to the branch.  Notice that we make a
+manifest ourselves here, not relying on the Inventory class, so we have to fire
+the creation event ourselves.
+
+    >>> branch = app['branch'] = Vault(v.intids)
+    >>> man = Manifest(v.manifest)
+    >>> event.notify(zope.lifecycleevent.ObjectCreatedEvent(man))
+    >>> branch.commit(man)
+    >>> branch.manifest.log[-2].summary
+    u'Vault changed'
+    >>> intids.getObject(branch.manifest.log[-2].record.vault_intid) is branch
+    True
+    >>> intids.getObject(branch.manifest.log[-3].record.vault_intid) is v
+    True
+    >>> i.vault = branch
+    >>> branch.manifest.log[-2].summary
+    u'Vault changed'
+    >>> i.beginUpdate()
+    >>> branch.commit(i)
+    >>> import transaction
+    >>> transaction.commit()


Property changes on: zc.vault/trunk/src/zc/vault/objectlog.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.vault/trunk/src/zc/vault/objectlog.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.zcml	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.zcml	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="zc.vault">
+
+  <subscriber handler=".objectlog.createManifestLog" />
+
+  <subscriber handler=".objectlog.createRelationshipLog" />
+
+  <subscriber handler=".objectlog.logRemoval" />
+
+  <subscriber handler=".objectlog.logAddition" />
+
+  <subscriber handler=".objectlog.logOrderChanged" />
+
+  <subscriber handler=".objectlog.logCommit" />
+
+  <subscriber handler=".objectlog.logVersioning" />
+
+  <subscriber handler=".objectlog.logNewLocal" />
+
+  <subscriber handler=".objectlog.logNewModified" />
+
+  <subscriber handler=".objectlog.logNewSuggested" />
+
+  <subscriber handler=".objectlog.logUpdateBegun" />
+
+  <subscriber handler=".objectlog.logUpdateAborted" />
+
+  <subscriber handler=".objectlog.logUpdateCompleted" />
+
+  <subscriber handler=".objectlog.logVaultChanged" />
+
+  <subscriber handler=".objectlog.logSelection" />
+
+  <subscriber handler=".objectlog.logDeselection" />
+
+  <subscriber handler=".objectlog.logObjectChanged" />
+
+  <adapter factory=".objectlog.ManifestChangesetAdapter" />
+
+  <adapter factory=".objectlog.RelationshipChangesetAdapter" />
+
+  <class class=".core.Manifest">
+    <require permission="zope.View"
+      attributes="log"/>
+  </class>
+
+  <class class=".core.Relationship">
+    <require permission="zope.View"
+      attributes="log"/>
+  </class>
+
+
+</configure>

Added: zc.vault/trunk/src/zc/vault/tests.py
===================================================================
--- zc.vault/trunk/src/zc/vault/tests.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/tests.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Relationship tests
+
+$Id$
+"""
+import unittest
+import re
+from zope.testing import doctest, renormalizing
+import zope.testing.module
+
+# these are used by setup
+import transaction
+import persistent
+from persistent.interfaces import IPersistent
+from ZODB.interfaces import IConnection
+from ZODB.tests.util import DB
+
+from zope import component, interface, event
+import zope.component.interfaces
+from zope.app.testing import placelesssetup
+from zope.app.keyreference.persistent import (
+    KeyReferenceToPersistent, connectionOfPersistent)
+from zope.app.folder import rootFolder
+from zope.app.component.site import LocalSiteManager
+from zope.app.intid import IntIds
+from zope.app.intid.interfaces import IIntIds
+import zope.app.component.interfaces.registration
+import zope.annotation.interfaces
+import zope.annotation.attribute
+import zope.app.component.hooks
+
+def setUp(test):
+    placelesssetup.setUp()
+    events = test.globs['events'] = []
+    event.subscribers.append(events.append)
+    component.provideAdapter(KeyReferenceToPersistent, adapts=(IPersistent,))
+    import zope.app.component.site
+    import zope.component.interfaces
+    import zope.location.interfaces
+    component.provideAdapter(
+        zope.app.component.site.SiteManagerAdapter,
+        (zope.location.interfaces.ILocation,),
+        zope.component.interfaces.IComponentLookup)
+    component.provideAdapter(
+        connectionOfPersistent,
+        adapts=(IPersistent,),
+        provides=IConnection)
+    test.globs['db'] = db = DB()
+    test.globs['conn'] = conn = db.open()
+    test.globs['root'] = root = conn.root()
+    test.globs['app'] = app = root['app'] = rootFolder()
+    app.setSiteManager(LocalSiteManager(app))
+    transaction.commit()
+    app = test.globs['app']
+    sm = app.getSiteManager()
+    sm['intids'] = IntIds()
+    registry = zope.component.interfaces.IComponentRegistry(sm)
+    registry.registerUtility(sm['intids'], IIntIds)
+    transaction.commit()
+    zope.app.component.hooks.setSite(app)
+    zope.app.component.hooks.setHooks()
+    zope.testing.module.setUp(test, 'zc.vault.README')
+
+def tearDown(test):
+    import transaction
+    transaction.abort()
+    zope.testing.module.tearDown(test)
+    zope.app.component.hooks.resetHooks()
+    zope.app.component.hooks.setSite()
+    events = test.globs.pop('events')
+    assert event.subscribers.pop().__self__ is events
+    del events[:] # being paranoid
+    transaction.abort()
+    test.globs['db'].close()
+    placelesssetup.tearDown()
+
+def objectlogSetUp(test):
+    setUp(test)
+    from zope.app.container.contained import NameChooser
+    from zope.app.container.interfaces import IWriteContainer
+    component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+    from zc.vault import objectlog
+    component.provideAdapter(objectlog.ManifestChangesetAdapter)
+    component.provideAdapter(objectlog.RelationshipChangesetAdapter)
+    component.provideHandler(objectlog.createManifestLog)
+    component.provideHandler(objectlog.createRelationshipLog)
+    component.provideHandler(objectlog.logRemoval)
+    component.provideHandler(objectlog.logAddition)
+    component.provideHandler(objectlog.logOrderChanged)
+    component.provideHandler(objectlog.logCommit)
+    component.provideHandler(objectlog.logVersioning)
+    component.provideHandler(objectlog.logNewLocal)
+    component.provideHandler(objectlog.logNewSuggested)
+    component.provideHandler(objectlog.logNewModified)
+    component.provideHandler(objectlog.logUpdateBegun)
+    component.provideHandler(objectlog.logUpdateAborted)
+    component.provideHandler(objectlog.logUpdateCompleted)
+    component.provideHandler(objectlog.logVaultChanged)
+    component.provideHandler(objectlog.logSelection)
+    component.provideHandler(objectlog.logDeselection)
+    component.provideHandler(objectlog.logObjectChanged)
+
+def catalogSetUp(test):
+    setUp(test)
+    from zope.app.container.contained import NameChooser
+    from zope.app.container.interfaces import IWriteContainer
+    component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+    from zc.vault import catalog
+    component.provideHandler(catalog.makeReferences)
+    component.provideHandler(catalog.updateCompleted)
+    component.provideHandler(catalog.updateAborted)
+    component.provideHandler(catalog.manifestCreated)
+    component.provideHandler(catalog.relationshipSelected)
+    component.provideHandler(catalog.relationshipDeselected)
+    component.provideHandler(catalog.objectChanged)
+
+def traversalSetUp(test):
+    setUp(test)
+    import zc.copyversion.copier
+    component.provideAdapter(zc.copyversion.copier.location_copyfactory)
+    component.provideAdapter(zc.copyversion.copier.versiondata_copyfactory)
+    import zope.copypastemove
+    component.provideAdapter(zope.copypastemove.ObjectCopier)
+    import zc.shortcut.adapters
+    component.provideAdapter(zc.shortcut.adapters.ObjectLinkerAdapter)
+
+def test_suite():
+    checker = renormalizing.RENormalizing([
+        (re.compile(r'^\d+$', re.M), '1234567'),
+        ])
+
+    tests = (
+        doctest.DocFileSuite(
+            'README.txt',
+            setUp=setUp, tearDown=tearDown, checker=checker),
+        doctest.DocFileSuite(
+            'versions.txt',
+            setUp=setUp, tearDown=tearDown),
+        # separate this out so this only runs if pertinent modules available?
+        doctest.DocFileSuite(
+            'catalog.txt',
+            setUp=catalogSetUp, tearDown=tearDown),
+        )
+
+    try:
+        import zc.objectlog
+        tests += (
+            doctest.DocFileSuite(
+                'objectlog.txt',
+                setUp=objectlogSetUp, tearDown=tearDown),)
+    except ImportError:
+        # no zc.objectlog available, so don't try to test integration with it
+        pass
+
+    try:
+        import zc.shortcut
+        tests += (doctest.DocFileSuite(
+                    'traversal.txt', 
+                    setUp=traversalSetUp, tearDown=tearDown),)
+    except ImportError:
+        pass
+
+    return unittest.TestSuite(tests)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: zc.vault/trunk/src/zc/vault/traversal.py
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,324 @@
+import zope.component
+import zope.interface
+import zope.interface.common.mapping
+import zope.location
+import zope.proxy
+import zope.copypastemove.interfaces
+import zope.app.container.interfaces
+import zope.app.container.constraints
+
+import zc.copyversion.copier
+import zc.copyversion.interfaces
+import zc.shortcut
+import zc.shortcut.proxy
+import zc.shortcut.interfaces
+
+# interfaces
+
+class IInventoryItemAware(zope.interface.Interface):
+    _z_inventory_node = zope.interface.Attribute(
+        """a zc.vault.interfaces.IContained (an IInventoryItem or an
+        IInventoryContents.""")
+
+class IInventoryItemAwareFactory(zope.interface.Interface):
+    def __call__(item, parent, name):
+        """returns an object that provudes IInventoryItemAware"""
+
+class IProxy(
+    IInventoryItemAware, zc.shortcut.interfaces.ITraversalProxy):
+    """these proxies have _z_inventory_node, __traversed_parent__, and
+    __traversed_name__"""
+
+class IData(zope.interface.Interface):
+    """A marker interface that indicates that this object should be adapted
+    to IInventoryItemAwareFactory, and then the factory should be called with
+    the object's item, its parent, and its name within the parent."""
+
+# the proxy
+
+class Proxy(zc.shortcut.proxy.ProxyBase):
+    zc.shortcut.proxy.implements(IProxy)
+    __slots__ = '_z_inventory_node',
+
+    def __new__(self, ob, parent, name, item):
+        return zc.shortcut.proxy.ProxyBase.__new__(self, ob, parent, name)
+
+    def __init__(self, ob, parent, name, item):
+        zc.shortcut.proxy.ProxyBase.__init__(self, ob, parent, name)
+        self._z_inventory_node = item
+
+# the containers
+
+class ReadContainer(zope.location.Location):
+    zope.interface.classProvides(IInventoryItemAwareFactory)
+    zope.interface.implements(
+        IInventoryItemAware, zope.interface.common.mapping.IEnumerableMapping)
+
+    def __init__(self, item, parent=None, name=None):
+        self.__parent__ = parent
+        self.__name__ = name
+        self._z_inventory_node = item
+
+    def __len__(self):
+        return len(self._z_inventory_node)
+
+    def __iter__(self):
+        return iter(self._z_inventory_node)
+
+    def __contains__(self, key):
+        return key in self._z_inventory_node
+
+    def __getitem__(self, key):
+        item = self._z_inventory_node(key)
+        if item.object is None:
+            factory = zope.component.getUtility(IInventoryItemAwareFactory)
+            return factory(item, self, key)
+        elif IData.providedBy(item.object):
+            factory = IInventoryItemAwareFactory(item.object)
+            return factory(item, self, key)
+        else:
+            return Proxy(item.object, self, key, item)
+
+    def keys(self):
+        return self._z_inventory_node.keys()
+
+    def values(self):
+        return [self[key] for key in self]
+
+    def items(self):
+        return [(key, self[key]) for key in self]
+
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError:
+            return default
+
+    def __getstate__(self):
+        raise RuntimeError('This should not be persisted.')
+
+class Container(ReadContainer):
+    zope.interface.implements(zope.interface.common.mapping.IMapping)
+
+    def __setitem__(self, key, value):
+        self._z_inventory_node[key] = value
+
+    def __delitem__(self, key):
+        del self._z_inventory_node[key]
+
+    def updateOrder(self, order):
+        self._z_inventory_node.updateOrder(order)
+
+# the movers and shakers
+
+# Unfortunately we have to duplicate the standard checkObject so we can
+# weed out the IContainer check, which is not pertinent here.
+def checkObject(container, name, object):
+    """Check containment constraints for an object and container
+    """
+
+    # check __setitem__ precondition
+    containerProvided = zope.interface.providedBy(container)
+    __setitem__ = containerProvided.get('__setitem__')
+    if __setitem__ is not None:
+        precondition = __setitem__.queryTaggedValue('precondition')
+        if precondition is not None:
+            precondition(container, name, object)
+
+    # check the constraint on __parent__
+    __parent__ = zope.interface.providedBy(object).get('__parent__')
+    if __parent__ is not None:
+        try:
+            validate = __parent__.validate
+        except AttributeError:
+            pass
+        else:
+            validate(container)
+
+def isInventoryObject(obj):
+    return obj._z_inventory_node.object is zope.proxy.removeAllProxies(obj)
+
+class ObjectMover(object):
+    """can only move objects within and among manifests; moving elsewhere
+    has reparenting connotations that are inappropriate, since inventory
+    membership and parentage are unrelated."""
+    zope.interface.implements(zope.copypastemove.interfaces.IObjectMover)
+    zope.component.adapts(IInventoryItemAware)
+
+    def __init__(self, context):
+        self.context = context
+        self.__parent__ = context
+
+    def moveTo(self, target, new_name=None):
+        if not IInventoryItemAware.providedBy(target):
+            raise ValueError('target must be IInventoryItemAware')
+        node = self.context._z_inventory_node
+        if new_name is None:
+            new_name = node.name
+        if node == target._z_inventory_node and new_name == node.name:
+            return # noop
+        manifest = node.inventory.manifest
+        if manifest._z_versioned:
+            raise zc.copyversion.interfaces.VersionedError(manifest)
+        if target._z_inventory_node.inventory.manifest._z_versioned:
+            raise zc.copyversion.interfaces.VersionedError(
+                target._z_inventory_node.inventory.manifest)
+        checkObject(target, new_name, self.context)
+        chooser = zope.app.container.interfaces.INameChooser(target)
+        new_name = chooser.chooseName(new_name, node.object)
+        node.moveTo(target._z_inventory_node, new_name)
+        return new_name
+
+    def moveable(self):
+        manifest = self.context._z_inventory_node.inventory.manifest
+        return not manifest._z_versioned
+
+    def moveableTo(self, target, new_name=None):
+        node = self.context._z_inventory_node
+        manifest = node.inventory.manifest
+        if (not manifest._z_versioned and
+            IInventoryItemAware.providedBy(target) and
+            not target._z_inventory_node.inventory.manifest._z_versioned):
+            if new_name is None:
+                new_name = node.name
+            try:
+                checkObject(target, new_name, self.context)
+            except zope.interface.Invalid:
+                pass
+            else:
+                return True
+        return False
+
+class ObjectCopier(object):
+    """Generally, make new copies of objects.
+    
+    If target is from a non-versioned manifest, use
+    copyTo and then copy all of the non-None data objects in the tree.
+
+    otherwise if the object is a proxied leaf node, do a normal copy; otherwise
+    puke (can't copy a vault-specific object out of a vault).
+    """
+    zope.interface.implements(zope.copypastemove.interfaces.IObjectCopier)
+    zope.component.adapts(IInventoryItemAware)
+
+    def __init__(self, context):
+        self.context = context
+        self.__parent__ = context
+
+    def copyTo(self, target, new_name=None):
+        if IInventoryItemAware.providedBy(target):
+            if target._z_inventory_node.inventory.manifest._z_versioned:
+                raise zc.copyversion.interfaces.VersionedError(
+                    target._z_inventory_node.inventory.manifest)
+        else:
+            if not isInventoryObject(self.context):
+                raise ValueError # TODO better error
+            return zope.copypastemove.interfaces.IObjectCopier(
+                zc.shortcut.proxy.removeProxy(self.context)).copyTo(
+                    target, new_name)
+        node = self.context._z_inventory_node
+        manifest = node.inventory.manifest
+        if new_name is None:
+            new_name = node.name
+        checkObject(target, new_name, self.context)
+        chooser = zope.app.container.interfaces.INameChooser(target)
+        new_name = chooser.chooseName(new_name, node.object)
+        node.copyTo(target._z_inventory_node, new_name)
+        new_node = zope.proxy.removeAllProxies(
+            target._z_inventory_node(new_name))
+        stack = [(lambda x: new_node, iter(('',)))]
+        while stack:
+            node, i = stack[-1]
+            try:
+                key = i.next()
+            except StopIteration:
+                stack.pop()
+            else:
+                next = node(key)
+                original = next.object
+                next.object = zc.copyversion.copier.copy(original)
+                stack.append((next, iter(next)))
+        return new_name
+
+    def copyable(self):
+        return True
+
+    def copyableTo(self, target, new_name=None):
+        if not self.copyable():
+            return False
+        if IInventoryItemAware.providedBy(target):
+            if target._z_inventory_node.inventory.manifest._z_versioned:
+                return False
+            check = checkObject
+        else:
+            if not isInventoryObject(self.context):
+                return False
+            check = zope.app.container.constraints.checkObject
+        node = self.context._z_inventory_node
+        manifest = node.inventory.manifest
+        if new_name is None:
+            new_name = node.name
+        try:
+            check(target, new_name, self.context)
+        except zope.interface.Invalid:
+            return False
+        else:
+            return True
+
+class ObjectLinker(object):
+    zope.component.adapts(IInventoryItemAware)
+    zope.interface.implements(zc.shortcut.interfaces.IObjectLinker)
+
+    def __init__(self, context):
+        self.context = context
+        self.__parent__ = context
+
+    def linkTo(self, target, new_name=None):
+        if IInventoryItemAware.providedBy(target):
+            if target._z_inventory_node.inventory.manifest._z_versioned:
+                raise zc.copyversion.interfaces.VersionedError(
+                    target._z_inventory_node.inventory.manifest)
+        else:
+            if not isInventoryObject(self.context):
+                raise ValueError # TODO better error
+            return zc.shortcut.interfaces.IObjectLinker(
+                zc.shortcut.proxy.removeProxy(self.context)).linkTo(
+                    target, new_name)
+        node = self.context._z_inventory_node
+        manifest = node.inventory.manifest
+        if new_name is None:
+            new_name = node.name
+        checkObject(target, new_name, self.context)
+        chooser = zope.app.container.interfaces.INameChooser(target)
+        new_name = chooser.chooseName(new_name, node.object)
+        node.copyTo(target._z_inventory_node, new_name)
+        return new_name
+
+    def linkable(self):
+        return True
+
+    def linkableTo(self, target, new_name=None):
+        if IInventoryItemAware.providedBy(target):
+            if target._z_inventory_node.inventory.manifest._z_versioned:
+                return False
+            obj = self.context
+            check = checkObject
+        else:
+            if not isInventoryObject(self.context):
+                return False
+            obj = self._createShortcut(
+                zc.shortcut.proxy.removeProxy(self.context))
+            check = zope.app.container.constraints.checkObject
+        node = self.context._z_inventory_node
+        manifest = node.inventory.manifest
+        if new_name is None:
+            new_name = node.name
+        try:
+            check(target, new_name, obj)
+        except zope.interface.Invalid:
+            return False
+        else:
+            return True
+
+    def _createShortcut(self, target):
+        return zc.shortcut.Shortcut(target)

Added: zc.vault/trunk/src/zc/vault/traversal.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.txt	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.txt	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,369 @@
+The vault package includes an add-on that helps build traversable elements.
+It has two main components: a containerish wrapper for inventory items, and
+optional proxies for returned objects.
+
+The container is intended to be a base class, typically exposing various
+elements of the data object.  It's a mapping API, similar to a stripped down
+version of the inventory contents.
+
+    >>> import zc.vault.vault
+    >>> import zc.vault.traversal
+    >>> v = app['vault'] = zc.vault.vault.Vault()
+    >>> i = app['i'] = zc.vault.vault.Inventory(vault=v)
+    >>> container = zc.vault.traversal.Container(i.contents)
+    >>> list(container.keys())
+    []
+    >>> list(container.values())
+    []
+    >>> container['foo']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'foo'
+    >>> container.get('foo') # None
+    >>> list(container)
+    []
+
+These containers have a special attribute: _z_inventory_node, the inventory
+node that backs the container.
+
+    >>> container._z_inventory_node == i.contents
+    True
+
+This means that they implement IInventoryItemAware.
+
+    >>> zc.vault.traversal.IInventoryItemAware.providedBy(container)
+    True
+
+If you add a simple data object, it will be returned from the container wrapped
+with a proxy.
+
+    >>> import zope.location
+    >>> import persistent
+    >>> import zope.annotation.interfaces
+    >>> import zope.interface
+    >>> import zc.copyversion.versioning
+    >>> import zope.app.container.interfaces
+    >>> class Demo(persistent.Persistent, zope.location.Location,
+    ...            zc.copyversion.versioning.Versioning):
+    ...     zope.interface.implements(
+    ...         zope.annotation.interfaces.IAttributeAnnotatable,
+    ...         zope.app.container.interfaces.IContained)
+    ...     _counter = 0
+    ...     def __init__(self):
+    ...         self.id = self._counter
+    ...         self.__class__._counter += 1
+    ...     def __repr__(self):
+    ...         return "<%s %d>" % (self.__class__.__name__, self.id)
+    ...     def __call__(self):
+    ...         return "I am number %d" % (self.id,)
+    ...
+    >>> d = app['demo'] = Demo()
+    >>> d
+    <Demo 0>
+    >>> d()
+    'I am number 0'
+    >>> container['d'] = d
+    >>> container['d']
+    <Demo 0>
+    >>> container['d']()
+    'I am number 0'
+    >>> container['d'] is d
+    False
+    >>> type(d) # doctest: +ELLIPSIS
+    <class '...Demo'>
+    >>> type(container['d'])
+    <class 'zc.vault.traversal.Proxy'>
+
+This proxy is a zc.shortcut proxy subclass (the pertinent code in
+zc.shortcut should probably be moved out to zc.traversed).  This means
+it has references to the traversed parent and the traversed name
+(without affecting __parent__ and __name__).
+
+    >>> container['d'].__traversed_parent__ is container
+    True
+    >>> container['d'].__traversed_name__
+    'd'
+    >>> container['d'].__name__
+    u'demo'
+    >>> container['d'].__parent__ is app
+    True
+
+This makes some of the standard adapters provided by zc.shortcut work, such as
+the one for breadcrumbs and traversedURL.
+
+It also has a reference to the inventory item that represents this node in the
+tree.
+
+    >>> container['d']._z_inventory_node.object is d
+    True
+
+This makes the proxied object provide IInventoryItemAware.
+
+    >>> zc.vault.traversal.IInventoryItemAware.providedBy(container['d'])
+    True
+
+Finally, it provides a special zc.vault.traversal.IProxy interface (as well as
+the zc.shortcut.interfaces.ITraversalProxy interface).
+
+    >>> zc.vault.traversal.IProxy.providedBy(container['d'])
+    True
+    >>> zc.shortcut.interfaces.ITraversalProxy.providedBy(container['d'])
+    True
+
+The inventory item reference and the special interface make it possible to
+support custom object movers, custom object copiers, and custom traversers, as
+desired.  We will observe some of those later.
+
+The containers have two special cases: None, and objects that implement 
+zc.vault.traversal.IData.  None values cause the container to look up a utility
+for zc.vault.traversal.IInventoryItemAwareFactory and call it with
+(inventory item, parent, name) and return the result.  The 
+zc.vault.traversal.Container itself can be used for this purpose.
+
+    >>> import zope.component
+    >>> zope.component.provideUtility(
+    ...     zc.vault.traversal.Container,
+    ...     provides=zc.vault.traversal.IInventoryItemAwareFactory)
+    >>> container['none'] = None
+    >>> isinstance(container['none'], zc.vault.traversal.Container)
+    True
+    >>> container['none'].__name__
+    'none'
+    >>> container['none'].__parent__ is container
+    True
+    >>> list(container['none'].keys())
+    []
+    >>> container['none']['obj'] = Demo()
+    >>> container['none']['obj']
+    <Demo 1>
+    >>> (container['none']['obj'].__traversed_parent__._z_inventory_node ==
+    ...  container['none']._z_inventory_node)
+    True
+
+Objects that implement zc.vault.traversal.IData are adapted to 
+zc.vault.traversal.IInventoryItemAwareFactory, and then the result of calling
+the factory with (inventory item, parent, name) is returned.
+
+    >>> class DemoData(Demo):
+    ...     zope.interface.implements(zc.vault.traversal.IData)
+    ...     def __init__(self, title):
+    ...         self.title = title
+    ...         super(DemoData, self).__init__()
+    ...
+    >>> class DemoFactory(zc.vault.traversal.Container):
+    ...     @property
+    ...     def title(self):
+    ...         return self._z_inventory_node.object.title
+    ...
+    >>> @zope.component.adapter(DemoData)
+    ... @zope.interface.implementer(
+    ...     zc.vault.traversal.IInventoryItemAwareFactory)
+    ... def demofactory(obj):
+    ...     return DemoFactory
+    ...
+    >>> zope.component.provideAdapter(demofactory)
+    >>> container['none']['data'] = DemoData('Demosthenes')
+    >>> container['none']['data'].title
+    'Demosthenes'
+    >>> list(container['none']['data'].keys())
+    []
+    >>> container['none']['data']._z_inventory_node.object
+    <DemoData 2>
+
+This approach is more powerful than using None even for simple containers: for
+instance, you can put attribute annotations on the Demo object, while with None
+there's no place to store them.
+
+CopyPasteMove
+=============
+
+The module provides a mover, copier, and linker to work with standard and
+shortcut-extended container views.  They adapt IInventoryItemAware, so can
+be used for objects wrapped with a zc.vault.traversal.Proxy,
+zc.vault.traversal.Containers, and any object that implements
+IInventoryItemAware.
+
+    >>> zope.component.provideAdapter(zc.vault.traversal.ObjectMover)
+    >>> zope.component.provideAdapter(zc.vault.traversal.ObjectCopier)
+    >>> zope.component.provideAdapter(zc.vault.traversal.ObjectLinker)
+
+ObjectMover
+-----------
+
+The mover will only move objects within and among manifests.
+
+    >>> import zope.copypastemove.interfaces
+    >>> mover = zope.copypastemove.interfaces.IObjectMover(container['d'])
+    >>> mover.moveable()
+    True
+    >>> mover.moveableTo(container['none']['data'])
+    True
+
+We need a name chooser to actually move.  We'll register the default one, then
+move.
+
+    >>> import zope.app.container.contained
+    >>> zope.component.provideAdapter(
+    ...     zope.app.container.contained.NameChooser,
+    ...     adapts=(zope.interface.Interface,))
+    >>> mover.moveTo(container['none']['data'])
+    'd'
+    >>> container['none']['data']['d']
+    <Demo 0>
+
+Insane moves won't work.
+
+    >>> mover = zope.copypastemove.interfaces.IObjectMover(container['none'])
+    >>> mover.moveTo(container['none']['data'])
+    Traceback (most recent call last):
+    ...
+    ValueError: May not move item to within itself
+
+When you move within an inventory, the move is remembered so that the previous
+location in an older inventory can be found.  You can also move between
+inventories, which really just deletes from one and adds to the other without
+history.
+
+    >>> v2 = app['vault2'] = zc.vault.vault.Vault()
+    >>> i2 = app['i2'] = zc.vault.vault.Inventory(vault=v2)
+    >>> container2 = zc.vault.traversal.Container(i2.contents)
+    >>> mover.moveTo(container2)
+    'none'
+    >>> list(container)
+    []
+    >>> list(container2)
+    ['none']
+    >>> container2['none']['data']['d']
+    <Demo 0>
+
+You can't move outside a vault: that has weird __parent__ issues that we claim
+make it insane.
+
+    >>> mover = zope.copypastemove.interfaces.IObjectMover(container2['none'])
+    >>> mover.moveableTo(app)
+    False
+    >>> mover.moveTo(app)
+    Traceback (most recent call last):
+    ...
+    ValueError: target must be IInventoryItemAware
+
+ObjectCopier
+------------
+
+The copier is similar, but supports more cases: sometimes you can copy outside
+of an inventory, too.
+
+    >>> copier = zope.copypastemove.interfaces.IObjectCopier(container2['none'])
+    >>> copier.copyable()
+    True
+    >>> copier.copyableTo(container2)
+    True
+
+Well, you can't for objects that aren't the proxied version of the
+actual object in the inventory, actually.
+
+    >>> copier.copyableTo(app)
+    False
+    >>> copier.copyTo(app)
+    Traceback (most recent call last):
+    ...
+    ValueError
+
+But they can be copied within the same manifest...
+
+    >>> copier.copyTo(container2)
+    u'none-2'
+    >>> sorted(container2)
+    ['none', u'none-2']
+    >>> container2['none']['data']['d']
+    <Demo 0>
+    >>> container2['none-2']['data']['d']
+    <Demo 0>
+    >>> import zc.shortcut.proxy
+    >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+    ...  zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']))
+    False
+
+...and across manifests.
+
+    >>> copier.copyTo(container)
+    'none'
+    >>> container['none']['data']['d']
+    <Demo 0>
+    >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+    ...  zc.shortcut.proxy.removeProxy(container['none']['data']['d']))
+    False
+    
+If the copier is for an object that is the proxied object in the inventory,
+then it may in fact copy outside of a manifest.
+
+    >>> copier = zope.copypastemove.interfaces.IObjectCopier(
+    ...     container['none']['data']['d'])
+    >>> copier.copyableTo(app)
+    True
+    >>> copier.copyTo(app)
+    u'Demo-3'
+    >>> (zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']) is
+    ...  app[u'Demo-3'])
+    False
+
+ObjectLinker
+------------
+
+The last adapter is a linker, as defined in zc.shortcut.  It puts the same
+object in inventories, or creates shortcuts out of inventories.
+
+    >>> import zc.shortcut.interfaces
+    >>> linker = zc.shortcut.interfaces.IObjectLinker(container2['none'])
+    >>> linker.linkable()
+    True
+    >>> linker.linkableTo(container2)
+    True
+
+Well, you can't create shortcuts for objects that aren't the proxied
+version of the actual object in the inventory, actually.
+
+    >>> linker.linkableTo(app)
+    False
+    >>> linker.linkTo(app)
+    Traceback (most recent call last):
+    ...
+    ValueError
+
+But they can be linked within the same manifest...
+
+    >>> linker.linkTo(container2)
+    u'none-3'
+    >>> sorted(container2)
+    ['none', u'none-2', u'none-3']
+    >>> container2['none']['data']['d']
+    <Demo 0>
+    >>> container2['none-3']['data']['d']
+    <Demo 0>
+    >>> import zc.shortcut.proxy
+    >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+    ...  zc.shortcut.proxy.removeProxy(container2['none-3']['data']['d']))
+    True
+
+...and across manifests.
+
+    >>> linker.linkTo(container)
+    u'none-2'
+    >>> container['none-2']['data']['d']
+    <Demo 0>
+    >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+    ...  zc.shortcut.proxy.removeProxy(container['none-2']['data']['d']))
+    True
+    
+If the linker is for an object that is the proxied object in the inventory,
+then it may in fact link outside of a manifest.
+
+    >>> linker = zc.shortcut.interfaces.IObjectLinker(
+    ...     container['none']['data']['d'])
+    >>> linker.linkableTo(app)
+    True
+    >>> linker.linkTo(app)
+    u'Demo-3-2'
+    >>> (zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']) is
+    ...  app[u'Demo-3-2'].raw_target)
+    False


Property changes on: zc.vault/trunk/src/zc/vault/traversal.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.vault/trunk/src/zc/vault/traversal.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.zcml	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.zcml	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,31 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="zope"
+    >
+
+  <adapter
+      factory=".traversal.ObjectCopier"
+      permission="zope.ManageContent"
+      trusted="y"
+      />
+
+  <adapter
+      factory=".traversal.ObjectMover"
+      permission="zope.ManageContent"
+      trusted="y"
+      />
+
+  <adapter
+      factory=".traversal.ObjectLinker"
+      permission="zope.ManageContent"
+      trusted="y"
+      />
+
+  <class class=".traversal.Proxy">
+    <require
+        permission="zope.Public"
+        interface=".traversal.IProxy"
+        />
+  </class>
+
+</configure>
\ No newline at end of file

Added: zc.vault/trunk/src/zc/vault/vault.py
===================================================================
--- zc.vault/trunk/src/zc/vault/vault.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/vault.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,646 @@
+
+import persistent # Not sure
+from zope import interface, component, event
+import zope.app.container.contained
+import zope.lifecycleevent
+import zope.proxy
+# import rwproperty
+from zc.copyversion import rwproperty
+import zc.copyversion.interfaces
+
+from zc.vault import interfaces, core
+
+def makeItem(rel, inventory):
+    if rel.token == inventory.vault.top_token:
+        return InventoryContents(inventory, relationship=rel)
+    else:
+        return InventoryItem(inventory, relationship=rel)
+
+class InventoryContents(object):
+    interface.implements(interfaces.IInventoryContents)
+
+    def __init__(self, inventory, relationship=None):
+        self.__parent__ = self.inventory = inventory
+        if relationship is not None:
+            if relationship.token != self.inventory.vault.top_token:
+                raise ValueError('contents must use top_token')
+            if not inventory.manifest.isOption(relationship):
+                raise ValueError('relationship is not in manifest')
+            self._relationship = relationship
+        else:
+            rel = inventory.manifest.get(self.inventory.vault.top_token)
+            if rel is None:
+                rel = core.Relationship(self.inventory.vault.top_token)
+                rel.__parent__ = inventory.manifest
+                event.notify(
+                    zope.lifecycleevent.ObjectCreatedEvent(rel))
+                inventory.manifest.addLocal(rel)
+            self._relationship = rel
+
+    def __getstate__(self):
+        raise RuntimeError('This should not be persisted.') # negotiable
+
+    def __eq__(self, other):
+        return (interfaces.IInventoryContents.providedBy(other) and 
+                self.relationship is other.relationship and
+                self.inventory.manifest is other.inventory.manifest)
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    @property
+    def relationship(self):
+        return self._relationship
+
+    def _getRelationshipFromKey(self, key, default=None):
+        token = self.relationship.containment.get(key)
+        if token is None:
+            return default
+        return self.inventory.manifest.get(token)
+
+    def __len__(self):
+        return len(self.relationship.containment)
+
+    def __getitem__(self, key):
+        rel = self._getRelationshipFromKey(key)
+        if rel is None:
+            raise KeyError(key)
+        return rel.object
+
+    def get(self, key, default=None):
+        rel = self._getRelationshipFromKey(key)
+        if rel is None:
+            return default
+        return rel.object
+
+    def items(self):
+        get = self.inventory.manifest.get
+        return [(k, get(t).object) for k, t in
+                self.relationship.containment.items()]
+
+    def keys(self):
+        return self.relationship.containment.keys()
+
+    def values(self):
+        get = self.inventory.manifest.get
+        return [get(t).object for t in self.relationship.containment.values()]
+
+    def __iter__(self):
+        return iter(self.relationship.containment)
+
+    def __contains__(self, key):
+        return key in self.relationship.containment
+
+    has_key = __contains__
+
+    def getKey(self, item, default=None):
+        if interfaces.IInventoryItem.providedBy(item):
+            item = item.relationship
+        return self.relationships.containment.getKey(item.token, default)
+
+    def makeMutable(self):
+        # local is mutable normally; modified is mutable while updating
+        if self.inventory.manifest._z_versioned:
+            raise zc.copyversion.interfaces.VersionedError
+        typ = self.type
+        if not self.inventory.manifest.updating:
+            if typ == interfaces.LOCAL:
+                return typ
+            elif self.has_local:
+                raise ValueError('Local revision already created')
+        elif typ == interfaces.MODIFIED:
+            return typ
+        selected = self.selected
+        rel = core.Relationship(
+            self.relationship.token, relationship=self.relationship,
+            source_manifest=self.inventory.manifest)
+        rel.__parent__ = self.inventory.manifest
+        event.notify(zope.lifecycleevent.ObjectCreatedEvent(rel))
+        if not self.inventory.manifest.updating:
+            self.inventory.manifest.addLocal(rel)
+            if selected:
+                self.inventory.manifest.select(rel)
+            res = interfaces.LOCAL
+        else:
+            self.inventory.manifest.addModified(rel)
+            if selected:
+                self.inventory.manifest.select(rel)
+            res = interfaces.MODIFIED
+        self._relationship = rel
+        return res
+
+    def __delitem__(self, key):
+        self.makeMutable()
+        old = self.relationship.containment[key]
+        del self.relationship.containment[key]
+
+    def __setitem__(self, key, value):
+        relset = self.inventory.manifest
+        token = self.relationship.containment.get(key)
+        if token is None:
+            self.makeMutable()
+            sub_rel = core.Relationship()
+            sub_rel.__parent__ = self.inventory.manifest
+            sub_rel.token = self.inventory.vault.intids.register(sub_rel)
+            event.notify(
+                zope.lifecycleevent.ObjectCreatedEvent(sub_rel))
+            sub_rel.object = value
+            if self.inventory.updating:
+                relset.addModified(sub_rel)
+            else:
+                relset.addLocal(sub_rel)
+            self.relationship.containment[key] = sub_rel.token
+        else:
+            sub_rel = self.inventory.manifest.get(token)
+            assert sub_rel is not None
+            if relset.getType(sub_rel) not in (
+                interfaces.LOCAL, interfaces.SUGGESTED, interfaces.MODIFIED):
+                item = makeItem(sub_rel, self.inventory)
+                item.makeMutable()
+                sub_rel = item.relationship
+            sub_rel.object = value
+
+    def updateOrder(self, order):
+        self.makeMutable()
+        self.relationship.containment.updateOrder(order)
+
+    def updateOrderFromTokens(self, order):
+        self.makeMutable()
+        c = self.relationship.containment
+        c.updateOrder(c.getKey(t) for t in order)
+
+    @property
+    def token(self):
+        return self.relationship.token
+
+    def __call__(self, name, *args):
+        if args:
+            if len(args) > 1:
+                raise TypeError(
+                    '__call__() takes at most 2 arguments (%d given)' %
+                    len(args) + 1)
+            rel = self.relationship.containment.get(name)
+            if rel is None:
+                return args[0]
+        else:
+            rel = self.relationship.containment[name]
+        return InventoryItem(self.inventory, rel)
+
+    @property
+    def previous(self):
+        previous = self.inventory.manifest.previous
+        if previous is None:
+            return None
+        previous_relationship = previous.get(self.relationship.token)
+        if previous_relationship is None:
+            return None
+        return makeItem(previous_relationship, Inventory(previous))
+
+    @property
+    def next(self):
+        next = self.inventory.manifest.next
+        if next is None:
+            return None
+        next_relationship = next.get(self.relationship.token)
+        if next_relationship is None:
+            return None
+        return makeItem(next_relationship, Inventory(next))
+
+    @property
+    def previous_version(self):
+        rel = self.inventory.vault.getPrevious(self.relationship)
+        if rel is not None:
+            return makeItem(rel, Inventory(rel.__parent__))
+
+    @property
+    def next_version(self):
+        rel = self.inventory.vault.getNext(self.relationship)
+        if rel is not None:
+            return makeItem(rel, Inventory(rel.__parent__))
+
+    @property
+    def type(self):
+        return self.inventory.manifest.getType(self.relationship)
+
+    @property
+    def selected(self):
+        return self.inventory.manifest.isSelected(self.relationship)
+
+    def select(self):
+        self.inventory.manifest.select(self.relationship)
+
+    @property
+    def is_update_conflict(self):
+        return self.inventory.manifest.isUpdateConflict(
+            self.relationship.token)
+
+    def resolveUpdateConflict(self):
+        if not self.inventory.manifest.updating:
+            raise RuntimeError('can only resolve merges while updating')
+        if not self.is_update_conflict:
+            raise RuntimeError('Not a conflict')
+        self.select() # XXX is this good behavior?
+        self.inventory.manifest.resolveUpdateConflict(
+            self.relationship.token)
+
+    @property
+    def has_base(self):
+        return bool(self.inventory.manifest.getBase(
+            self.relationship.token))
+
+    @property
+    def has_local(self):
+        return bool(self.inventory.manifest.getLocal(
+            self.relationship.token))
+
+    @property
+    def has_updated(self):
+        return bool(self.inventory.manifest.getUpdated(
+            self.relationship.token))
+
+    @property
+    def has_suggested(self):
+        return bool(list(
+            self.inventory.manifest.iterSuggested(
+                self.relationship.token)))
+
+    @property
+    def has_modified(self):
+        return bool(list(
+            self.inventory.manifest.iterModified(
+                self.relationship.token)))
+
+    @property
+    def has_merged(self):
+        return bool(list(
+            self.inventory.manifest.iterMerged(
+                self.relationship.token)))
+
+    @property
+    def base_item(self):
+        rel = self.inventory.manifest.getBase(
+            self.relationship.token)
+        if rel is not None:
+            return makeItem(rel, self.inventory)
+
+    @property
+    def local_item(self):
+        rel = self.inventory.manifest.getLocal(
+            self.relationship.token)
+        if rel is not None:
+            return makeItem(rel, self.inventory)
+
+    @property
+    def updated_item(self):
+        rel = self.inventory.manifest.getUpdated(
+            self.relationship.token)
+        if rel is not None:
+            return makeItem(rel, self.inventory)
+
+    def iterSuggestedItems(self):
+        for rel in self.inventory.manifest.iterSuggested(
+            self.relationship.token):
+            yield makeItem(rel, self.inventory)
+
+    def iterModifiedItems(self):
+        for rel in self.inventory.manifest.iterModified(
+            self.relationship.token):
+            yield makeItem(rel, self.inventory)
+
+    def iterMergedItems(self):
+        for rel in self.inventory.manifest.iterMerged(
+            self.relationship.token):
+            yield makeItem(rel, self.inventory)
+
+    @property
+    def copy_source(self):
+        if self.relationship.copy_source is not None:
+            return makeItem(
+                self.relationship.copy_source[0],
+                Inventory(self.relationship.copy_source[1]))
+
+    @property
+    def selected_item(self):
+        if self.selected:
+            return self
+        return makeItem(
+            self.inventory.manifest.get(self.relationship.token),
+            self.inventory)
+
+class InventoryItem(InventoryContents):
+
+    interface.implements(interfaces.IInventoryItem)
+
+    def __init__(self, inventory, token=None, relationship=None):
+        if token is inventory.vault.top_token:
+            raise ValueError('Cannot create inventory item with top_token')
+        self.__parent__ = self.inventory = inventory
+        if relationship is None:
+            if token is None:
+                raise ValueError('must provide one of relationship or token')
+            relationship = inventory.manifest.get(token)
+            if relationship is None:
+                raise ValueError('token is not used in this inventory')
+        elif not inventory.manifest.isOption(relationship):
+            raise ValueError('relationship is not in inventory')
+        self._relationship = relationship
+
+    def resolveOrphanConflict(self):
+        self.inventory.manifest.resolveOrphanConflict(
+            self.relationship.token)
+
+    @property
+    def is_orphan(self):
+        return self.inventory.manifest.isOrphan(self.relationship.token)
+
+    @property
+    def is_orphan_conflict(self):
+        return self.inventory.manifest.isOrphanConflict(
+            self.relationship.token)
+
+    @property
+    def is_parent_conflict(self):
+        return len(list(
+            self.inventory.manifest.iterSelectedParents(
+                self.relationship.token))) > 1
+
+    @property
+    def parent(self):
+        res = self.inventory.manifest.getParent(self.relationship.token)
+        if res is not None:
+            return makeItem(res, self.inventory)
+
+    @property
+    def name(self):
+        res = self.inventory.manifest.getParent(self.relationship.token)
+        if res is not None:
+            return res.containment.getKey(self.relationship.token)
+
+    def iterSelectedParents(self):
+        return (makeItem(rel, self.inventory) for rel in
+                self.inventory.manifest.iterSelectedParents(
+                    self.relationship.token))
+
+    def iterParents(self):
+        return (makeItem(rel, self.inventory) for rel in
+                self.inventory.manifest.iterParents(
+                    self.relationship.token))
+
+    @property
+    def object(self):
+        return self.relationship.object
+    @rwproperty.setproperty
+    def object(self, value):
+        self.relationship.object = value # may raise VersionedError
+
+    def moveTo(self, location=None, name=None):
+        clean_location = zope.proxy.removeAllProxies(location)
+        if clean_location.inventory.manifest is not self.inventory.manifest:
+            self.copyTo(location, name)
+            if self.name:
+                del self.parent[self.name]
+            # go down & resolve all orphan conflicts: too much of a heuristic?
+            stack = [(lambda x: self, iter(('',)))]
+            while stack:
+                s, i = stack[-1]
+                try:
+                    key = i.next()
+                except StopIteration:
+                    stack.pop()
+                else:
+                    val = s(key)
+                    if val.is_orphan_conflict:
+                        val.resolveOrphanConflict()
+                    stack.append((val, iter(val)))
+            return
+        if location is None:
+            location = self.parent
+        if location is None:
+            raise ValueError('location must be supplied for orphans')
+        old_name = self.name
+        if name is None:
+            name = old_name
+        if name is None:
+            raise ValueError('Must specify name')
+        if name in location:
+            if zope.proxy.removeAllProxies(
+                location(name).relationship) is self.relationship:
+                return
+            raise ValueError(
+                'Object with same name already exists in new location')
+        if (self.selected and location.selected and
+            self.inventory.manifest.isLinked(
+                self.relationship.token, location.relationship.token)):
+            # location is in self
+            raise ValueError('May not move item to within itself')
+        parent = self.parent
+        if old_name:
+            del self.parent[old_name]
+        if (parent is None or
+            clean_location.relationship is not parent.relationship):
+            location.makeMutable()
+        else:
+            location = parent
+        location.relationship.containment[name] = self.relationship.token
+        if location.selected and not self.selected:
+            self.select()
+
+    def copyTo(self, location, name=None):
+        if name is None:
+            name = self.name
+        if name in location:
+            raise ValueError(
+                'Object with same name already exists in new location')
+        location.makeMutable()
+        # to get around error-checking constraints in the core, we go from
+        # bottom-to-top.
+        # this also prevents the possibility of infinite recursion in the copy.
+        stack = [
+            (location.relationship, iter((name,)), lambda x: self, {})]
+        if location.inventory.updating:
+            add = location.inventory.manifest.addModified
+        else:
+            add = location.inventory.manifest.addLocal
+        clean_location = zope.proxy.removeAllProxies(location)
+        while stack:
+            relationship, keys, src, queued = stack[-1]
+            try:
+                key = keys.next()
+            except StopIteration:
+                stack.pop()
+                for k, v in queued.items():
+                    relationship.containment[k] = v
+                if (zope.proxy.removeAllProxies(relationship) is not
+                    clean_location.relationship):
+                    add(relationship)
+            else:
+                value = src(key)
+                rel = core.Relationship(
+                    relationship=value.relationship,
+                    source_manifest=value.inventory.manifest)
+                rel.__parent__ = clean_location.inventory.manifest
+                rel.token = location.inventory.vault.intids.register(rel)
+                event.notify(
+                    zope.lifecycleevent.ObjectCreatedEvent(rel))
+                queued[key] = rel.token
+                stack.append((rel, iter(value), value, {}))
+
+class Inventory(persistent.Persistent, zope.app.container.contained.Contained):
+
+    interface.implements(interfaces.IInventory)
+
+    def __init__(
+        self, manifest=None, inventory=None, vault=None, mutable=False):
+        if manifest is None:
+            if vault is None:
+                if inventory is None:
+                    raise ValueError(
+                        'must provide manifest, inventory, or vault')
+                manifest = inventory.manifest
+            else: # vault exists
+                if inventory is None:
+                    manifest = vault.manifest
+                    if manifest is None:
+                        manifest = core.Manifest(vault=vault)
+                        event.notify(
+                            zope.lifecycleevent.ObjectCreatedEvent(
+                                manifest))
+                else:
+                    manifest = inventory.manifest
+        elif inventory is not None:
+            raise ValueError('cannot pass both manifest and inventory')
+        if mutable and manifest._z_versioned:
+            manifest = core.Manifest(manifest, vault)
+            event.notify(
+                zope.lifecycleevent.ObjectCreatedEvent(manifest))
+        self._manifest = manifest
+        self.__parent__ = manifest.vault
+
+    def __eq__(self, other):
+        return (interfaces.IInventory.providedBy(other) and
+                self.manifest is other.manifest)
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    @property
+    def vault(self):
+        return self._manifest.vault
+    @rwproperty.setproperty
+    def vault(self, value):
+        self._manifest.vault = value
+
+    @property
+    def contents(self):
+        return InventoryContents(
+            self, self.manifest.get(self.vault.top_token))
+
+    @property
+    def manifest(self):
+        return self._manifest
+
+    def iterUpdateConflicts(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterUpdateConflicts())
+
+    def iterUpdateResolutions(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterUpdateResolutions())
+
+    def iterOrphanConflicts(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterOrphanConflicts())
+
+    def iterOrphanResolutions(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterOrphanResolutions())
+
+    def iterUnchangedOrphans(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterUnchangedOrphans())
+
+    def iterParentConflicts(self):
+        return (makeItem(r, self)
+                for r in self.manifest.iterParentConflicts())
+
+    def __iter__(self): # selected items
+        return (makeItem(r, self) for r in self.manifest)
+
+    @property
+    def updating(self):
+        return self.manifest.updating
+
+    @property
+    def merged_sources(self):
+        return tuple(Inventory(r) for r in self.manifest.merged_sources)
+
+    @property
+    def update_source(self):
+        res = self.manifest.update_source
+        if res is not None:
+            return Inventory(res)
+
+    def beginUpdate(self, source=None, previous=None):
+        if interfaces.IInventory.providedBy(source):
+            source = source.manifest
+        if interfaces.IInventory.providedBy(previous):
+            previous = previous.manifest
+        self.manifest.beginUpdate(source, previous)
+
+    def completeUpdate(self):
+        self.manifest.completeUpdate()
+
+    def abortUpdate(self):
+        self.manifest.abortUpdate()
+
+    def beginCollectionUpdate(self, items):
+        self.manifest.beginCollectionUpdate(
+            frozenset(i.relationship for i in items))
+
+    def iterChangedItems(self, source=None):
+        if interfaces.IInventory.providedBy(source):
+            source = source.manifest
+        return (makeItem(r, self)
+                for r in self.manifest.iterChanges(source))
+
+    def getItemFromToken(self, token, default=None):
+        rel = self.manifest.get(token)
+        if rel is None:
+            return default
+        return makeItem(rel, self)
+
+    @property
+    def previous(self):
+        p = self.manifest.previous
+        if p is not None:
+            return Inventory(p)
+        return None
+
+    @property
+    def next(self):
+        p = self.manifest.next
+        if p is not None:
+            return Inventory(p)
+        return None
+
+class Vault(core.Vault):
+    interface.implements(interfaces.IInventoryVault)
+
+    def getInventory(self, ix):
+        return Inventory(self[ix])
+
+    @property
+    def inventory(self):
+        if self._data:
+            return Inventory(self._data[self._data.maxKey()])
+        return None
+
+    def commit(self, value):
+        if interfaces.IInventory.providedBy(value):
+            value = value.manifest
+        super(Vault, self).commit(value)
+
+    def commitFrom(self, value):
+        if interfaces.IInventory.providedBy(value):
+            value = value.manifest
+        super(Vault, self).commitFrom(value)

Added: zc.vault/trunk/src/zc/vault/versions.py
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.py	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.py	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,174 @@
+import sys
+import persistent
+import zope.interface
+import zope.component
+import zope.location
+import zc.vault.vault
+import zope.publisher.browser
+import zope.publisher.interfaces
+import zope.publisher.interfaces.browser
+
+class IReadVersions(zope.interface.Interface):
+    """abstract: see IVersions"""
+
+    vault = zope.interface.Attribute(
+        """the vault that this collection of versions uses.""")
+
+    factory = zope.interface.Attribute(
+        """the (persistable) callable that gets an inventory and returns the
+        persistable wrapper object that has the desired API.""")
+
+    def __getitem__(ix):
+        """return version for given index, or raise KeyError
+        if no such index exists."""
+
+    def __len__():
+        """return number of versions"""
+
+class IWriteVersions(zope.interface.Interface):
+    """abstract: see IVersions"""
+
+    def commit(version):
+        """commit version"""
+
+    def commitFrom(version):
+        """commit from previous version"""
+
+    def create():
+        """create and return an editable version of the most recently
+        committed."""
+
+class IVersions(IReadVersions, IWriteVersions):
+    """a collection of versions"""
+
+class IWrapperAware(zope.interface.Interface):
+    """A manifest that has a wrapper attribute pointing to it's
+    desired wrapper"""
+
+    wrapper = zope.interface.Attribute(
+        """the desired wrapper""")
+
+
+class Versions(persistent.Persistent, zope.location.Location):
+    """Sequence of capability family versions.
+
+    Used to implement CapabilityFamily.versions
+    """
+    
+    zope.interface.implements(IVersions)
+
+    def __init__(self, vault, factory, parent=None, name=None, initialize=None):
+        self.vault = vault
+        self.factory = factory
+        if vault.__parent__ is None:
+            zope.location.locate(self.vault, self, 'vault')
+        elif parent is None:
+            raise RuntimeError(
+                "programmer error: Locate the vault, or pass a parent in, "
+                "or both")
+        if parent is not None:
+            if name is not None:
+                zope.location.locate(self, parent, name)
+            else:
+                self.__parent__ = parent
+        for ix in range(len(vault)):
+            i = vault.getInventory(ix)
+            assert not IWrapperAware.providedBy(i.manifest), (
+                'programmer error: manifests in vault have already been placed '
+                'in a Versions container')
+            i.__parent__ = self
+            wrapper = self.factory(i)
+            i.manifest.wrapper = wrapper
+            zope.interface.directlyProvides(i.manifest, IWrapperAware)
+            if zope.location.interfaces.ILocation.providedBy(wrapper):
+                zope.location.locate(wrapper, self, str(i.manifest.vault_index))
+        if initialize is not None:
+            res = self.create()
+            initialize(res)
+            self.commit(res)
+
+    def __len__(self):
+        return len(self.vault)
+
+    def __getitem__(self, idx):
+        manifest = self.vault[idx]
+        return manifest.wrapper
+
+    def __iter__(self):
+        for m in self.vault:
+            yield m.wrapper
+
+    def commit(self, wrapper):
+        manifest = wrapper.inventory.manifest # XXX currently .inventory is
+        # undocumented, hard requirement of wrapper...
+        assert manifest.wrapper is wrapper, (
+            'programmer error: manifest should have back reference to '
+            'version')
+        self.vault.commit(manifest)
+        if zope.location.interfaces.ILocation.providedBy(wrapper):
+            zope.location.locate(wrapper, self, str(manifest.vault_index))
+
+    def commitFrom(self, wrapper):
+        manifest = wrapper.inventory.manifest
+        assert manifest.wrapper is wrapper, (
+            'programmer error: manifest should have back reference to '
+            'version')
+        self.vault.commitFrom(manifest)
+        i = self.vault.getInventory(-1)
+        wrapper = self.factory(i)
+        i.manifest.wrapper = wrapper
+        zope.interface.directlyProvides(i.manifest, IWrapperAware)
+        if zope.location.interfaces.ILocation.providedBy(wrapper):
+            zope.location.locate(wrapper, self, str(i.manifest.vault_index))
+
+    def create(self):
+        inventory = zc.vault.vault.Inventory(vault=self.vault, mutable=True)
+        inventory.__parent__ = self
+        res = self.factory(inventory)
+        inventory.manifest.wrapper = res
+        zope.interface.directlyProvides(
+            inventory.manifest, IWrapperAware)
+        res.__parent__ = self
+        return res
+
+
+class deferredProperty(object):
+    def __init__(self, name, initialize):
+        self.name = name
+        sys._getframe(1).f_locals[name] = self
+        self.initialize = initialize
+    def __get__(self, obj, typ=None):
+        if obj is not None:
+            self.initialize(obj)
+            return obj.__dict__[self.name]
+        return self
+
+
+class Traverser(zope.publisher.browser.BrowserView):
+    zope.component.adapts(
+        IVersions, zope.publisher.interfaces.browser.IBrowserRequest)
+    zope.interface.implements(
+        zope.publisher.interfaces.browser.IBrowserPublisher)
+
+    _default = 'index.html'
+
+    def browserDefault(self, request):
+        return self.context, (self._default, )
+
+    def publishTraverse(self, request, name):
+        try:
+            ix = int(name)
+        except ValueError:
+            pass
+        else:
+            try:
+                v = self.context[ix]
+            except IndexError:
+                name = self._default
+            else:
+                return v
+        view = zope.component.queryMultiAdapter(
+            (self.context, request), name=name)
+        if view is not None:
+            return view
+        raise zope.publisher.interfaces.NotFound(self.context, name, request)

Added: zc.vault/trunk/src/zc/vault/versions.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.txt	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.txt	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,165 @@
+========
+Versions
+========
+
+A pattern for using the vault often involves wrapping it, so that clients
+are not aware of the vault or inventory API, but have a custom API more
+pertinent to the specific use case.  One convenient way to follow this pattern
+is to have an object that acts as a container and factory for the
+wrappers.  The `versions` module provides this object, and a traverser that
+can use it.
+
+To show it in use, we will need a class that uses a Versions object and a
+factory that creates the persistent versions.  Here, we'll have a Project class
+that has ProjectVersion classes.
+
+    >>> import persistent
+    >>> import zc.vault.vault
+    >>> import zc.vault.versions
+    >>> import zc.vault.core
+    >>> import zope.location
+    >>> class ProjectVersion(persistent.Persistent, zope.location.Location):
+    ...     def __init__(self, inventory):
+    ...         self.inventory = inventory
+    ...         zope.location.locate(self.inventory, self, 'inventory')
+    ...     def __getitem__(self, key):
+    ...         return self.inventory.contents('library')[key]
+    ...     def __setitem__(self, key, value):
+    ...         self.inventory.contents('library')[key] = value
+    ...     def get(self, key, default=None):
+    ...         try:
+    ...             return self[key]
+    ...         except KeyError:
+    ...             return default
+    ...
+    >>> def initialize(obj):
+    ...     assert obj.__dict__.get('held') is None, 'programmer error'
+    ...     obj.held = zc.vault.core.HeldContainer()
+    ...     zope.location.locate(obj.held, obj, 'held')
+    ...     def _initialize(version):
+    ...         inventory = version.inventory
+    ...         if inventory.contents.get('library', inventory) is inventory:
+    ...             # initialize inventory
+    ...             inventory.contents['library'] = None
+    ...             inventory.contents['data'] = None
+    ...     obj.versions = zc.vault.versions.Versions(
+    ...         zc.vault.vault.Vault(held=obj.held), ProjectVersion,
+    ...         obj, 'versions', _initialize)
+    ...     obj.active = obj.versions.create()
+    ...     zope.location.locate(obj.active, obj, 'active')
+    ...
+    >>> class Project(persistent.Persistent, zope.location.Location):
+    ...     zc.vault.versions.deferredProperty('active', initialize)
+    ...     zc.vault.versions.deferredProperty('versions', initialize)
+    ...     zc.vault.versions.deferredProperty('held', initialize)
+    ...     def commit(self):
+    ...         self.versions.commit(self.active)
+    ...         self.active = self.versions.create()
+    ...         zope.location.locate(self.active, self, 'active')
+    ...
+
+Now if we instantiate the Project (and put it in an application), we can
+examine the versions.
+
+    >>> p = app['project'] = Project()
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(zc.vault.versions.IVersions, p.versions)
+    True
+    >>> len(p.versions)
+    1
+    >>> p.versions[0].get('foo') # None
+    >>> import zc.copyversion.versioning
+    >>> class Demo(persistent.Persistent, zc.copyversion.versioning.Versioning):
+    ...     pass
+    >>> o = p.active['foo'] = Demo()
+    >>> p.commit()
+    >>> len(p.versions)
+    2
+    >>> p.versions[1]['foo'] is o
+    True
+    >>> o2 = p.active['bar'] = Demo()
+
+Old versions that are ILocations have been placed within the versions object
+and given __name__ values that correspond to their indices.
+
+    >>> p.versions[0].__name__
+    '0'
+    >>> p.versions[0].__parent__ is p.versions
+    True
+    >>> p.versions[1].__name__
+    '1'
+    >>> p.versions[1].__parent__ is p.versions
+    True
+
+In addition to `commit`, the Versions object also provides `commitFrom`.
+This works the same as the vault `commitFrom` method, and is not explained
+further here (for now).
+
+    >>> new_o = p.active['foo'] = Demo()
+    >>> p.commit()
+    >>> p.versions[2]['foo'] is new_o
+    True
+    >>> p.versions.commitFrom(p.versions[1])
+    >>> p.versions[3]['foo'] is new_o
+    False
+    >>> p.versions[3]['foo'] is o
+    True
+    >>> p.versions[3].__name__
+    '3'
+    >>> p.versions[3].__parent__ is p.versions
+    True
+
+If you instantiate a Versions object with a vault that has pre-existing
+manifests--like one that comes from a branch--the pre-existing manifests will
+automatically get set up as if they had been part of the versions object.
+
+    >>> p.alt_versions = zc.vault.versions.Versions(
+    ...     p.versions.vault.createBranch(), ProjectVersion, p, 'alt_versions')
+    >>> len(p.alt_versions)
+    1
+    >>> p.alt_versions[0].get('foo') is o
+    True
+    >>> p.alt_versions[0].__parent__ is p.alt_versions
+    True
+    >>> p.alt_versions[0].__name__
+    '0'
+
+The module also includes a traverser.
+
+    >>> import zope.publisher.browser
+    >>> request = zope.publisher.browser.TestRequest()
+    >>> traverser = zc.vault.versions.Traverser(p.versions, request)
+    >>> traverser.browserDefault(request) # doctest: +ELLIPSIS
+    (<zc.vault.versions.Versions object at ...>, ('index.html',))
+    >>> traverser.publishTraverse(request, '1') is p.versions[1]
+    True
+    >>> traverser.publishTraverse(request, '0') is p.versions[0]
+    True
+
+If the traverser gets a non-integer, it tries to get a view, but fails with
+a NotFound error.
+
+    >>> traverser.publishTraverse(request, 'index.html')
+    ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    NotFound: Object: <zc.vault.versions.Versions object at ...>,
+              name: 'index.html'
+
+If the traverser gets an integer out-of-range, it tries for 'index.html' at
+the moment; this may need to be rethought.
+
+    >>> traverser.publishTraverse(request, '200')
+    ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    NotFound: Object: <zc.vault.versions.Versions object at ...>,
+              name: 'index.html'
+
+With this stuff, I usually like to make sure we can actually commit the
+transaction.
+
+    >>> import transaction
+    >>> transaction.commit()
+
+Yay.
\ No newline at end of file


Property changes on: zc.vault/trunk/src/zc/vault/versions.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.vault/trunk/src/zc/vault/versions.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.zcml	2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.zcml	2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="zc.vault"
+    >
+
+  <class class=".versions.Versions">
+    <factory id="zc.vault.versions.Versions" />
+    <require
+        permission="zope.View"
+        interface=".versions.IReadVersions"
+        />
+    <require
+        permission="zope.ManageContent"
+        interface=".versions.IWriteVersions"
+        />
+  </class>
+
+  <view
+      for=".versions.IVersions"
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+      factory=".versions.Traverser"
+      permission="zope.Public"
+      allowed_interface="zope.publisher.interfaces.browser.IBrowserPublisher"
+      />
+
+</configure>



More information about the Checkins mailing list