[Checkins] SVN: z3c.vcsync/trunk/ Fix a bug where in some scenarios the svn R flag was generated (replaced),

Martijn Faassen faassen at infrae.com
Tue Aug 19 13:33:27 EDT 2008


Log message for revision 90016:
  Fix a bug where in some scenarios the svn R flag was generated (replaced),
  which isn't recognized by Py 0.9.1.
  

Changed:
  U   z3c.vcsync/trunk/CHANGES.txt
  U   z3c.vcsync/trunk/setup.py
  U   z3c.vcsync/trunk/src/z3c/vcsync/__init__.py
  U   z3c.vcsync/trunk/src/z3c/vcsync/conflict.txt
  A   z3c.vcsync/trunk/src/z3c/vcsync/monkey.py
  U   z3c.vcsync/trunk/src/z3c/vcsync/vc.py

-=-
Modified: z3c.vcsync/trunk/CHANGES.txt
===================================================================
--- z3c.vcsync/trunk/CHANGES.txt	2008-08-19 16:54:33 UTC (rev 90015)
+++ z3c.vcsync/trunk/CHANGES.txt	2008-08-19 17:33:26 UTC (rev 90016)
@@ -4,8 +4,16 @@
 0.15 (unreleased)
 -----------------
 
-* ...
+* Fix a bug where the SVN "R" status was not recognized by the py lib.
+  Monkey-patch py for now, though a fix should be released with py
+  0.9.2 eventually. Strictly depend on py 0.9.1 for now to make sure
+  monkey-patch applies cleanly.
 
+* A bit of refactoring of duplicated code in retrieving the objects
+  modified and objects removed lists. Still not happy that this gets
+  called twice per synchronization, but it was already doing this so
+  doesn't get worse either.
+
 0.14 (2008-07-04)
 -----------------
 

