[Checkins] SVN: zope.fssync/trunk/ Added subversion support by a reimplementation of z3c.vcsync

Uwe Oestermeier u.oestermeier at iwm-kmrc.de
Wed Jul 4 15:27:21 EDT 2007


Log message for revision 77427:
  Added subversion support by a reimplementation of z3c.vcsync

Changed:
  U   zope.fssync/trunk/buildout.cfg
  U   zope.fssync/trunk/src/zope/fssync/interfaces.py
  U   zope.fssync/trunk/src/zope/fssync/metadata.py
  U   zope.fssync/trunk/src/zope/fssync/repository.py
  A   zope.fssync/trunk/src/zope/fssync/svn.py
  A   zope.fssync/trunk/src/zope/fssync/svn.txt
  U   zope.fssync/trunk/src/zope/fssync/task.py
  A   zope.fssync/trunk/src/zope/fssync/tests/svntestdir/
  U   zope.fssync/trunk/src/zope/fssync/tests/test_docs.py

-=-
Modified: zope.fssync/trunk/buildout.cfg
===================================================================
--- zope.fssync/trunk/buildout.cfg	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/buildout.cfg	2007-07-04 19:27:20 UTC (rev 77427)
@@ -7,3 +7,4 @@
 [test]
 recipe = zc.recipe.testrunner
 eggs = zope.fssync
+       py

Modified: zope.fssync/trunk/src/zope/fssync/interfaces.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/interfaces.py	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/src/zope/fssync/interfaces.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -244,7 +244,13 @@
     def iterPaths():
         """Iterates over all paths in the archive."""
 
+class IVersionControlRepository(IRepository):
+    """A repository that stores the data in a version control system."""
 
