You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by st...@apache.org on 2012/04/18 17:42:14 UTC

svn commit: r1327547 - in /jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk: core/ model/ store/

Author: stefan
Date: Wed Apr 18 15:42:14 2012
New Revision: 1327547

URL: http://svn.apache.org/viewvc?rev=1327547&view=rev
Log:
 OAK-45: Add support for branching and merging of private copies to MicroKernel

Added:
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/DiffBuilder.java
Modified:
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java
    jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java Wed Apr 18 15:42:14 2012
@@ -18,9 +18,7 @@ package org.apache.jackrabbit.mk.core;
 
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.mk.api.MicroKernelException;
@@ -30,12 +28,11 @@ import org.apache.jackrabbit.mk.model.Ch
 import org.apache.jackrabbit.mk.model.Commit;
 import org.apache.jackrabbit.mk.model.CommitBuilder;
 import org.apache.jackrabbit.mk.model.CommitBuilder.NodeTree;
+import org.apache.jackrabbit.mk.model.DiffBuilder;
 import org.apache.jackrabbit.mk.model.Id;
 import org.apache.jackrabbit.mk.model.NodeState;
 import org.apache.jackrabbit.mk.model.PropertyState;
 import org.apache.jackrabbit.mk.model.StoredCommit;
-import org.apache.jackrabbit.mk.model.TraversingNodeDiffHandler;
-import org.apache.jackrabbit.mk.store.RevisionProvider;
 import org.apache.jackrabbit.mk.util.CommitGate;
 import org.apache.jackrabbit.mk.util.PathUtils;
 