Modified: z3c.vcsync/trunk/setup.py
===================================================================
--- z3c.vcsync/trunk/setup.py	2008-08-19 16:54:33 UTC (rev 90015)
+++ z3c.vcsync/trunk/setup.py	2008-08-19 17:33:26 UTC (rev 90016)
@@ -25,7 +25,7 @@
       install_requires=[
         'setuptools',
         'grok',
-        'py >= 0.9.1',
+        'py == 0.9.1',
       ],
       license="ZPL 2.1",
       entry_points="""

Modified: z3c.vcsync/trunk/src/z3c/vcsync/__init__.py
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/__init__.py	2008-08-19 16:54:33 UTC (rev 90015)
+++ z3c.vcsync/trunk/src/z3c/vcsync/__init__.py	2008-08-19 17:33:26 UTC (rev 90016)
@@ -1,3 +1,7 @@
-from vc import Synchronizer
-from importexport import (export, import_,
-                          export_zip, import_zip)
+from z3c.vcsync.vc import Synchronizer
+from z3c.vcsync.importexport import (export, import_,
+                                     export_zip, import_zip)
+
+from z3c.vcsync.monkey import apply_monkey_patches
+apply_monkey_patches()
+

Modified: z3c.vcsync/trunk/src/z3c/vcsync/conflict.txt
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/conflict.txt	2008-08-19 16:54:33 UTC (rev 90015)
+++ z3c.vcsync/trunk/src/z3c/vcsync/conflict.txt	2008-08-19 17:33:26 UTC (rev 90016)
@@ -391,6 +391,55 @@
   >>> found2['data']['folder']['content'].payload
   15
 
+Adding an object that was just removed: Py error
+------------------------------------------------
+
+In some cases an object that was removed in a state is later re-added,
+such as in the following scenario:
+
+* there is an object 'testing'
+ 
+* synchronized
+
+* the object is removed
+
+* a new object is created with the same name, 'testing'
+  
+* resynchronization
+
+The result is an error in the Py lib that the svn status code ``R``
+cannot be processed. 
+
+The interface of ``IState`` states that it is safe for the ``removed``
+method to return objects that were removed but were since then
+re-added (and thus also appear in the ``objects`` list). Unfortunately
+the 
+
+This is however not the underlying error. The true error is that the
+object got added to the removed list of the state while it is also in
+the modified list of the state (as it was re-added). Let's demonstrate
+the error::
+
+  >>> current_synchronizer = s1
+  >>> data1['testing'] = Item(200)
+  >>> info = s1.sync("synchronize")
+
+We claim the object is removed::
+
+  >>> state1._removed.append(
+  ...    get_object_path(root1, data1['testing']))
+  >>> del data1['testing']
+
+But we also claim it is modified (the test state declares everything
+modified)::
+
+  >>> data1['testing'] = Item(250)
+
+The result is an error when synchronizing::
+
+  >>> info = s1.sync("synchronize")
+
+
 Conflicting directory conflicts
 -------------------------------
 

Added: z3c.vcsync/trunk/src/z3c/vcsync/monkey.py
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/monkey.py	                        (rev 0)
+++ z3c.vcsync/trunk/src/z3c/vcsync/monkey.py	2008-08-19 17:33:26 UTC (rev 90016)
@@ -0,0 +1,130 @@
+
+def apply_monkey_patches():
+    """Apply all monkey-patches
+    """
+    svn_r_status_support()
+
+def svn_r_status_support():
+    """monkey-patch py 0.9.1 with support for SVN 'R' status flag.
+
+    this should be fixed in py 0.9.2, so the monkey patch should go
+    away when we start using that.
+    """
+    from py.__.path.svn import wccommand
+    wccommand.WCStatus.attrnames = wccommand.WCStatus.attrnames + ('replaced',)
+    wccommand.SvnWCCommandPath.status = status
+
+from py.__.path.svn.wccommand import WCStatus
+
+# taken from py.path.svn.wccommand
+def status(self, updates=0, rec=0, externals=0):
+    """ return (collective) Status object for this file. """
+    # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
+    #             2201     2192        jum   test
+    # XXX
+    if externals:
+        raise ValueError("XXX cannot perform status() "
+                         "on external items yet")
+    else:
+        #1.2 supports: externals = '--ignore-externals'
+        externals = ''
+    if rec:
+        rec= ''
+    else:
+        rec = '--non-recursive'
+
+    # XXX does not work on all subversion versions
+    #if not externals: 
+    #    externals = '--ignore-externals' 
+
+    if updates:
+        updates = '-u'
+    else:
+        updates = ''
+
+    update_rev = None
+
+    cmd = 'status -v %s %s %s' % (updates, rec, externals)
+    out = self._authsvn(cmd)
+    rootstatus = WCStatus(self)
+    for line in out.split('\n'):
+        if not line.strip():
+            continue
+        #print "processing %r" % line
+        flags, rest = line[:8], line[8:]
+        # first column
+        c0,c1,c2,c3,c4,c5,x6,c7 = flags
+        #if '*' in line:
+        #    print "flags", repr(flags), "rest", repr(rest)
+
+        if c0 in '?XI':
+            fn = line.split(None, 1)[1]
+            if c0 == '?':
+                wcpath = self.join(fn, abs=1)
+                rootstatus.unknown.append(wcpath)
+            elif c0 == 'X':
+                wcpath = self.__class__(self.localpath.join(fn, abs=1),
+                                        auth=self.auth)
+                rootstatus.external.append(wcpath)
+            elif c0 == 'I':
+                wcpath = self.join(fn, abs=1)
+                rootstatus.ignored.append(wcpath)
+
+            continue
+
+        #elif c0 in '~!' or c4 == 'S':
+        #    raise NotImplementedError("received flag %r" % c0)
+
+        m = self._rex_status.match(rest)
+        if not m:
+            if c7 == '*':
+                fn = rest.strip()
+                wcpath = self.join(fn, abs=1)
+                rootstatus.update_available.append(wcpath)
+                continue
+            if line.lower().find('against revision:')!=-1:
+                update_rev = int(rest.split(':')[1].strip())
+                continue
+            # keep trying
+            raise ValueError, "could not parse line %r" % line
+        else:
+            rev, modrev, author, fn = m.groups()
+        wcpath = self.join(fn, abs=1)
+        #assert wcpath.check()
+        if c0 == 'M':
+            assert wcpath.check(file=1), "didn't expect a directory with changed content here"
+            rootstatus.modified.append(wcpath)
+        elif c0 == 'A' or c3 == '+' :
+            rootstatus.added.append(wcpath)
+        elif c0 == 'D':
+            rootstatus.deleted.append(wcpath)
+        elif c0 == 'C':
+            rootstatus.conflict.append(wcpath)
+        # XXX following two lines added by monkey-patch
+        elif c0 == 'R':
+            rootstatus.replaced.append(wcpath)
+        elif c0 == '~':
+            rootstatus.kindmismatch.append(wcpath)
+        elif c0 == '!':
+            rootstatus.incomplete.append(wcpath)
+        elif not c0.strip():
+            rootstatus.unchanged.append(wcpath)
+        else:
+            raise NotImplementedError("received flag %r" % c0)
+
+        if c1 == 'M':
+            rootstatus.prop_modified.append(wcpath)
+        # XXX do we cover all client versions here?
+        if c2 == 'L' or c5 == 'K':
+            rootstatus.locked.append(wcpath)
+        if c7 == '*':
+            rootstatus.update_available.append(wcpath)
+
+        if wcpath == self:
+            rootstatus.rev = rev
+            rootstatus.modrev = modrev
+            rootstatus.author = author
+            if update_rev:
+                rootstatus.update_rev = update_rev
+            continue
+    return rootstatus

Modified: z3c.vcsync/trunk/src/z3c/vcsync/vc.py
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/vc.py	2008-08-19 16:54:33 UTC (rev 90015)
+++ z3c.vcsync/trunk/src/z3c/vcsync/vc.py	2008-08-19 17:33:26 UTC (rev 90016)
@@ -104,12 +104,10 @@
 
     def sync(self, message='', modified_function=None):
         revision_nr = self.state.get_revision_nr()
-        # store these to report in SynchronizationInfo below
-        objects_removed = list(self.state.removed(revision_nr))
-        root = self.state.root
-        objects_changed = [get_object_path(root, obj) for obj in
-                           self.state.objects(revision_nr)]
-        # now save the state to the checkout
+        # store some information for the synchronization info
+        objects_changed, object_paths_changed, object_paths_removed =\
+                         self._get_changed_removed(revision_nr)
+        # save the state to the checkout
         self.save(revision_nr)
         # update the checkout
         self.checkout.up()
@@ -141,15 +139,37 @@
         
         # we store the new revision number in the state
         self.state.set_revision_nr(revision_nr)
-        # and we return some informatino about happened
+        # and we return some information about what happened
         return SynchronizationInfo(revision_nr,
-                                   objects_removed, objects_changed,
+                                   object_paths_removed, object_paths_changed,
                                    files_removed, files_changed)
 
+    def _get_changed_removed(self, revision_nr):
+        """Construct the true lists of objects that are changed and removed.
+
+        Returns tuple with:
+
+        * actual objects changed
+        * object paths changed
+        * object paths removed
+        """
+        # store these to report in SynchronizationInfo below
+        object_paths_removed = list(self.state.removed(revision_nr))
+        root = self.state.root
+        objects_changed = list(self.state.objects(revision_nr))
+        object_paths_changed = [get_object_path(root, obj) for obj in
+                           objects_changed]
+        return objects_changed, object_paths_changed, object_paths_removed
+        
     def save(self, revision_nr):
+        """Save objects to filesystem.
+        """
+        objects_changed, object_paths_changed, object_paths_removed =\
+                         self._get_changed_removed(revision_nr)
+        
         # remove all files that have been removed in the database
         path = self.checkout.path
-        for removed_path in self.state.removed(revision_nr):
+        for removed_path in object_paths_removed:
             # construct path to directory containing file/dir to remove
             # note: this is a state-specific path which always uses /, so we
             # shouldn't use os.path.sep here
@@ -178,10 +198,10 @@
 
         # now save all files that have been modified/added
         root = self.state.root
-        for obj in self.state.objects(revision_nr):
+        for obj in objects_changed:
             if obj is not root:
                 IDump(obj).save(self._get_container_path(root, obj))
-
+  
     def load(self, revision_nr):
         # remove all objects that have been removed in the checkout
         root = self.state.root



More information about the Checkins mailing list