+class ISVNRepository(IRepository):
+    """A repository that stores the data in a subversion checkout."""
+
+
 class ISynchronizerFactory(component.interfaces.IFactory):
     """A factory for synchronizer, i.e. serializers/de-serializers.
     

Modified: zope.fssync/trunk/src/zope/fssync/metadata.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/metadata.py	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/src/zope/fssync/metadata.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -92,6 +92,7 @@
                 entry["flag"] = "added"
 
 class DirectoryManager(object):
+
     def __init__(self, dir):
         self.zdir = join(dir, "@@Zope")
         self.efile = join(self.zdir, "Entries.xml")

Modified: zope.fssync/trunk/src/zope/fssync/repository.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/repository.py	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/src/zope/fssync/repository.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -213,7 +213,7 @@
         fp = self.files[path] = file(path, 'wb')
         return fp
 
-    def split(path):
+    def split(self, path):
         """Split a path, making sure that the tail returned is real."""
         head, tail = os.path.split(path)
         if tail in unwanted:

Added: zope.fssync/trunk/src/zope/fssync/svn.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/svn.py	                        (rev 0)
+++ zope.fssync/trunk/src/zope/fssync/svn.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -0,0 +1,216 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 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.
+#
+##############################################################################
+"""A subversion repository for serialized data.
+
+$Id: svn.py 73003 2007-03-06 10:34:19Z oestermeier $
+"""
+
+import shutil
+import zope.interface
+import os.path
+import py.path
+
+import repository
+import metadata
+import interfaces
+import task
+
+class VersionControlRepository(repository.FileSystemRepository):
+    """Version Control Repository.
+
+    (hopefully) version control system agnostic.
+    """
+    zope.interface.implements(interfaces.IVersionControlRepository)
+    
+    def __init__(self):
+        super(VersionControlRepository, self).__init__()
+        self.clear()
+
+    def sync(self, object, message=''):
+        self.save(object)
+        self.up()
+        self.resolve()
+        self.load(object)
+        self.commit(message)
+
+    def clear(self):
+        self._added_by_save = []
+        self._deleted_by_save = []
+        
+    def up(self):
+        raise NotImplementedError
+
+    def resolve(self):
+        raise NotImplementedError
+
+    def commit(self, message):
+        raise NotImplementedError
+
+
+class SVNChanges(object):
+    """A collector for changes in a SVN repository."""
+    
+    def __init__(self, before, after):
+        self.added = sorted(after - before)
+        self.deleted = sorted(before - after)
+        
+    def accept(self):
+        for path in self.deleted:
+            if path.check(versioned=True):
+                path.remove()
+            if path.check():
+                shutil.rmtree(str(path))
+
+
+class SVNDirectoryManager(metadata.DirectoryManager):
+    """Keeps fssync metadata of objects under version control."""
+    
+    def __init__(self, wcpath):
+        self.entries = {}
+        for path in wcpath.listdir():
+            if path.check(versioned=True):
+                self.entries[path.basename] = dict(name=path.basename)
+
+    def getentry(self, name):
+        if name not in self.entries:
+            return dict(name=name, flag='removed')
+        return self.entries.get(name, {})
+
+
+class SVNMetadata(metadata.Metadata):
+    """Reads the metadata from a SVN repository."""
+
+    def __init__(self, repository):
+        super(SVNMetadata, self).__init__()
+        self.repository = repository
+
+    def getentry(self, file):
+        """Return the metadata entry for a given file (or directory).
+
+        Modifying the dict that is returned will cause the changes to
+        the metadata to be written out when flush() is called.  If
+        there is no metadata entry for the file, return a new empty
+        dict, modifications to which will also be flushed.
+        """
+        fullpath = self.repository.fullpath(file)
+        dir, base = self.repository.split(file)
+        return self.getmanager(dir).getentry(base)
+
+    def getmanager(self, dir):
+        dir = self.repository.fullpath(dir)
+        if dir not in self.cache:
+            self.cache[dir] = SVNDirectoryManager(dir)
+        return self.cache[dir]
+
+
+class SVNRepository(VersionControlRepository):
+    """A subversion repository."""
+
+    zope.interface.implements(interfaces.ISVNRepository)
+
+    debug = False
+    
+    def __init__(self, path):
+        self.svnwc = py.path.svnwc(path)
+        allpaths = self.svnwc.visit(rec=True)
+        self.before = set(allpaths)
+        self.after = set(allpaths)
+        super(SVNRepository, self).__init__()
+
+    def up(self):
+        self.svnwc.update()
+
+    def fullpath(self, relpath):
+        return self.svnwc.join(relpath)
+        
+    def getMetadata(self):
+        """Returns a special metadata database which reads directly 
+        from the SVN repository."""
+        return SVNMetadata(self)
+
+    def clear(self):
+        super(SVNRepository, self).clear()
+        self.before = self.after
+        self.after = set()
+        
+    def changes(self):
+        return SVNChanges(self.before, self.after)
+        
+    def isdir(self, path):
+        return self.svnwc.join(path).check(dir=True)
+    
+    def split(self, path):
+        if isinstance(path, str):
+            return os.path.split(path)
+        return path.dirpath(), path.basename
+        
+    def ensuredir(self, path):
+        dir = self.svnwc.ensure(path, directory=True)
+        self.after.add(dir)
+        return dir
+        
+    def readable(self, path):
+        """Returns a file like object that is open for read operations."""
+        realpath = self.svnwc.join(path)
+        fp = self.files[path] = realpath.open('rb')
+        return fp
+
+    def writeable(self, path):
+        """Returns a file like object that is open for write operations.
+        
+        XXX: Ignores all files with @@ at the moment. These files
+        should not be generated at all.
+        
+        """
+        if '@@' in path:
+            class DummyStream(object):
+                def write(self, data):
+                    pass
+                def close(self):
+                    pass
+            return DummyStream()
+            
+        wcpath = self.svnwc.join(path)
+        self.svnwc.ensure(path)
+        fp = self.files[path] = wcpath.open('wb')
+        while len(str(wcpath)) > len(str(self.svnwc)):
+            self.after.add(wcpath)
+            wcpath = wcpath.dirpath()
+        return fp
+
+
+class SVNSyncTask(task.SyncTask):
+    """A sync task that performs a complete update cycle."""
+ 
+    def resolve(self):
+        pass
+        
+    def commit(self, message):
+        self.repository.commit(message)
+
+    def perform(self, container, name, message=''):
+
+        self.repository.debug = True
+        
+        export = task.Checkout(self.getSynchronizer, self.repository)
+        export.perform(container[name], name)
+
+        self.repository.up()
+
+        load = task.Commit(self.getSynchronizer, self.repository)
+        load.debug = True
+        load.perform(container, name, name)
+
+
+

Added: zope.fssync/trunk/src/zope/fssync/svn.txt
===================================================================
--- zope.fssync/trunk/src/zope/fssync/svn.txt	                        (rev 0)
+++ zope.fssync/trunk/src/zope/fssync/svn.txt	2007-07-04 19:27:20 UTC (rev 77427)
@@ -0,0 +1,641 @@
+Synchronization with a Version Control System
+=============================================
+
+The synchronization of persistent content with a version control system
+can be used to support offline editing of documents in a collaborative 
+setting. It can also be used to save and restore versions of a site or 
+parts of a site.
+
+SVN is used as an example here since it is available to all Zope 
+programmers.
+
+The original motivation for the SVN support stems from the z3c.vcsync 
+package developed by Martijn Faassen. This doctest is an attempt to 
+show that the z3c.vcsync package can be easily reimplemented in 
+zope.fssync. See the README.txt in z3c.vcsync. At the same time this
+doctest shows that the use case of content management can be handled
+with zope.fssync although historically the original focus was on 
+site management and ttw development.
+
+The following lines are directly copied from Martijn Faassen's doctest.
+Code and comments are only modified as far as necessary.
+
+
+Synchronization with SVN
+------------------------
+
+The synchronization sequence is as follows (example given with SVN as
+the version control system):
+ 
+  1) save persistent state to svn checkout on the same machine as the
+     Zope application.
+
+  2) ``svn up``. Subversion merges in changed made by others users
+     that were checked into the svn server.
+
+  3) Any svn conflicts are automatically resolved.
+
+  4) reload changes in svn checkout into persistent Python objects
+
+  5) ``svn commit``.
+
+This is all happening in a single step. It can happen over and over
+again in a reasonably safe manner, as after the synchronization has
+concluded, the state of the persistent objects and that of the local
+SVN checkout will always be perfectly in sync.
+
+
+SVN difficulties
+----------------
+
+Changing a file into a directory with SVN requires the following
+procedure::
+  
+  * svn remove file
+
+  * svn commit file
+
+  * svn up
+
+  * mdkir file
+
+  * svn add file
+
+If during the serialization procedure a file changed into a directory,
+it would require an ``svn up`` to be issued during step 1. This is too
+early. As we see later, we instead ask the application developer to
+avoid this situation altogether.
+
+
+Serialization
+-------------
+
+In order to export content to a version control system, it first needs
+to be possible to serialize a content object to a text representation.
+
+For the purposes of this document, we have defined a simple item that
+just carries an integer payload attribute::
+
+  >>> class Item(object):
+  ...   def __init__(self, payload):
+  ...     self.payload = payload
+  >>> item = Item(payload=1)
+  >>> item.payload
+  1
+
+We will use an ISynchronizer adapter to serialize it to a file. Let's
+specialize the default sychronizer which saves no annotations and
+no extra attributes.
+
+  >>> from zope.fssync import synchronizer, interfaces
+  >>> class ItemSynchronizer(synchronizer.DefaultSynchronizer):
+  ...     def dump(self, f):
+  ...         f.write(str(self.context.payload))
+  ...         f.write('\n')
+  ...     def load(self, f):
+  ...         self.context.payload = int(f.read())
+
+Let's test our adapter::
+
+  >>> from StringIO import StringIO
+  >>> f= StringIO()
+  >>> ItemSynchronizer(item).dump(f)
+  >>> f.getvalue()
+  '1\n'
+
+Let's register the adapter factory as a named utility as described 
+in the README.txt::
+
+  >>> zope.component.provideUtility(ItemSynchronizer, 
+  ...       provides=interfaces.ISynchronizerFactory,
+  ...       name=synchronizer.dottedname(Item)) 
+
+We can now use the adapter::
+
+  >>> f = StringIO()
+  >>> synchronizer.getSynchronizer(item).dump(f)
+  >>> f.getvalue()
+  '1\n'
+
+
+Export persistent state to version control system checkout
+----------------------------------------------------------
+
+As part of the synchronization procedure we need the ability to export
+persistent python objects to the version control checkout directory in
+the form of files and directories. 
+ 
+Content is assumed to consist of two types of objects:
+
+* containers. These are represented as directories on the filesystem.
+
+* items. These are represented as files on the filesystem. The files
+  will have an extension to indicate the type of item.
+
+Let's imagine we have this object structure consisting of a container
+with some items and sub-containers in it::
+
+  >>> class Container(dict):
+  ...     pass
+  
+We can use the default container synchronizer as a base class. Our 
+container synchronizer adds a file extension to the filenames. 
+This extension is also taken into account on import::
+
+  >>> class ContainerSynchronizer(synchronizer.DirectorySynchronizer):
+  ...     def iteritems(self):
+  ...         for key, value in self.context.items():
+  ...             if isinstance(value, Container):
+  ...                 yield key, value
+  ...             else:
+  ...                 yield key + '.test', value
+  ...     def __setitem__(self, name, obj):
+  ...         if name.endswith('.test'):
+  ...             self.context[name[:-5]] = obj
+  ...         else:
+  ...             self.context[name] = obj
+  ...     def __delitem__(self, name):
+  ...         if name.endswith('.test'):
+  ...             del self.context[name[:-5]]
+  ...         else:
+  ...             del self.context[name]
+  
+  >>> zope.component.provideUtility(ContainerSynchronizer,
+  ...                             interfaces.ISynchronizerFactory,
+  ...                             name=synchronizer.dottedname(Container))
+
+Now we can build an example structure::
+
+  >>> data = Container()
+  >>> data['foo'] = Item(payload=1)
+  >>> data['bar'] = Item(payload=2)
+  >>> data['sub'] = Container()
+  >>> data['sub']['qux'] = Item(payload=3)
+
+This object structure has some test payload data::
+
+  >>> data['foo'].payload
+  1
+  >>> data['sub']['qux'].payload
+  3
+
+We have a SVN repository in testpath on the filesystem::
+
+  >>> from zope.fssync import svn, task
+  >>> checkoutdir = svn_test_checkout()
+  >>> svnrepository = svn.SVNRepository(checkoutdir)
+  
+  >>> export = task.Checkout(synchronizer.getSynchronizer, svnrepository)
+
+The object structure can now be saved into that repository::
+
+  >>> export.perform(data, 'root')
+
+The filesystem should now contain the right objects.
+
+Everything is always saved in a directory called ``root``:
+ 
+  >>> root = checkoutdir.join('root')
+  >>> root.check(dir=True)
+  True
+
+This root directory should contain the right objects::
+
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['bar.test', 'foo.test', 'sub']
+
+We expect the right contents in ``bar.test`` and ``foo.test``::
+
+  >>> root.join('bar.test').read()
+  '2\n'
+  >>> root.join('foo.test').read()
+  '1\n'
+
+``sub`` is a container so should be represented as a directory::
+
+  >>> sub_path = root.join('sub')
+  >>> sub_path.check(dir=True)
+  True
+
+  >>> sorted([entry.basename for entry in sub_path.listdir()])
+  ['qux.test']
+
+  >>> sub_path.join('qux.test').read()
+  '3\n'
+
+We know that no existing files or directories were deleted by this save,
+as the checkout was empty before this::
+
+  >>> svnrepository.changes().deleted
+  []
+
+We also know that certain files have been added::
+
+  >>> rel_paths(checkoutdir, svnrepository.changes().added)
+  ['/root', '/root/bar.test', '/root/foo.test', '/root/sub', '/root/sub/qux.test']
+
+
+Modifying an existing checkout
+------------------------------
+
+Now let's assume that the version control checkout is that as
+generated by step 1a). We will bring it to its initial state first::
+
+  >>> svnrepository.clear()
+
+We will now change some data in the ZODB again to test whether we
+detect additions and deletions (we need to inform the version control
+system about these).
+
+Let's add ``hoi``::
+  
+  >>> data['hoi'] = Item(payload=4)
+
+And let's delete ``bar``::
+
+  >>> del data['bar']
+
+Let's save the object structure again to the same checkout::
+  
+  >>> export.perform(data, 'root')
+
+The checkout will now know which files were added and deleted during
+the save::
+
+  >>> changes = svnrepository.changes()
+  >>> rel_paths(checkoutdir, changes.added)
+  ['/root/hoi.test']
+
+We also know which files got deleted::
+
+  >>> rel_paths(checkoutdir, changes.deleted)
+  ['/root/bar.test']
+  
+  >>> changes.accept()
+
+
+Modifying an existing checkout, some edge cases
+-----------------------------------------------
+
+Let's take our checkout as one fully synched up again::
+
+  >>> svnrepository.clear()
+
+The ZODB has changed again.  Item 'hoi' has changed from an item into
+a container::
+
+  >>> del data['hoi']
+  >>> data['hoi'] = Container()
+
+We put some things into the container::
+
+  >>> data['hoi']['something'] = Item(payload=15)
+
+We export again into the existing checkout (which still has 'hoi' as a
+file)::
+
+  >>> export.perform(data, 'root')
+
+The file ``hoi.test`` should now be removed::
+
+  >>> changes = svnrepository.changes()
+  >>> rel_paths(checkoutdir, changes.deleted)
+  ['/root/hoi.test']
+
+And the directory ``hoi`` should now be added::
+
+  >>> rel_paths(checkoutdir, changes.added)
+  ['/root/hoi', '/root/hoi/something.test']
+  
+Let's check the filesystem state::
+
+  >>> changes.accept()
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['foo.test', 'hoi', 'sub']
+
+We expect ``hoi`` to contain ``something.test``::
+
+  >>> hoi_path = root.join('hoi')
+  >>> something_path = hoi_path.join('something.test')
+  >>> something_path.read()
+  '15\n'
+
+Let's now consider the checkout synched up entirely again::
+
+  >>> svnrepository.clear()
+
+Let's now change the ZODB again and change the ``hoi`` container back
+into a file::
+
+  >>> del data['hoi']
+  >>> data['hoi'] = Item(payload=16)
+  >>> export.perform(data, 'root')
+
+The ``hoi`` directory (and everything in it) is now deleted::
+
+  >>> changes = svnrepository.changes()
+  >>> rel_paths(checkoutdir, changes.deleted)
+  ['/root/hoi', '/root/hoi/something.test']
+
+We have added ``hoi.test``::
+
+  >>> rel_paths(checkoutdir, changes.added)
+  ['/root/hoi.test']
+
+We expect to see a ``hoi.test`` but no ``hoi`` directory anymore::
+  
+  >>> changes.accept()
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['foo.test', 'hoi.test', 'sub']
+
+Let's be synched-up again::
+
+  >>> svnrepository.clear()
+
+Note: creating a container with the name ``hoi.test`` (using the
+``.test`` postfix) will lead to trouble now, as we already have a file
+``hoi.test``. ``svn`` doesn't allow a single-step replace of a file
+with a directory - as expressed earlier, an ``svn up`` would need to
+be issued first, but this would be too early in the process. Solving
+this problem is quite involved. Instead, we require the application to
+avoid creating any directories with a postfix in use by items. The
+following should be forbidden::
+
+  data['hoi.test'] = Container()
+
+
+loading a checkout state into python objects
+--------------------------------------------
+
+Since we have no metadata with factory declarations we rely solely
+on file extensions for the creation of content objects. To ensure this
+behavior we must provide our own IFileGenerator and IDirectoryGenerator
+utilities:
+
+  >>> class ItemGenerator(object):
+  ...    zope.interface.implements(interfaces.IFileGenerator)
+  ...    def create(self, location, name, extension):
+  ...        if extension == '.test':
+  ...            return Item(0)
+  ...        raise
+  ...    def load(self, obj, readable):
+  ...        obj.payload = int(readable.read())
+
+  >>> class ContainerGenerator(object):
+  ...    zope.interface.implements(interfaces.IDirectoryGenerator)
+  ...    def create(self, location, name):
+  ...        return Container()
+
+  >>> zope.component.provideUtility(ItemGenerator(),
+  ...    provides=interfaces.IFileGenerator)
+  >>> zope.component.provideUtility(ContainerGenerator(),
+  ...    provides=interfaces.IDirectoryGenerator)
+
+We have registered enough. Let's load up the contents from the
+filesystem now::
+
+  >>> container1 = Container()
+  
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+
+  >>> container2 = container1['root']
+  >>> sorted(container2.keys())
+  ['foo', 'hoi', 'sub']
+
+We check whether the items contains the right information::
+
+  >>> isinstance(container2['foo'], Item)
+  True
+  >>> container2['foo'].payload
+  1
+  >>> isinstance(container2['hoi'], Item)
+  True
+  >>> container2['hoi'].payload
+  16
+  >>> isinstance(container2['sub'], Container)
+  True
+  >>> sorted(container2['sub'].keys())
+  ['qux']
+  >>> container2['sub']['qux'].payload
+  3
+
+
+version control changes a file
+------------------------------
+
+Now we synchronize our checkout by synchronizing the checkout with the
+central coordinating server (or shared branch in case of a distributed
+version control system). We do a ``svnrepository.up()`` that causes the
+text in a file to be modified.
+
+We monkey patch the ``svnrepository.up()`` method with a 
+``update_function`` during an update. This function should then
+simulate what might happen during a version control system ``update``
+operation. Let's define one here that modifies text in a file like 
+an original ``up``
+
+  >>> hoi_path = root.join('hoi.test')
+  >>> def update_function():
+  ...    hoi_path.write('200\n')
+  >>> svnrepository.up = update_function
+
+  >>> svnrepository.up()
+
+We will reload the checkout into Python objects::
+
+  >>> load.perform(container1, 'root', 'root')
+ 
+We expect the ``hoi`` object to be modified::
+
+  >>> container2['hoi'].payload
+  200
+
+
+version control adds a file
+---------------------------
+
+We update our checkout again and cause a file to be added::
+
+  >>> hallo = root.join('hallo.test').ensure()
+  >>> def update_function():
+  ...   hallo.write('300\n')
+  >>> svnrepository.up = update_function
+
+  >>> svnrepository.up()
+
+We will reload the checkout into Python objects again (we must create
+a new task.Commit instance to ensure that the metadata are up to date)::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+ 
+We expect there to be a new object ``hallo``::
+
+  >>> 'hallo' in container2.keys()
+  True
+
+
+version control removes a file
+------------------------------
+
+We update our checkout and cause a file to be removed (ToDo: on update
+we must ensure that the metadata entry is marked as removed)::
+
+  >>> def update_function():
+  ...   path = root.join('hallo.test')
+  ...   path.remove()
+ 
+  >>> svnrepository.up = update_function
+  >>> svnrepository.up()
+
+We will reload the checkout into Python objects::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+
+We expect the object ``hallo`` to be gone again::
+
+  >>> 'hallo' in container2.keys()
+  False
+
+
+version control adds a directory
+--------------------------------
+
+We update our checkout and cause a directory (with a file inside) to be
+added::
+
+  >>> newdir_path = root.join('newdir')
+  >>> def update_function():
+  ...   newdir_path.ensure(dir=True)
+  ...   newfile_path = newdir_path.join('newfile.test').ensure()
+  ...   newfile_path.write('400\n')
+  >>> svnrepository.up = update_function
+  
+  >>> svnrepository.up()
+
+Reloading this will cause a new container to exist::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+
+  >>> 'newdir' in container2.keys()
+  True
+  >>> isinstance(container2['newdir'], Container)
+  True
+  >>> container2['newdir']['newfile'].payload
+  400
+
+
+version control removes a directory
+-----------------------------------
+
+We update our checkout once again and cause a directory to be removed::
+
+  >>> def update_function():
+  ...   newdir_path.remove()
+  >>> svnrepository.up = update_function
+
+  >>> svnrepository.up()
+
+
+Reloading this will cause the new container to be gone again::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+  >>> 'newdir' in container2.keys()
+  False
+
+
+version control changes a file into a directory
+-----------------------------------------------
+
+Some sequence of actions by other users has caused a name that previously
+referred to a file to now refer to a directory::
+
+  >>> hoi_path2 = root.join('hoi')
+  >>> def update_function():
+  ...   hoi_path.remove()
+  ...   hoi_path2.ensure(dir=True)
+  ...   some_path = hoi_path2.join('some.test').ensure(file=True)
+  ...   some_path.write('1000\n')
+  >>> svnrepository.up = update_function
+
+  >>> svnrepository.up()
+
+Reloading this will cause a new container to be there instead of the file::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+  >>> isinstance(container2['hoi'], Container)
+  True
+  >>> container2['hoi']['some'].payload
+  1000
+
+
+version control changes a directory into a file
+-----------------------------------------------
+
+Some sequence of actions by other users has caused a name that
+previously referred to a directory to now refer to a file::
+
+  >>> def update_function():
+  ...   hoi_path2.remove()
+  ...   hoi_path = root.join('hoi.test').ensure()
+  ...   hoi_path.write('2000\n')
+  >>> svnrepository.up = update_function
+
+  >>> svnrepository.up()
+
+Reloading this will cause a new item to be there instead of the
+container::
+
+  >>> load = task.Commit(synchronizer.getSynchronizer, svnrepository)
+  >>> load.perform(container1, 'root', 'root')
+  >>> isinstance(container2['hoi'], Item)
+  True
+  >>> container2['hoi'].payload
+  2000
+
+
+
+Complete synchronization
+------------------------
+
+Let's now exercise the ``sync`` method directly. First we'll modify
+the payload of the ``hoi`` item::
+
+  >>> container2['hoi'].payload = 3000
+  
+Next, we will add a new ``alpha`` file to the checkout when we do an
+``up()``, so again we simulate the actions of our version control system::
+
+  >>> def update_function():
+  ...   alpha_path = root.join('alpha.test').ensure()
+  ...   alpha_path.write('4000\n')
+  ...   alpha_path.add()
+  >>> svnrepository.up = update_function
+
+Now we'll synchronize with the memory structure::
+
+  >>> sync = svn.SVNSyncTask(synchronizer.getSynchronizer, svnrepository)
+  >>> sync.perform(container1, 'root')
+
+We expect the checkout to reflect the changed state of the ``hoi`` object::
+
+  >>> root.join('hoi.test').read()
+  '3000\n'
+
+We also expect the database to reflect the creation of the new
+``alpha`` object::
+
+  >>> container2['alpha'].payload
+  4000
+
+
+To Dos
+------
+
+@@Zope files and directories should not be involved at all
+

Modified: zope.fssync/trunk/src/zope/fssync/task.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/task.py	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/src/zope/fssync/task.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -97,8 +97,6 @@
         return sorted(result)
 
     def dump(self, synchronizer, path):
-    
-        
         if synchronizer is None:
             return
         if interfaces.IDirectorySynchronizer.providedBy(synchronizer):
@@ -180,11 +178,11 @@
     
     def __init__(self, getSynchronizer, repository):
         super(Commit, self).__init__(getSynchronizer, repository)
-        self.metadata = repository.getMetadata()
+        self.metadata = self.repository.getMetadata()
 
     def perform(self, container, name, fspath):
         self.synchronize(container, name, fspath)
-        
+
     def synchronize(self, container, name, fspath):
         """Synchronize an object or object tree from a repository.
 