@@ -209,176 +206,21 @@ public class MicroKernelImpl implements 
     }
 
     public String diff(String fromRevision, String toRevision, String filter) throws MicroKernelException {
-        Id toRevisionId = toRevision == null ? getHeadRevisionId() : Id.fromString(toRevision);
-        
-        return diff(Id.fromString(fromRevision), toRevisionId, filter);
-    }
-    
-    /**
-     * Same as <code>diff</code>, with typed <code>Id</code> arguments instead of strings.
-     * 
-     * @see #diff(String, String, String) 
-     */
-    private String diff(Id fromRevisionId, Id toRevisionId, String filter) throws MicroKernelException {
         // TODO extract and evaluate filter criteria (such as e.g. 'path') specified in 'filter' parameter
         String path = "/";
 
-        try {
-            final JsopBuilder buff = new JsopBuilder();
-            final RevisionProvider rp = rep.getRevisionStore();
-            // maps (key: id of target node, value: path/to/target)
-            // for tracking added/removed nodes; this allows us
-            // to detect 'move' operations
-            final HashMap<Id, String> addedNodes = new HashMap<Id, String>();
-            final HashMap<Id, String> removedNodes = new HashMap<Id, String>();
-            NodeState node1 = rep.getNodeState(fromRevisionId, path);
-            NodeState node2 = rep.getNodeState(toRevisionId, path);
-
-            if (node1 == null) {
-                if (node2 != null) {
-                    buff.tag('+').key(path).object();
-                    toJson(buff, node2, Integer.MAX_VALUE, 0, -1, false);
-                    return buff.endObject().newline().toString();
-                } else {
-                    throw new MicroKernelException("path doesn't exist in the specified revisions: " + path);
-                }
-            } else if (node2 == null) {
-                buff.tag('-');
-                buff.value(path);
-                return buff.newline().toString();
-            }
-
-            TraversingNodeDiffHandler diffHandler = new TraversingNodeDiffHandler(rp) {
-                @Override
-                public void propertyAdded(PropertyState after) {
-                    buff.tag('+').
-                            key(PathUtils.concat(getCurrentPath(), after.getName())).
-                            encodedValue(after.getEncodedValue()).
-                            newline();
-                }
-
-                @Override
-                public void propertyChanged(PropertyState before, PropertyState after) {
-                    buff.tag('^').
-                            key(PathUtils.concat(getCurrentPath(), after.getName())).
-                            encodedValue(after.getEncodedValue()).
-                            newline();
-                }
-
-                @Override
-                public void propertyDeleted(PropertyState before) {
-                    // since property and node deletions can't be distinguished
-                    // using the "- <path>" notation we're representing
-                    // property deletions as "^ <path>:null"
-                    buff.tag('^').
-                            key(PathUtils.concat(getCurrentPath(), before.getName())).
-                            value(null).
-                            newline();
-                }
-
-                @Override
-                public void childNodeAdded(String name, NodeState after) {
-                    addedNodes.put(rp.getId(after), PathUtils.concat(getCurrentPath(), name));
-                    buff.tag('+').
-                            key(PathUtils.concat(getCurrentPath(), name)).object();
-                    toJson(buff, after, Integer.MAX_VALUE, 0, -1, false);
-                    buff.endObject().newline();
-                }
-
-                @Override
-                public void childNodeDeleted(String name, NodeState before) {
-                    removedNodes.put(rp.getId(before), PathUtils.concat(getCurrentPath(), name));
-                    buff.tag('-');
-                    buff.value(PathUtils.concat(getCurrentPath(), name));
-                    buff.newline();
-                }
-            };
-            diffHandler.start(node1, node2, path);
-
-            // check if this commit includes 'move' operations
-            // by building intersection of added and removed nodes
-            addedNodes.keySet().retainAll(removedNodes.keySet());
-            if (!addedNodes.isEmpty()) {
-                // this commit includes 'move' operations
-                removedNodes.keySet().retainAll(addedNodes.keySet());
-                // addedNodes & removedNodes now only contain information about moved nodes
-
-                // re-build the diff in a 2nd pass, this time representing moves correctly
-                buff.resetWriter();
-
-                // TODO refactor code, avoid duplication
-
-                diffHandler = new TraversingNodeDiffHandler(rp) {
-                    @Override
-                    public void propertyAdded(PropertyState after) {
-                        buff.tag('+').
-                                key(PathUtils.concat(getCurrentPath(), after.getName())).
-                                encodedValue(after.getEncodedValue()).
-                                newline();
-                    }
-
-                    @Override
-                    public void propertyChanged(PropertyState before, PropertyState after) {
-                        buff.tag('^').
-                                key(PathUtils.concat(getCurrentPath(), after.getName())).
-                                encodedValue(after.getEncodedValue()).
-                                newline();
-                    }
-
-                    @Override
-                    public void propertyDeleted(PropertyState before) {
-                        // since property and node deletions can't be distinguished
-                        // using the "- <path>" notation we're representing
-                        // property deletions as "^ <path>:null"
-                        buff.tag('^').
-                                key(PathUtils.concat(getCurrentPath(), before.getName())).
-                                value(null).
-                                newline();
-                    }
-
-                    @Override
-                    public void childNodeAdded(String name, NodeState after) {
-                        if (addedNodes.containsKey(rp.getId(after))) {
-                            // moved node, will be processed separately
-                            return;
-                        }
-                        buff.tag('+').
-                                key(PathUtils.concat(getCurrentPath(), name)).object();
-                        toJson(buff, after, Integer.MAX_VALUE, 0, -1, false);
-                        buff.endObject().newline();
-                    }
-
-                    @Override
-                    public void childNodeDeleted(String name, NodeState before) {
-                        if (addedNodes.containsKey(rp.getId(before))) {
-                            // moved node, will be processed separately
-                            return;
-                        }
-                        buff.tag('-');
-                        buff.value(PathUtils.concat(getCurrentPath(), name));
-                        buff.newline();
-                    }
-
-                };
-                diffHandler.start(node1, node2, path);
+        Id toRevisionId = toRevision == null ? getHeadRevisionId() : Id.fromString(toRevision);
 
-                // finally process moved nodes
-                for (Map.Entry<Id, String> entry : addedNodes.entrySet()) {
-                    buff.tag('>').
-                            // path/to/deleted/node
-                            key(removedNodes.get(entry.getKey())).
-                            // path/to/added/node
-                            value(entry.getValue()).
-                            newline();
-                }
-            }
-            return buff.toString();
+        try {
+            NodeState before = rep.getNodeState(Id.fromString(fromRevision), path);
+            NodeState after = rep.getNodeState(toRevisionId, path);
 
+            return new DiffBuilder(before, after, path, rep.getRevisionStore(), filter).build();
         } catch (Exception e) {
             throw new MicroKernelException(e);
         }
     }
-
+    
     public boolean nodeExists(String path, String revisionId) throws MicroKernelException {
         if (rep == null) {
             throw new IllegalStateException("this instance has already been disposed");
@@ -569,6 +411,40 @@ public class MicroKernelImpl implements 
         }
     }
 
+    public String branch(String publicRevisionId) throws MicroKernelException {
+        // create a private branch
+
+        if (rep == null) {
+            throw new IllegalStateException("this instance has already been disposed");
+        }
+
+        Id revId = publicRevisionId == null ? getHeadRevisionId() : Id.fromString(publicRevisionId);
+
+        try {
+            CommitBuilder cb = rep.getCommitBuilder(revId, "");
+            return cb.doCommit(true).toString();
+        } catch (Exception e) {
+            throw new MicroKernelException(e);
+        }
+    }
+
+    public String merge(String privateRevisionId) throws MicroKernelException {
+        // create a private branch
+
+        if (rep == null) {
+            throw new IllegalStateException("this instance has already been disposed");
+        }
+
+        Id revId = Id.fromString(privateRevisionId);
+
+        try {
+            CommitBuilder cb = rep.getCommitBuilder(revId, "");
+            return cb.doMerge().toString();
+        } catch (Exception e) {
+            throw new MicroKernelException(e);
+        }
+    }
+
     public long getLength(String blobId) throws MicroKernelException {
         if (rep == null) {
             throw new IllegalStateException("this instance has already been disposed");

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/AbstractCommit.java Wed Apr 18 15:42:14 2012
@@ -38,6 +38,9 @@ public abstract class AbstractCommit imp
     // id of parent commit
     protected Id parentId;
 
+    // id of branch root commit
+    protected Id branchRootId;
+
     protected AbstractCommit() {
     }
 
@@ -47,6 +50,7 @@ public abstract class AbstractCommit imp
         this.msg = other.getMsg();
         this.changes = other.getChanges();
         this.commitTS = other.getCommitTS();
+        this.branchRootId = other.getBranchRootId();
     }
 
     public Id getParentId() {
@@ -69,11 +73,16 @@ public abstract class AbstractCommit imp
         return changes;
     }
 
+    public Id getBranchRootId() {
+        return branchRootId;
+    }
+
     public void serialize(Binding binding) throws Exception {
         binding.write("rootNodeId", rootNodeId.getBytes());
         binding.write("commitTS", commitTS);
         binding.write("msg", msg == null ? "" : msg);
         binding.write("changes", changes == null ? "" : changes);
         binding.write("parentId", parentId == null ? "" : parentId.toString());
+        binding.write("branchRootId", branchRootId == null ? "" : branchRootId.toString());
     }
 }

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/Commit.java Wed Apr 18 15:42:14 2012
@@ -25,13 +25,25 @@ public interface Commit {
     
     Id getRootNodeId();
 
-    public Id getParentId();
+    Id getParentId();
 
-    public long getCommitTS();
+    long getCommitTS();
 
-    public String getMsg();
+    String getMsg();
 
-    public String getChanges();
+    String getChanges();
+
+    /**
+     * Returns {@code null} if this commit does not represent a branch.
+     * <p/>
+     * Otherwise, returns the id of the branch root commit
+     * (i.e. the <i>public</i> commit that this <i>private</i> branch is based upon).
+     *
+     *
+     * @return the id of the branch root commit or {@code null} if this commit
+     * does not represent a branch.
+     */
+    Id getBranchRootId();
 
     void serialize(Binding binding) throws Exception;
 }

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/CommitBuilder.java Wed Apr 18 15:42:14 2012
@@ -33,6 +33,7 @@ import org.apache.jackrabbit.mk.util.Pat
  */
 public class CommitBuilder {
 
+    /** revision changes are based upon */
     private Id baseRevId;
 
     private final String msg;
@@ -86,47 +87,80 @@ public class CommitBuilder {
     }
 
     public Id /* new revId */ doCommit() throws Exception {
-        if (staged.isEmpty()) {
+        return doCommit(false);
+    }
+
+    public Id /* new revId */ doCommit(boolean createBranch) throws Exception {
+        if (staged.isEmpty() && !createBranch) {
             // nothing to commit
             return baseRevId;
         }
 
-        Id currentHead = store.getHeadCommitId();
-        if (!currentHead.equals(baseRevId)) {
-            // todo gracefully handle certain conflicts (e.g. changes on moved sub-trees, competing deletes etc)
-            // update base revision to new head
-            baseRevId = currentHead;
-            // clear staging area
-            staged.clear();
-            // replay change log on new base revision
-            for (Change change : changeLog) {
-                change.apply();
+        StoredCommit baseCommit = store.getCommit(baseRevId);
+        boolean privateCommit = createBranch || baseCommit.getBranchRootId() != null;
+
+
+        if (!privateCommit) {
+            Id currentHead = store.getHeadCommitId();
+            if (!currentHead.equals(baseRevId)) {
+                // todo gracefully handle certain conflicts (e.g. changes on moved sub-trees, competing deletes etc)
+                // update base revision to new head
+                baseRevId = currentHead;
+                // clear staging area
+                staged.clear();
+                // replay change log on new base revision
+                for (Change change : changeLog) {
+                    change.apply();
+                }
             }
         }
 
-        Id rootNodeId = persistStagedNodes();
+        Id rootNodeId =
+                changeLog.isEmpty() ? baseCommit.getRootNodeId() : persistStagedNodes();
 
         Id newRevId;
-        store.lockHead();
-        try {
-            currentHead = store.getHeadCommitId();
-            if (!currentHead.equals(baseRevId)) {
-                StoredNode baseRoot = store.getRootNode(baseRevId);
-                StoredNode theirRoot = store.getRootNode(currentHead);
-                StoredNode ourRoot = store.getNode(rootNodeId);
 
-                rootNodeId = mergeTree(baseRoot, ourRoot, theirRoot);
+        if (!privateCommit) {
+            store.lockHead();
+            try {
+                Id currentHead = store.getHeadCommitId();
+                if (!currentHead.equals(baseRevId)) {
+                    StoredNode baseRoot = store.getRootNode(baseRevId);
+                    StoredNode theirRoot = store.getRootNode(currentHead);
+                    StoredNode ourRoot = store.getNode(rootNodeId);
 
-                baseRevId = currentHead;
-            }
+                    rootNodeId = mergeTree(baseRoot, ourRoot, theirRoot);
 
-            if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) {
-                // the commit didn't cause any changes,
-                // no need to create new commit object/update head revision
-                return currentHead;
+                    baseRevId = currentHead;
+                }
+
+                if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) {
+                    // the commit didn't cause any changes,
+                    // no need to create new commit object/update head revision
+                    return currentHead;
+                }
+                MutableCommit newCommit = new MutableCommit();
+                newCommit.setParentId(baseRevId);
+                newCommit.setCommitTS(System.currentTimeMillis());
+                newCommit.setMsg(msg);
+                StringBuilder diff = new StringBuilder();
+                for (Change change : changeLog) {
+                    if (diff.length() > 0) {
+                        diff.append('\n');
+                    }
+                    diff.append(change.asDiff());
+                }
+                newCommit.setChanges(diff.toString());
+                newCommit.setRootNodeId(rootNodeId);
+                newCommit.setBranchRootId(null);
+                newRevId = store.putHeadCommit(newCommit);
+            } finally {
+                store.unlockHead();
             }
+        } else {
+            // private commit/branch
             MutableCommit newCommit = new MutableCommit();
-            newCommit.setParentId(baseRevId);
+            newCommit.setParentId(baseCommit.getId());
             newCommit.setCommitTS(System.currentTimeMillis());
             newCommit.setMsg(msg);
             StringBuilder diff = new StringBuilder();
@@ -138,6 +172,57 @@ public class CommitBuilder {
             }
             newCommit.setChanges(diff.toString());
             newCommit.setRootNodeId(rootNodeId);
+            if (createBranch) {
+                newCommit.setBranchRootId(baseCommit.getId());
+            } else {
+                newCommit.setBranchRootId(baseCommit.getBranchRootId());
+            }
+            newRevId = store.putCommit(newCommit);
+        }
+
+        // reset instance
+        staged.clear();
+        changeLog.clear();
+
+        return newRevId;
+    }
+
+    public Id /* new revId */ doMerge() throws Exception {
+        StoredCommit branchCommit = store.getCommit(baseRevId);
+        Id branchRootId = branchCommit.getBranchRootId();
+        if (branchRootId == null) {
+            throw new Exception("can only merge a private branch commit");
+        }
+
+        Id rootNodeId =
+                changeLog.isEmpty() ? branchCommit.getRootNodeId() : persistStagedNodes();
+
+        Id newRevId;
+
+        store.lockHead();
+        try {
+            Id currentHead = store.getHeadCommitId();
+
+            StoredNode baseRoot = store.getRootNode(branchRootId);
+            StoredNode theirRoot = store.getRootNode(currentHead);
+            StoredNode ourRoot = store.getNode(rootNodeId);
+
+            rootNodeId = mergeTree(baseRoot, ourRoot, theirRoot);
+
+            if (store.getCommit(currentHead).getRootNodeId().equals(rootNodeId)) {
+                // the merge didn't cause any changes,
+                // no need to create new commit object/update head revision
+                return currentHead;
+            }
+            MutableCommit newCommit = new MutableCommit();
+            newCommit.setParentId(currentHead);
+            newCommit.setCommitTS(System.currentTimeMillis());
+            newCommit.setMsg(msg);
+            // dynamically build diff of merged commit
+            String diff = new DiffBuilder(store.getNodeState(theirRoot), store.getNodeState(ourRoot), "/", store, "").build();
+            newCommit.setChanges(diff);
+            newCommit.setRootNodeId(rootNodeId);
+            newCommit.setBranchRootId(null);
             newRevId = store.putHeadCommit(newCommit);
         } finally {
             store.unlockHead();
@@ -150,7 +235,7 @@ public class CommitBuilder {
         return newRevId;
     }
 
-    //--------------------------------------------------------< inner classes >
+    //-------------------------------------------------------< implementation >
 
     MutableNode getOrCreateStagedNode(String nodePath) throws Exception {
         MutableNode node = staged.get(nodePath);

Added: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/DiffBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/DiffBuilder.java?rev=1327547&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/DiffBuilder.java (added)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/DiffBuilder.java Wed Apr 18 15:42:14 2012
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.mk.model;
+
+import org.apache.jackrabbit.mk.json.JsopBuilder;
+import org.apache.jackrabbit.mk.util.PathUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JSOP Diff Builder
+ */
+public class DiffBuilder {
+
+    private final NodeState before;
+    private final NodeState after;
+    private final String path;
+    private final String filter;
+    private final NodeStore store;
+
+    public DiffBuilder(NodeState before, NodeState after, String path,
+                       NodeStore store, String filter) {
+        this.before = before;
+        this.after = after;
+        this.path = path;
+        this.store = store;
+        this.filter = filter;
+    }
+
+    public String build() throws Exception {
+        // TODO extract and evaluate filter criteria specified in 'filter' parameter
+
+        final JsopBuilder buff = new JsopBuilder();
+        // maps (key: id of target node, value: path/to/target)
+        // for tracking added/removed nodes; this allows us
+        // to detect 'move' operations
+        final HashMap<NodeState, String> addedNodes = new HashMap<NodeState, String>();
+        final HashMap<NodeState, String> removedNodes = new HashMap<NodeState, String>();
+
+        if (before == null) {
+            if (after != null) {
+                buff.tag('+').key(path).object();
+                toJson(buff, after);
+                return buff.endObject().newline().toString();
+            } else {
+                throw new Exception("path doesn't exist in the specified revisions: " + path);
+            }
+        } else if (after == null) {
+            buff.tag('-');
+            buff.value(path);
+            return buff.newline().toString();
+        }
+
+        TraversingNodeDiffHandler diffHandler = new TraversingNodeDiffHandler(store) {
+            @Override
+            public void propertyAdded(PropertyState after) {
+                buff.tag('+').
+                        key(PathUtils.concat(getCurrentPath(), after.getName())).
+                        encodedValue(after.getEncodedValue()).
+                        newline();
+            }
+
+            @Override
+            public void propertyChanged(PropertyState before, PropertyState after) {
+                buff.tag('^').
+                        key(PathUtils.concat(getCurrentPath(), after.getName())).
+                        encodedValue(after.getEncodedValue()).
+                        newline();
+            }
+
+            @Override
+            public void propertyDeleted(PropertyState before) {
+                // since property and node deletions can't be distinguished
+                // using the "- <path>" notation we're representing
+                // property deletions as "^ <path>:null"
+                buff.tag('^').
+                        key(PathUtils.concat(getCurrentPath(), before.getName())).
+                        value(null).
+                        newline();
+            }
+
+            @Override
+            public void childNodeAdded(String name, NodeState after) {
+                addedNodes.put(after, PathUtils.concat(getCurrentPath(), name));
+                buff.tag('+').
+                        key(PathUtils.concat(getCurrentPath(), name)).object();
+                toJson(buff, after);
+                buff.endObject().newline();
+            }
+
+            @Override
+            public void childNodeDeleted(String name, NodeState before) {
+                removedNodes.put(before, PathUtils.concat(getCurrentPath(), name));
+                buff.tag('-');
+                buff.value(PathUtils.concat(getCurrentPath(), name));
+                buff.newline();
+            }
+        };
+        diffHandler.start(before, after, path);
+
+        // check if this commit includes 'move' operations
+        // by building intersection of added and removed nodes
+        addedNodes.keySet().retainAll(removedNodes.keySet());
+        if (!addedNodes.isEmpty()) {
+            // this commit includes 'move' operations
+            removedNodes.keySet().retainAll(addedNodes.keySet());
+            // addedNodes & removedNodes now only contain information about moved nodes
+
+            // re-build the diff in a 2nd pass, this time representing moves correctly
+            buff.resetWriter();
+
+            // TODO refactor code, avoid duplication
+
+            diffHandler = new TraversingNodeDiffHandler(store) {
+                @Override
+                public void propertyAdded(PropertyState after) {
+                    buff.tag('+').
+                            key(PathUtils.concat(getCurrentPath(), after.getName())).
+                            encodedValue(after.getEncodedValue()).
+                            newline();
+                }
+
+                @Override
+                public void propertyChanged(PropertyState before, PropertyState after) {
+                    buff.tag('^').
+                            key(PathUtils.concat(getCurrentPath(), after.getName())).
+                            encodedValue(after.getEncodedValue()).
+                            newline();
+                }
+
+                @Override
+                public void propertyDeleted(PropertyState before) {
+                    // since property and node deletions can't be distinguished
+                    // using the "- <path>" notation we're representing
+                    // property deletions as "^ <path>:null"
+                    buff.tag('^').
+                            key(PathUtils.concat(getCurrentPath(), before.getName())).
+                            value(null).
+                            newline();
+                }
+
+                @Override
+                public void childNodeAdded(String name, NodeState after) {
+                    if (addedNodes.containsKey(after)) {
+                        // moved node, will be processed separately
+                        return;
+                    }
+                    buff.tag('+').
+                            key(PathUtils.concat(getCurrentPath(), name)).object();
+                    toJson(buff, after);
+                    buff.endObject().newline();
+                }
+
+                @Override
+                public void childNodeDeleted(String name, NodeState before) {
+                    if (addedNodes.containsKey(before)) {
+                        // moved node, will be processed separately
+                        return;
+                    }
+                    buff.tag('-');
+                    buff.value(PathUtils.concat(getCurrentPath(), name));
+                    buff.newline();
+                }
+
+            };
+            diffHandler.start(before, after, path);
+
+            // finally process moved nodes
+            for (Map.Entry<NodeState, String> entry : addedNodes.entrySet()) {
+                buff.tag('>').
+                        // path/to/deleted/node
+                        key(removedNodes.get(entry.getKey())).
+                        // path/to/added/node
+                        value(entry.getValue()).
+                        newline();
+            }
+        }
+        return buff.toString();
+    }
+
+    private void toJson(JsopBuilder builder, NodeState node) {
+        for (PropertyState property : node.getProperties()) {
+            builder.key(property.getName()).encodedValue(property.getEncodedValue());
+        }
+        for (ChildNodeEntry entry : node.getChildNodeEntries(0, -1)) {
+            builder.key(entry.getName()).object();
+            toJson(builder, entry.getNode());
+            builder.endObject();
+        }
+    }
+}

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/MutableCommit.java Wed Apr 18 15:42:14 2012
@@ -40,6 +40,7 @@ public class MutableCommit extends Abstr
         setCommitTS(other.getCommitTS());
         setMsg(other.getMsg());
         setChanges(other.getChanges());
+        setBranchRootId(other.getBranchRootId());
         this.id = other.getId();
     }
 
@@ -63,6 +64,10 @@ public class MutableCommit extends Abstr
         this.changes = changes;
     }
 
+    public void setBranchRootId(Id branchRootId) {
+        this.branchRootId = branchRootId;
+    }
+
     /**
      * Return the commit id.
      * 

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StoredCommit.java Wed Apr 18 15:42:14 2012
@@ -31,17 +31,20 @@ public class StoredCommit extends Abstra
         String msg = binding.readStringValue("msg");
         String changes = binding.readStringValue("changes");
         String parentId = binding.readStringValue("parentId");
+        String branchRootId = binding.readStringValue("branchRootId");
         return new StoredCommit(id, "".equals(parentId) ? null : Id.fromString(parentId),
-                commitTS, rootNodeId, "".equals(msg) ? null : msg, changes);
+                commitTS, rootNodeId, "".equals(msg) ? null : msg, changes,
+                "".equals(parentId) ? null : Id.fromString(branchRootId));
     }
 
-    public StoredCommit(Id id, Id parentId, long commitTS, Id rootNodeId, String msg, String changes) {
+    public StoredCommit(Id id, Id parentId, long commitTS, Id rootNodeId, String msg, String changes, Id branchRootId) {
         this.id = id;
         this.parentId = parentId;
         this.commitTS = commitTS;
         this.rootNodeId = rootNodeId;
         this.msg = msg;
         this.changes = changes;
+        this.branchRootId = branchRootId;
     }
 
     public StoredCommit(Id id, Commit commit) {

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/CopyingGC.java Wed Apr 18 15:42:14 2012
@@ -204,6 +204,11 @@ public class CopyingGC extends AbstractR
         return runState.get() == STARTED ? rsTo.putHeadCommit(commit) : rsFrom.putHeadCommit(commit);
     }
 
+    public Id putCommit(MutableCommit commit) throws Exception {
+        // TODO: review, should GC ignore private branch commits?
+        return runState.get() == STARTED ? rsTo.putCommit(commit) : rsFrom.putCommit(commit);
+    }
+
     // TODO: potentially dangerous, if lock & unlock interfere with GC start
     public void unlockHead() {
         if (runState.get() == STARTED) {

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/DefaultRevisionStore.java Wed Apr 18 15:42:14 2012
@@ -19,6 +19,7 @@ package org.apache.jackrabbit.mk.store;
 import java.io.Closeable;
 import java.util.Collections;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import org.apache.jackrabbit.mk.model.ChildNode;
@@ -48,7 +49,7 @@ public class DefaultRevisionStore extend
 
     private boolean initialized;
     private Id head;
-    private long headCounter;
+    private AtomicLong commitCounter;
     private final ReentrantReadWriteLock headLock = new ReentrantReadWriteLock();
     private final Persistence pm;
 
@@ -56,6 +57,7 @@ public class DefaultRevisionStore extend
 
     public DefaultRevisionStore(Persistence pm) {
         this.pm = pm;
+        commitCounter = new AtomicLong();
     }
     
     public void initialize() throws Exception {
@@ -69,7 +71,7 @@ public class DefaultRevisionStore extend
         head = pm.readHead();
         if (head == null || head.getBytes().length == 0) {
             // assume virgin repository
-            byte[] rawHead = longToBytes(++headCounter);
+            byte[] rawHead = longToBytes(commitCounter.incrementAndGet());
             head = new Id(rawHead);
             
             Id rootNodeId = pm.writeNode(new MutableNode(this, "/"));
@@ -79,7 +81,7 @@ public class DefaultRevisionStore extend
             pm.writeCommit(head, initialCommit);
             pm.writeHead(head);
         } else {
-            headCounter = Long.parseLong(head.toString(), 16);
+            commitCounter.set(Long.parseLong(head.toString(), 16));
         }
 
         initialized = true;
@@ -171,38 +173,19 @@ public class DefaultRevisionStore extend
     public Id putHeadCommit(MutableCommit commit) throws Exception {
         verifyInitialized();
         if (!headLock.writeLock().isHeldByCurrentThread()) {
-            throw new IllegalStateException("putCommit called without holding write lock.");
+            throw new IllegalStateException("putHeadCommit called without holding write lock.");
         }
 
-        PersistHook callback = null;
-        if (commit instanceof PersistHook) {
-            callback = (PersistHook) commit;
-            callback.prePersist(this);
-        }
-
-        Id id = commit.getId();
-        if (id == null) {
-            id = new Id(longToBytes(++headCounter));
-        }
-        pm.writeCommit(id, commit);
+        Id id = writeCommit(commit);
         setHeadCommitId(id);
 
-        if (callback != null)  {
-            callback.postPersist(this);
-        }
-        cache.put(id, new StoredCommit(id, commit));
-
         return id;
     }
 
-    private void setHeadCommitId(Id id) throws Exception {
-        pm.writeHead(id);
-        head = id;
-        
-        long headCounter = Long.parseLong(id.toString(), 16);
-        if (headCounter > this.headCounter) {
-            this.headCounter = headCounter;
-        }
+    public Id putCommit(MutableCommit commit) throws Exception {
+        verifyInitialized();
+
+        return writeCommit(commit);
     }
 
     public void unlockHead() {
@@ -275,8 +258,41 @@ public class DefaultRevisionStore extend
         }
     }
 
-    //------------------------------------------------------------< overrides >
+    //-------------------------------------------------------< implementation >
 
+    private Id writeCommit(MutableCommit commit) throws Exception {
+        PersistHook callback = null;
+        if (commit instanceof PersistHook) {
+            callback = (PersistHook) commit;
+            callback.prePersist(this);
+        }
+
+        Id id = commit.getId();
+        if (id == null) {
+            id = new Id(longToBytes(commitCounter.incrementAndGet()));
+        }
+        pm.writeCommit(id, commit);
+
+        if (callback != null)  {
+            callback.postPersist(this);
+        }
+        cache.put(id, new StoredCommit(id, commit));
+        return id;
+    }
+
+    private void setHeadCommitId(Id id) throws Exception {
+        // non-synchronized since we're called from putHeadCommit
+        // which requires a write lock
+        pm.writeHead(id);
+        head = id;
+
+        long counter = Long.parseLong(id.toString(), 16);
+        if (counter > commitCounter.get()) {
+            commitCounter.set(counter);
+        }
+    }
+
+    //------------------------------------------------------------< overrides >
 
     @Override
     public void compare(final NodeState before, final NodeState after, final NodeStateDiff diff) {

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java?rev=1327547&r1=1327546&r2=1327547&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/store/RevisionStore.java Wed Apr 18 15:42:14 2012
@@ -33,6 +33,7 @@ public interface RevisionStore extends R
      * Lock the head. Must be called prior to putting a new head commit.
      * 
      * @see #putHeadCommit(MutableCommit)
+     * @see #unlockHead()
      */
     void lockHead();
     
@@ -49,6 +50,21 @@ public interface RevisionStore extends R
     
     /**
      * Unlock the head.
+     *
+     * @see #lockHead()
      */
     void unlockHead();
+
+    /**
+     * Store a new commit.
+     * <p/>
+     * Unlike {@code putHeadCommit(MutableCommit)}, this method
+     * does not affect the current head commit and therefore doesn't
+     * require a lock on the head.
+     *
+     * @param commit commit
+     * @return new commit id
+     * @throws Exception if an error occurs
+     */
+    Id /*id*/ putCommit(MutableCommit commit) throws Exception;
 }