propmerging_sanity.txt   [plain text]

When merging property lists together during 'svn up' and 'svn merge',
things can get tricky.  In the case of file texts, we have a nice
diff3 algorithm to do a 3-way merge for us.  But for properties,
libsvn_wc needs to do the 3-way merge itself.  This document explains
what's going on.

'svn update'

The old-baseprops live in .svn/.

During update, the server sends a bunch of 'propset' commands of the
form {name, val}.  (val == NULL implies a deletion.  If name doesn't
already exist, it implies an addition.)

The propsets are applied to old-baseprops, producing new-baseprops.

Meanwhile, the user may have made local edits to the workingprops.

So our ancestry diagram looks like:

                    /             \
 (server-propsets) /               \ (local-edit-propsets)
                  /                 \
                 v                   v
          new-baseprops             workingprops

Because the old-baseprops are a common ancestor, the 3-way merge
algorithm is relatively simple.  All we need to do is compare two
lists of 'propsets':

    1. the propset list sent from the server (which transforms
       old-baseprops into new-baseprops)

    2. the propset list representing local edits (which transforms
       old-baseprops into workingprops)

If there are no local edits at all (i.e. the 2nd list is empty), then
we simply apply the first list to workingprops, and we're done.  No
conflicts can possibly happen; the common ancestor here guarantees
that workingprops and new-baseprops will be identical when we're done.

If there are local edits (i.e. the 2nd list is non-empty), then

       foreach propset in server-propsets:
          if propname exists in localedit-propsets:
             compare intentions of the 2 propsets, possibly mark conflict.
             apply propset to workingprops.

Note that because of the common ancestor here, the *only way* property
conflicts can happen is if local-mods are present.

'svn merge'

This is a harder problem, because we're not guaranteed a common

During the merge command, the server sends the complete list of
old-baseprops from the 'left' side of the merge, as well as a list of
propsets which can be applied to get the new-baseprops (which
represent the 'right' side of the merge):

                    |  (server-propsets)

But the target of the merge could be *anything*.  It has a completely
unrelated set of baseprops, no common ancestor.

     old-baseprops                    target-baseprops
          |                                 |         
          |  (server-propsets)              |  (localedit-propsets)
          |                                 |                      
          v                                 v
     new-baseprops                      workingprops

So for a correct 3-way merge, our algorithm needs to be different.  We
can't blindly apply the server propsets to the workingprops, we need
to carefully look at the server-propsets as *deltas*, noticing the
"from" and "to" values.

The upshot here is that while 'svn update' can *only* produce
conflicts when local-mods are present, 'svn merge' can produce
conflicts anytime, even in the absence of local-mods.

After some discussion on the dev@ list, we've decided to implement the
following algorithm.  It works correctly for 'svn merge', because it
makes no assumption about a common ancestor.  The FROM variable
represents the old-baseprops coming from the server.  The algorithm
still works for 'svn up', of course;  in that case, the FROM variable
simply represents the value of target-baseprops.

      foreach propset in server-propsets:

         /* we have old-baseprops, so we can do this */
         convert into propdelta of the form (PROPNAME, FROM, TO).

         if FROM == NULL:  /* adding a new property */

            if PROPNAME exists in workingprops:
               if (value of PROPNAME in workingprops) == TO:
                  do nothing, it's a 'clean merge'
                  conflict: "property already exists with different value" 

            else:  /* PROPNAME doesn't exist in workingprops */
               set property as a new local mod

         else: /* FROM != NULL -- changing or deleting a property */

            if PROPNAME doesn't exist in workingprops:
               print "skipped: cannot change/delete non-existent property"

            else:  /* PROPNAME exists in workingprops */

               if (value of PROPNAME in workingprops) == FROM:
                  /* note: this may destroy existing local mods,
                     because target-baseprops are being ignored.  */
                  apply the propchange.

               else if (value of PROPNAME in workingprops) == TO:
                  do nothing, it's a 'clean merge'  

               else:  /* has some other  value */
                  conflict:  "property has conflicting value"