@@ -192,6 +190,7 @@
         corrected by a update operation, including invalid object
         names.
         """
+        
         self.context = container
         modifications = []
         if invalidName(name):
@@ -239,7 +238,10 @@
                         modifications.append(modified)
                         
             if modifications:
-                zope.event.notify(zope.lifecycleevent.ObjectModifiedEvent(obj, *modifications))
+                zope.event.notify(
+                    zope.lifecycleevent.ObjectModifiedEvent(
+                        obj, 
+                        *modifications))
 
 
     def synchSpecials(self, fspath, specials):
@@ -281,6 +283,7 @@
             
         # Sort the list of keys for repeatability
         names_paths = nameset.items()
+            
         names_paths.sort()
         subdirs = []
         # Do the non-directories first.
@@ -299,6 +302,14 @@
         """Helper to synchronize a new object."""
         entry = self.metadata.getentry(fspath)
         if entry:
+            # In rare cases (e.g. if the original name and replicated name
+            # differ and the replica has been deleted) we can get 
+            # something apparently new that is marked for deletion. Since the
+            # names are provided by the synchronizer we must at least
+            # inform the synchronizer.
+            if entry.get("flag") == "removed":
+                self.deleteItem(container, name)
+                return
             obj = self.createObject(container, name, entry, fspath)
             synchronizer = self.getSynchronizer(obj)
             if interfaces.IDirectorySynchronizer.providedBy(synchronizer):
@@ -388,9 +399,11 @@
             obj = unpickler.load(fp)
         else:
             if isdir:
-                generator = zope.component.queryUtility(interfaces.IDirectoryGenerator)
+                generator = zope.component.queryUtility(
+                    interfaces.IDirectoryGenerator)
             else:
-                generator = zope.component.queryUtility(interfaces.IFileGenerator)
+                generator = zope.component.queryUtility(
+                    interfaces.IFileGenerator)
                 isuffix = name.rfind(".")
                 if isuffix >= 0:
                     suffix = name[isuffix:]
@@ -398,7 +411,8 @@
                     suffix = "."
 
             if generator is None:
-                raise fsutil.Error("Don't know how to create object for %s" % fspath)
+                msg = "Don't know how to create object for %s"
+                raise fsutil.Error(msg % fspath)
 
             if isdir:
                 obj = generator.create(container, name)

Modified: zope.fssync/trunk/src/zope/fssync/tests/test_docs.py
===================================================================
--- zope.fssync/trunk/src/zope/fssync/tests/test_docs.py	2007-07-04 19:06:21 UTC (rev 77426)
+++ zope.fssync/trunk/src/zope/fssync/tests/test_docs.py	2007-07-04 19:27:20 UTC (rev 77427)
@@ -17,12 +17,13 @@
 """
 import sys
 import unittest
