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 md...@apache.org on 2013/02/01 15:50:25 UTC

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

Author: mduerig
Date: Fri Feb  1 14:50:24 2013
New Revision: 1441465

URL: http://svn.apache.org/viewvc?rev=1441465&view=rev
Log:
OAK-536: Implement rebase for branches in Microkernel
implement rebase() for the H2 Microkernel

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/core/Repository.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/StagedNodeTree.java
    jackrabbit/oak/trunk/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.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=1441465&r1=1441464&r2=1441465&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 Fri Feb  1 14:50:24 2013
@@ -20,8 +20,6 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.annotation.Nonnull;
-
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.mk.api.MicroKernelException;
 import org.apache.jackrabbit.mk.json.JsonObject;
@@ -126,6 +124,15 @@ public class MicroKernelImpl implements 
         }
     }
 
+    private Id getBaseRevisionId(Id branchId) throws MicroKernelException {
+        try {
+            return rep.getBaseRevision(branchId);
+        }
+        catch (Exception e) {
+            throw new MicroKernelException(e);
+        }
+    }
+
     public String getRevisionHistory(long since, int maxEntries, String path) throws MicroKernelException {
         if (rep == null) {
             throw new IllegalStateException("this instance has already been disposed");
@@ -542,10 +549,26 @@ public class MicroKernelImpl implements 
         }
     }
 
-    @Nonnull
     @Override