+import tempfile
 import zope
+import py
+#import shutil
 
 from zope import interface
-from zope.testing import doctest
-from zope.testing import doctestunit
-from zope.testing import module
+from zope.testing import doctest, doctestunit, module, cleanup
 
 
 from zope.traversing.interfaces import IContainmentRoot
@@ -30,13 +31,38 @@
 
 from zope.fssync import pickle
 
+_test_dirs = []
+
+def cleanUpZope(test):
+    for wcdir in _test_dirs:
+        wcdir.remove()
+    cleanup.cleanUp()
+
+def svn_test_checkout():
+    base = py.path.local(__file__).dirpath('svntestdir')
+    pat = 'test%s'
+    count = 1
+    while base.join(pat % count).check():
+        count += 1
+    name = pat % count
+    wcdir = py.path.svnwc(base).mkdir(name)
+    _test_dirs.append(wcdir)
+    return wcdir
+
+def rel_paths(checkout, paths):
+    result = []
+    start = len(str(checkout))
+    for path in paths:
+        result.append(str(path)[start:])
+    return sorted(result)
+
 def setUp(test):
     module.setUp(test, 'zope.fssync.doctest')
 
 def tearDown(test):
     module.tearDown(test, 'zope.fssync.doctest')
+    cleanUpZope(test)
 
-
 class PersistentLoaderTestCase(unittest.TestCase):
 
     def setUp(self):
@@ -70,7 +96,9 @@
 def test_suite():
 
     globs = {'zope':zope,
-            'pprint': doctestunit.pprint}
+            'pprint': doctestunit.pprint,
+            'svn_test_checkout': svn_test_checkout,
+            'rel_paths': rel_paths}
 
     flags = doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS
     suite = unittest.TestSuite()
@@ -93,6 +121,12 @@
                                             globs=globs,
                                             setUp=setUp, tearDown=tearDown,
                                             optionflags=flags))
+
+    suite.addTest(doctest.DocFileSuite('../svn.txt',
+                                            globs=globs,
+                                            setUp=setUp, tearDown=tearDown,
+                                            optionflags=flags))
+
     return suite
 
 if __name__ == '__main__':



More information about the Checkins mailing list