-    public String rebase(@Nonnull String branchRevisionId, String newBaseRevisionId) {
-        throw new UnsupportedOperationException();
+    public String rebase(String branchRevisionId, String newBaseRevisionId) {
+        Id branchId = Id.fromString(branchRevisionId);
+        Id baseId = getBaseRevisionId(branchId);
+        Id newBaseId = newBaseRevisionId == null ? getHeadRevisionId() : Id.fromString(newBaseRevisionId);
+
+        if (baseId.equals(newBaseId)) {
+            return branchRevisionId;
+        }
+        else {
+            Id newBranchId = Id.fromString(branch(newBaseRevisionId));
+            try {
+                CommitBuilder cb = rep.getCommitBuilder(newBranchId,
+                        "rebasing " + branchRevisionId + " onto " + newBaseRevisionId);
+                return cb.rebase(baseId, branchId).toString();
+            }
+            catch (Exception e) {
+                throw new MicroKernelException(e);
+            }
+        }
     }
 
     public long getLength(String blobId) throws MicroKernelException {

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java?rev=1441465&r1=1441464&r2=1441465&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/core/Repository.java Fri Feb  1 14:50:24 2013
@@ -16,6 +16,9 @@
  */
 package org.apache.jackrabbit.mk.core;
 
+import java.io.Closeable;
+import java.io.File;
+
 import org.apache.jackrabbit.mk.blobs.BlobStore;
 import org.apache.jackrabbit.mk.blobs.FileBlobStore;
 import org.apache.jackrabbit.mk.blobs.MemoryBlobStore;
@@ -31,9 +34,6 @@ import org.apache.jackrabbit.mk.store.Re
 import org.apache.jackrabbit.mk.util.IOUtils;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 
-import java.io.Closeable;
-import java.io.File;
-
 /**
  *
  */
@@ -144,6 +144,14 @@ public class Repository {
         return rs.getHeadCommitId();
     }
 
+    public Id getBaseRevision(Id branchRevision) throws Exception {
+        if (!initialized) {
+            throw new IllegalStateException("not initialized");
+        }
+        StoredCommit commit = rs.getCommit(branchRevision);
+        return commit == null ? null : commit.getBranchRootId();
+    }
+
     public StoredCommit getHeadCommit() throws Exception {
         if (!initialized) {
             throw new IllegalStateException("not initialized");

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=1441465&r1=1441464&r2=1441465&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 Fri Feb  1 14:50:24 2013
@@ -16,6 +16,9 @@
  */
 package org.apache.jackrabbit.mk.model;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.apache.jackrabbit.mk.json.JsonObject;
 import org.apache.jackrabbit.mk.json.JsopBuilder;
 import org.apache.jackrabbit.mk.model.tree.DiffBuilder;
@@ -25,9 +28,6 @@ import org.apache.jackrabbit.oak.commons
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  *
  */
@@ -194,6 +194,39 @@ public class CommitBuilder {
         return newRevId;
     }
 
+    public Id rebase(Id fromId, Id toId) throws Exception {
+        RevisionStore.PutToken token = store.createPutToken();
+
+        Id rebasedId = stagedTree.rebase(baseRevId, fromId, toId, token);
+
+        if (store.getCommit(toId).getRootNodeId().equals(rebasedId)) {
+            // the rebase didn't cause any changes,
+            // no need to create new commit object/update head revision
+            return toId;
+        }
+
+        StoredCommit baseCommit = store.getCommit(baseRevId);
+        MutableCommit newCommit = new MutableCommit();
+        newCommit.setParentId(baseRevId);
+        newCommit.setCommitTS(System.currentTimeMillis());
+        newCommit.setMsg(msg);
+        // dynamically build diff for rebased commit
+        String diff = new DiffBuilder(
+                store.getNodeState(store.getRootNode(toId)),
+                store.getNodeState(store.getNode(rebasedId)),
+                "/", -1, store, "").build();
+        newCommit.setChanges(diff);
+        newCommit.setRootNodeId(rebasedId);
+        newCommit.setBranchRootId(baseCommit.getBranchRootId());
+        Id newRevId = store.putCommit(token, newCommit);
+
+        // reset instance
+        stagedTree.reset(newRevId);
+        changeLog.clear();
+
+        return newRevId;
+    }
+
     public Id /* new revId */ doMerge() throws Exception {
         StoredCommit branchCommit = store.getCommit(baseRevId);
         Id branchRootId = branchCommit.getBranchRootId();

Modified: jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java?rev=1441465&r1=1441464&r2=1441465&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/main/java/org/apache/jackrabbit/mk/model/StagedNodeTree.java Fri Feb  1 14:50:24 2013
@@ -16,16 +16,18 @@
  */
 package org.apache.jackrabbit.mk.model;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
 import org.apache.jackrabbit.mk.json.JsonObject;
 import org.apache.jackrabbit.mk.model.tree.NodeDelta;
 import org.apache.jackrabbit.mk.store.NotFoundException;
 import org.apache.jackrabbit.mk.store.RevisionStore;
+import org.apache.jackrabbit.mk.store.RevisionStore.PutToken;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 /**
  * A {@code StagedNodeTree} provides methods to manipulate a specific revision
  * of the tree. The changes are recorded and can be persisted by calling
@@ -80,6 +82,21 @@ public class StagedNodeTree {
         return root != null ? root.persist(token) : null;
     }
 
+    public Id rebase(Id baseId, Id fromId, Id toId, PutToken token) throws Exception {
+        // reset staging area to new base revision
+        reset(baseId);
+
+        StoredNode baseRoot = store.getRootNode(baseId);
+        StoredNode fromRoot = store.getRootNode(fromId);
+        StoredNode toRoot = store.getRootNode(toId);
+
+        // recursively apply changes from 'fromRoot' to 'toRoot' onto 'baseRoot'
+        rebaseNode(baseRoot, fromRoot, toRoot, "/");
+
+        // persist staged nodes
+        return persist(token);
+    }
+
     /**
      * Performs a three-way merge merging <i>our</i> tree (rooted at {@code ourRoot})
      * and <i>their</i> tree (identified by {@code newBaseRevisionId}),
@@ -362,6 +379,142 @@ public class StagedNodeTree {
         return node;
     }
 
+    private void rebaseNode(StoredNode base, StoredNode from, StoredNode to, String path)
+            throws Exception {
+        assert from != null;
+        assert to != null;
+        assert base != null;
+
+        NodeDelta theirDelta = new NodeDelta(store, store.getNodeState(from), store.getNodeState(base));
+        NodeDelta ourDelta = new NodeDelta(store, store.getNodeState(from), store.getNodeState(to));
+
+        // apply the changes
+        StagedNode stagedNode = getStagedNode(path, true);
+
+        for (Entry<String, String> added : ourDelta.getAddedProperties().entrySet()) {
+            String name = added.getKey();
+            String ourValue = added.getValue();
+            String theirValue = theirDelta.getAddedProperties().get(name);
+
+            if (theirValue != null && !theirValue.equals(ourValue)) {
+                markConflict(stagedNode, "addExistingProperty", name, ourValue);
+            }
+            else {
+                stagedNode.getProperties().put(name, ourValue);
+            }
+        }
+
+        for (Entry<String, String> removed : ourDelta.getRemovedProperties().entrySet()) {
+            String name = removed.getKey();
+            String ourValue = removed.getValue();
+
+            if (theirDelta.getRemovedProperties().containsKey(name)) {
+                markConflict(stagedNode, "deleteDeletedProperty", name, ourValue);
+            }
+            else if (theirDelta.getChangedProperties().containsKey(name)) {
+                markConflict(stagedNode, "deleteChangedProperty", name, ourValue);
+            }
+            else {
+                stagedNode.getProperties().remove(name);
+            }
+        }
+
+        for (Entry<String, String> changed : ourDelta.getChangedProperties().entrySet()) {
+            String name = changed.getKey();
+            String ourValue = changed.getValue();
+            String theirValue = theirDelta.getChangedProperties().get(name);
+
+            if (theirDelta.getRemovedProperties().containsKey(name)) {
+                markConflict(stagedNode, "changeDeletedProperty", name, ourValue);
+            }
+            else if (theirValue != null && !theirValue.equals(ourValue)) {
+                markConflict(stagedNode, "changeChangedProperty", name, ourValue);
+            }
+            else {
+                stagedNode.getProperties().put(name, ourValue);
+            }
+        }
+
+        for (Entry<String, Id> added : ourDelta.getAddedChildNodes().entrySet()) {
+            String name = added.getKey();
+            Id ourId = added.getValue();
+            Id theirId = theirDelta.getAddedChildNodes().get(name);
+
+            if (theirId != null && !theirId.equals(ourId)) {
+                markConflict(stagedNode, "addExistingNode", name, ourId);
+            }
+            else {
+                stagedNode.add(new ChildNodeEntry(name, ourId));
+            }
+        }
+
+        for (Entry<String, Id> removed : ourDelta.getRemovedChildNodes().entrySet()) {
+            String name = removed.getKey();
+            Id ourId = removed.getValue();
+
+            if (theirDelta.getRemovedChildNodes().containsKey(name)) {
+                markConflict(stagedNode, "deleteDeletedNode", name, ourId);
+            }
+            else if (theirDelta.getChangedChildNodes().containsKey(name)) {
+                markConflict(stagedNode, "deleteChangedNode", name, ourId);
+            }
+            else {
+                stagedNode.remove(name);
+            }
+        }
+
+        for (Entry<String, Id> changed : ourDelta.getChangedChildNodes().entrySet()) {
+            String name = changed.getKey();
+            Id ourId = changed.getValue();
+
+            StoredNode changedBase = getChildNode(base, name);
+            if (changedBase == null) {
+                markConflict(stagedNode, "changeDeletedNode", name, ourId);
+                continue;
+            }
+
+            StoredNode changedFrom = getChildNode(from, name);
+            StoredNode changedTo = getChildNode(to, name);
+            String changedPath = PathUtils.concat(path, name);
+            rebaseNode(changedBase, changedFrom, changedTo, changedPath);
+        }
+    }
+
+    private void markConflict(StagedNode parent, String conflictType, String name, String ourValue) {
+        StagedNode marker = getOrAddConflictMarker(parent, conflictType);
+        marker.getProperties().put(name, ourValue);
+    }
+
+    private void markConflict(StagedNode parent, String conflictType, String name, Id ourId) {
+        StagedNode marker = getOrAddConflictMarker(parent, conflictType);
+        marker.add(new ChildNodeEntry(name, ourId));
+    }
+
+    private StagedNode getOrAddConflictMarker(StagedNode parent, String name) {
+        StagedNode conflict = getOrAddNode(parent, ":conflict");
+        return getOrAddNode(conflict, name);
+    }
+
+    private StagedNode getOrAddNode(StagedNode parent, String name) {
+        ChildNodeEntry cne = parent.getChildNodeEntry(name);
+        if (cne == null) {
+            return parent.add(name, new StagedNode(store));
+        } else {
+            try {
+                return parent.getStagedChildNode(name, true);
+            }
+            catch (Exception e) {
+                // should never happen
+                throw new IllegalStateException(e);
+            }
+        }
+    }
+
+    private StoredNode getChildNode(StoredNode parent, String name) throws Exception {
+        ChildNodeEntry cne = parent.getChildNodeEntry(name);
+        return cne == null ? null : store.getNode(cne.getId());
+    }
+
     /**
      * Performs a three-way merge of the trees rooted at {@code ourRoot},
      * {@code theirRoot}, using the tree at {@code baseRoot} as reference.

Modified: jackrabbit/oak/trunk/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java?rev=1441465&r1=1441464&r2=1441465&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java (original)
+++ jackrabbit/oak/trunk/oak-mk/src/test/java/org/apache/jackrabbit/mk/MicroKernelImplTest.java Fri Feb  1 14:50:24 2013
@@ -26,6 +26,8 @@ import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class MicroKernelImplTest {
     
@@ -74,4 +76,319 @@ public class MicroKernelImplTest {
         assertEquals("+\"/b\":{\"l\":1,\"x\":{\"l\":2,\"y\":{}}}", mk.diff(r1, r2, "/b", -1).trim());
     }
 
+
+    @Test
+    public void rebaseWithoutChanges() {
+        String branch = mk.branch(null);
+        String rebased = mk.rebase(branch, null);
+        assertEquals(branch, rebased);
+    }
+
+    @Test
+    public void fastForwardRebase() {
+        String branch = mk.branch(null);
+        branch = mk.commit("", "+\"/a\":{}", branch, null);
+        String rebased = mk.rebase(branch, null);
+        assertEquals(branch, rebased);
+    }
+
+    @Test
+    public void rebaseEmptyBranch() {
+        String branch = mk.branch(null);
+        String trunk = mk.commit("", "+\"/a\":{}", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        assertEquals("{\":childNodeCount\":1,\"a\":{}}", mk.getNodes("/", rebased, 0, 0, -1, null));
+        assertEquals("{\":childNodeCount\":1,\"a\":{}}", mk.getNodes("/", null, 0, 0, -1, null));
+        assertEquals(trunk, mk.getHeadRevision());
+        assertFalse((trunk.equals(rebased)));
+    }
+
+    @Test
+    public void rebaseAddNode() {
+        mk.commit("", "+\"/x\":{}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "+\"/x/b\":{}", branch, null);
+        String trunk = mk.commit("", "+\"/x/a\":{}", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        assertEquals(1, mk.getChildNodeCount("/x", null));
+        assertNotNull(mk.getNodes("/x/a", null, 0, 0, -1, null));
+
+        assertEquals(1, mk.getChildNodeCount("/x", branch));
+        assertNotNull(mk.getNodes("/x/b", branch, 0, 0, -1, null));
+
+        assertEquals(2, mk.getChildNodeCount("/x", rebased));
+        assertNotNull(mk.getNodes("/x/a", rebased, 0, 0, -1, null));
+        assertNotNull(mk.getNodes("/x/b", rebased, 0, 0, -1, null));
+    }
+
+    @Test
+    public void rebaseRemoveNode() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "-\"/x/y\"", branch, null);
+        String trunk = mk.commit("", "+\"/x/a\":{}", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        assertEquals(2, mk.getChildNodeCount("/x", null));
+        assertNotNull(mk.getNodes("/x/a", null, 0, 0, -1, null));
+        assertNotNull(mk.getNodes("/x/y", null, 0, 0, -1, null));
+
+        assertEquals(0, mk.getChildNodeCount("/x", branch));
+
+        assertEquals(1, mk.getChildNodeCount("/x", rebased));
+        assertNotNull(mk.getNodes("/x/a", rebased, 0, 0, -1, null));
+    }
+
+    @Test
+    public void rebaseAddProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":42", branch, null);
+        String trunk = mk.commit("", "^\"/x/y/q\":99", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":42"));
+        assertFalse(branchNode.contains("\"q\":99"));
+
+        String rebasedNode = mk.getNodes("/x/y", rebased, 0, 0, -1, null);
+        assertTrue(rebasedNode.contains("\"p\":42"));
+        assertTrue(rebasedNode.contains("\"q\":99"));
+
+        String trunkNode = mk.getNodes("/x/y", null, 0, 0, -1, null);
+        assertFalse(trunkNode.contains("\"p\":42"));
+        assertTrue(trunkNode.contains("\"q\":99"));
+    }
+
+    @Test
+    public void rebaseRemoveProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":null", branch, null);
+        String trunk = mk.commit("", "^\"/x/y/q\":99", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertFalse(branchNode.contains("\"p\":42"));
+        assertFalse(branchNode.contains("\"q\":99"));
+
+        String rebasedNode = mk.getNodes("/x/y", rebased, 0, 0, -1, null);
+        assertFalse(rebasedNode.contains("\"p\":42"));
+        assertTrue(rebasedNode.contains("\"q\":99"));
+
+        String trunkNode = mk.getNodes("/x/y", null, 0, 0, -1, null);
+        assertTrue(trunkNode.contains("\"p\":42"));
+        assertTrue(trunkNode.contains("\"q\":99"));
+    }
+
+    @Test
+    public void rebaseChangeProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":41", branch, null);
+        String trunk = mk.commit("", "^\"/x/y/q\":99", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":41"));
+        assertFalse(branchNode.contains("\"q\":99"));
+
+        String rebasedNode = mk.getNodes("/x/y", rebased, 0, 0, -1, null);
+        assertTrue(rebasedNode.contains("\"p\":41"));
+        assertTrue(rebasedNode.contains("\"q\":99"));
+
+        String trunkNode = mk.getNodes("/x/y", null, 0, 0, -1, null);
+        assertTrue(trunkNode.contains("\"p\":42"));
+        assertTrue(trunkNode.contains("\"q\":99"));
+    }
+
+    @Test
+    public void rebaseChangePropertyWithSameValue() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":99", branch, null);
+        String trunk = mk.commit("", "^\"/x/y/p\":99", null, null);
+        String rebased = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":99"));
+
+        String rebasedNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(rebasedNode.contains("\"p\":99"));
+
+        String trunkNode = mk.getNodes("/x/y", null, 0, 0, -1, null);
+        assertTrue(trunkNode.contains("\"p\":99"));
+    }
+
+    @Test
+    public void rebaseAddExistingNode() {
+        mk.commit("", "+\"/x\":{}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "+\"/x/a\":{}", branch, null);
+        mk.commit("", "+\"/x/a\":{\"b\":{}}", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        assertTrue(mk.nodeExists("/x/a/b", branch));
+        String conflict = mk.getNodes("/x/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+            "{\":childNodeCount\":1,\"addExistingNode\":{\":childNodeCount\":1,\"a\":{\":childNodeCount\":0}}}",
+            conflict);
+    }
+
+    @Test
+    public void rebaseAddExistingProperties() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":42 ^\"/x/y/q\":42", branch, null);
+        mk.commit("", "^\"/x/y/p\":99 ^\"/x/y/q\":99", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":99"));
+        String conflict = mk.getNodes("/x/y/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"addExistingProperty\":{\"q\":42,\"p\":42,\":childNodeCount\":0}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseChangeRemovedProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":99", branch, null);
+        mk.commit("", "^\"/x/y/p\":null", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertFalse(branchNode.contains("\"p\":99"));
+        String conflict = mk.getNodes("/x/y/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"changeDeletedProperty\":{\"p\":99,\":childNodeCount\":0}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseRemoveChangedProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":null", branch, null);
+        mk.commit("", "^\"/x/y/p\":99", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":99"));
+        String conflict = mk.getNodes("/x/y/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"deleteChangedProperty\":{\"p\":42,\":childNodeCount\":0}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseChangedChangedProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":41", branch, null);
+        mk.commit("", "^\"/x/y/p\":99", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":99"));
+        String conflict = mk.getNodes("/x/y/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"changeChangedProperty\":{\"p\":41,\":childNodeCount\":0}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseRemoveChangedNode() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "-\"/x/y\"", branch, null);
+        mk.commit("", "^\"/x/y/p\":42", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":42"));
+        String conflict = mk.getNodes("/x/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"deleteChangedNode\":{\":childNodeCount\":1,\"y\":{\":childNodeCount\":0}}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseChangeRemovedNode() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/p\":42", branch, null);
+        mk.commit("", "-\"/x\"", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        assertFalse(mk.nodeExists("/x", branch));
+        String conflict = mk.getNodes("/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"changeDeletedNode\":{\":childNodeCount\":1,\"x\":{\"p\":42,\"" +
+                ":childNodeCount\":1,\"y\":{\":childNodeCount\":0}}}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseRemoveRemovedProperty() {
+        mk.commit("", "+\"/x\":{\"y\":{\"p\":42}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "^\"/x/y/p\":null", branch, null);
+        mk.commit("", "^\"/x/y/p\":null", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x/y", branch, 0, 0, -1, null);
+        assertFalse(branchNode.contains("\"p\":42"));
+        String conflict = mk.getNodes("/x/y/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"deleteDeletedProperty\":{\"p\":42,\":childNodeCount\":0}}",
+                conflict);
+    }
+
+    @Test
+    public void rebaseRemoveRemovedNode() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        branch = mk.commit("", "-\"/x/y\"", branch, null);
+        mk.commit("", "-\"/x/y\"", null, null);
+
+        branch = mk.rebase(branch, null);
+
+        assertFalse(mk.nodeExists("/x/y", branch));
+        String conflict = mk.getNodes("/x/:conflict", branch, 100, 0, -1, null);
+        assertEquals(
+                "{\":childNodeCount\":1,\"deleteDeletedNode\":{\":childNodeCount\":1,\"y\":{\":childNodeCount\":0}}}",
+                conflict);
+    }
+
+    @Test
+    public void mergeRebased() {
+        mk.commit("", "+\"/x\":{\"y\":{}}", null, null);
+        String branch = mk.branch(null);
+        String trunk = mk.commit("", "^\"/x/p\":42", null, null);
+        branch = mk.commit("", "^\"/x/q\":43", branch, null);
+        branch = mk.rebase(branch, null);
+
+        String branchNode = mk.getNodes("/x", branch, 0, 0, -1, null);
+        assertTrue(branchNode.contains("\"p\":42"));
+        assertTrue(branchNode.contains("\"q\":43"));
+
+        mk.merge(branch, null);
+        String trunkNode = mk.getNodes("/x", branch, 0, 0, -1, null);
+        assertTrue(trunkNode.contains("\"p\":42"));
+        assertTrue(trunkNode.contains("\"q\":43"));
+    }
+
 }