Index: oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/NodeStoreKernel.java (working copy) @@ -519,7 +519,30 @@ throw new UnsupportedOperationException(); } + @Nonnull @Override + public synchronized String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + Revision revision = getRevision(branchRevisionId); + if (revision.branch == null) { + throw new MicroKernelException( + "Branch not found: " + branchRevisionId); + } + Revision ancestor = getRevision(ancestorRevisionId); + while (!ancestor.id.equals(revision.id)) { + revision = revision.base; + if (revision.branch == null) { + throw new MicroKernelException(ancestorRevisionId + " is not " + + "an ancestor revision of " + branchRevisionId); + } + } + Revision r = new Revision(ancestor); + revisions.put(r.id, r); + return r.id; + } + + @Override public long getLength(String blobId) throws MicroKernelException { Blob blob = blobs.get(blobId); if (blob != null) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java (working copy) @@ -449,7 +449,23 @@ return nodeStore.rebase(r, base).toString(); } + @Nonnull @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + Revision branch = Revision.fromString(branchRevisionId); + if (!branch.isBranch()) { + throw new MicroKernelException("Not a branch revision: " + branchRevisionId); + } + Revision ancestor = Revision.fromString(ancestorRevisionId); + if (!ancestor.isBranch()) { + throw new MicroKernelException("Not a branch revision: " + ancestorRevisionId); + } + return nodeStore.reset(branch, ancestor).toString(); + } + + @Override public long getLength(String blobId) throws MicroKernelException { try { return nodeStore.getBlob(blobId).length(); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStore.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStore.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStore.java (working copy) @@ -56,6 +56,7 @@ import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.cache.CacheStats; import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.kernel.BlobSerializer; import org.apache.jackrabbit.oak.plugins.mongomk.util.LoggingDocumentStoreWrapper; import org.apache.jackrabbit.oak.plugins.mongomk.util.TimingDocumentStoreWrapper; import org.apache.jackrabbit.oak.plugins.mongomk.util.Utils; @@ -219,6 +220,25 @@ */ private final BlobStore blobStore; + /** + * The BlobSerializer. + */ + private final BlobSerializer blobSerializer = new BlobSerializer() { + @Override + public String serialize(Blob blob) { + if (blob instanceof MongoBlob) { + return blob.toString(); + } + String id; + try { + id = createBlob(blob.getNewStream()).toString(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return id; + } + }; + public MongoNodeStore(MongoMK.Builder builder) { this.blobStore = builder.getBlobStore(); if (builder.isUseSimpleRevision()) { @@ -758,6 +778,38 @@ } @Nonnull + Revision reset(@Nonnull Revision branchHead, @Nonnull Revision ancestor) { + checkNotNull(branchHead); + checkNotNull(ancestor); + Branch b = getBranches().getBranch(branchHead); + if (b == null) { + throw new MicroKernelException("Empty branch cannot be reset"); + } + if (!b.containsCommit(ancestor)) { + throw new MicroKernelException(ancestor + " is not " + + "an ancestor revision of " + branchHead); + } + Revision rev; + boolean success = false; + Commit commit = newCommit(branchHead); + try { + // apply reverse diff + getRoot(ancestor).compareAgainstBaseState(getRoot(branchHead), + new CommitDiff(commit, getBlobSerializer())); + // TODO: clear collisions on root document + rev = apply(commit); + success = true; + } finally { + if (!success) { + canceled(commit); + } else { + done(commit, true, null); + } + } + return rev; + } + + @Nonnull Revision merge(@Nonnull Revision branchHead, @Nullable CommitInfo info) { Branch b = getBranches().getBranch(branchHead); Revision base = branchHead; @@ -1194,5 +1246,9 @@ public BlobStore getBlobStore() { return blobStore; } + + BlobSerializer getBlobSerializer() { + return blobSerializer; + } } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStoreBranch.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStoreBranch.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoNodeStoreBranch.java (working copy) @@ -16,15 +16,11 @@ */ package org.apache.jackrabbit.oak.plugins.mongomk; -import java.io.IOException; - import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.jackrabbit.mk.api.MicroKernelException; -import org.apache.jackrabbit.oak.api.Blob; import org.apache.jackrabbit.oak.api.CommitFailedException; -import org.apache.jackrabbit.oak.kernel.BlobSerializer; import org.apache.jackrabbit.oak.spi.commit.ChangeDispatcher; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; @@ -43,22 +39,6 @@ */ private static final int MERGE_RETRIES = 10; - private final BlobSerializer blobs = new BlobSerializer() { - @Override - public String serialize(Blob blob) { - if (blob instanceof MongoBlob) { - return blob.toString(); - } - String id; - try { - id = store.createBlob(blob.getNewStream()).toString(); - } catch (IOException e) { - throw new IllegalStateException(e); - } - return id; - } - }; - public MongoNodeStoreBranch(MongoNodeStore store, MongoNodeState base) { super(store, new ChangeDispatcher(store.getRoot()), base); @@ -92,7 +72,8 @@ MongoNodeState state = persist(new Changes() { @Override public void with(Commit c) { - toPersist.compareAgainstBaseState(base, new CommitDiff(c, blobs)); + toPersist.compareAgainstBaseState(base, + new CommitDiff(c, store.getBlobSerializer())); } }, base, info); if (base.isBranch()) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/LogWrapper.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/LogWrapper.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/LogWrapper.java (working copy) @@ -261,6 +261,22 @@ } } + @Nonnull + @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + try { + logMethod("reset", branchRevisionId, ancestorRevisionId); + String result = mk.reset(branchRevisionId, ancestorRevisionId); + logResult(result); + return result; + } catch (Exception e) { + logException(e); + throw convert(e); + } + } + private void logMethod(String methodName, Object... args) { StringBuilder buff = new StringBuilder("mk"); buff.append(id).append('.').append(methodName).append('('); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/TimingWrapper.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/TimingWrapper.java (revision 1543855) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/util/TimingWrapper.java (working copy) @@ -267,6 +267,21 @@ } } + @Nonnull + @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + try { + long start = now(); + String result = mk.reset(branchRevisionId, ancestorRevisionId); + updateAndLogTimes("reset", start, 0, 0); + return result; + } catch (Exception e) { + throw convert(e); + } + } + private static RuntimeException convert(Exception e) { if (e instanceof RuntimeException) { return (RuntimeException) e; Index: oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreCacheTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreCacheTest.java (revision 1543855) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/kernel/KernelNodeStoreCacheTest.java (working copy) @@ -250,7 +250,15 @@ return kernel.rebase(branchRevisionId, newBaseRevisionId); } + @Nonnull @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + return kernel.reset(branchRevisionId, ancestorRevisionId); + } + + @Override public long getLength(String blobId) throws MicroKernelException { return kernel.getLength(blobId); } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKBranchMergeTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKBranchMergeTest.java (revision 1543855) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKBranchMergeTest.java (working copy) @@ -457,13 +457,6 @@ //--------------------------< internal >------------------------------------ - private String addNodes(String rev, String...nodes) { - for (String node : nodes) { - rev = mk.commit("", "+\"" + node + "\":{}", rev, ""); - } - return rev; - } - private String removeNodes(String rev, String...nodes) { for (String node : nodes) { rev = mk.commit("", "-\"" + node + "\"", rev, ""); Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKResetTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKResetTest.java (revision 0) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKResetTest.java (revision 0) @@ -0,0 +1,68 @@ +/* + * 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.oak.plugins.mongomk; + +import org.apache.jackrabbit.mk.api.MicroKernelException; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests MongoMKs implementation of MicroKernel.reset(String, String). + */ +public class MongoMKResetTest extends BaseMongoMKTest { + + @Test + public void resetToCurrentBranchHead() { + String rev = mk.branch(null); + rev = addNodes(rev, "/foo"); + String reset = mk.reset(rev, rev); + assertTrue(mk.diff(rev, reset, "/", 0).length() == 0); + } + + @Test + public void resetTrunk() { + String rev = addNodes(null, "/foo"); + try { + mk.reset(rev, rev); + fail("MicroKernelException expected"); + } catch (MicroKernelException expected) {} + } + + @Test + public void resetNonAncestor() { + String rev = mk.getHeadRevision(); + addNodes(null, "/foo"); + String branch = mk.branch(null); + branch = addNodes(branch, "/bar"); + try { + mk.reset(branch, rev); + fail("MicroKernelException expected"); + } catch (MicroKernelException expected) {} + } + + @Test + public void resetBranch() { + String branch = mk.branch(null); + branch = addNodes(branch, "/foo"); + String head = addNodes(branch, "/bar"); + assertNodesExist(head, "/bar"); + head = mk.reset(head, branch); + assertNodesNotExist(head, "/bar"); + } +} Property changes on: oak-core\src\test\java\org\apache\jackrabbit\oak\plugins\mongomk\MongoMKResetTest.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision Rev URL Added: svn:eol-style + native Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKTestBase.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKTestBase.java (revision 1543855) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMKTestBase.java (working copy) @@ -182,4 +182,12 @@ } return val; } + + protected String addNodes(String rev, String...nodes) { + String newRev = rev; + for (String node : nodes) { + newRev = getMicroKernel().commit("", "+\"" + node + "\":{}", newRev, ""); + } + return newRev; + } } Index: oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java =================================================================== --- oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java (revision 1543855) +++ oak-it/mk/src/main/java/org/apache/jackrabbit/mk/test/MicroKernelIT.java (working copy) @@ -1295,6 +1295,45 @@ } @Test + public void resetToCurrentBranchHead() { + String rev = mk.branch(null); + rev = addNodes(rev, "/foo"); + String reset = mk.reset(rev, rev); + assertTrue(mk.diff(rev, reset, "/", -1).length() == 0); + } + + @Test + public void resetTrunk() { + String rev = addNodes(null, "/foo"); + try { + mk.reset(rev, rev); + fail("MicroKernelException expected"); + } catch (MicroKernelException expected) {} + } + + @Test + public void resetNonAncestor() { + String rev = mk.getHeadRevision(); + addNodes(null, "/foo"); + String branch = mk.branch(null); + branch = addNodes(branch, "/bar"); + try { + mk.reset(branch, rev); + fail("MicroKernelException expected"); + } catch (MicroKernelException expected) {} + } + + @Test + public void resetBranch() { + String branch = mk.branch(null); + branch = addNodes(branch, "/foo"); + String head = addNodes(branch, "/bar"); + assertNodesExist(head, "/bar"); + head = mk.reset(head, branch); + assertNodesNotExist(head, "/bar"); + } + + @Test public void testSmallBlob() { testBlobs(1024, 1024); } Index: oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java =================================================================== --- oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java (revision 1543855) +++ oak-mk-api/src/main/java/org/apache/jackrabbit/mk/api/MicroKernel.java (working copy) @@ -555,6 +555,24 @@ String /*revisionId */ rebase(@Nonnull String branchRevisionId, String newBaseRevisionId) throws MicroKernelException; + /** + * Resets the branch identified by {@code branchRevisionId} to an ancestor + * branch commit identified by {@code ancestorRevisionId}. + * + * @param branchRevisionId id of the private branch revision + * @param ancestorRevisionId id of the ancestor commit to reset the branch to. + * @return the id of the new head of the branch. This may not necessarily + * be the same as {@code ancestorRevisionId}. An implementation is + * free to create a new id for the reset branch. + * @throws MicroKernelException if {@code branchRevisionId} doesn't exist, + * if it's not a branch revision, if {@code ancestorRevisionId} + * is not a revision on that branch or if another error occurs. + */ + @Nonnull + String /* revisionId */ reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException; + //--------------------------------------------------< BLOB READ/WRITE ops > /** Index: oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java =================================================================== --- oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java (revision 1543855) +++ oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/client/Client.java (working copy) @@ -321,7 +321,27 @@ throw new UnsupportedOperationException(); } + @Nonnull @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + + Request request = null; + + try { + request = createRequest("reset"); + request.addParameter("branch_revision_id", branchRevisionId); + request.addParameter("ancestor_revision_id", ancestorRevisionId); + return request.getString(); + } catch (IOException e) { + throw toMicroKernelException(e); + } finally { + IOUtils.closeQuietly(request); + } + } + + @Override public long getLength(String blobId) throws MicroKernelException { Request request = null; Index: oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java =================================================================== --- oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java (revision 1543855) +++ oak-mk-remote/src/main/java/org/apache/jackrabbit/mk/server/MicroKernelServlet.java (working copy) @@ -85,6 +85,7 @@ COMMANDS.put("commit", new Commit()); COMMANDS.put("branch", new Branch()); COMMANDS.put("merge", new Merge()); + COMMANDS.put("reset", new Reset()); COMMANDS.put("getLength", new GetLength()); COMMANDS.put("read", new Read()); COMMANDS.put("write", new Write()); @@ -311,6 +312,21 @@ } } + static class Reset implements Command { + + @Override + public void execute(MicroKernel mk, Request request, Response response) + throws IOException, MicroKernelException { + String branchRevisionId = request.getParameter("branch_revision_id"); + String ancestorRevisionId = request.getParameter("ancestor_revision_id"); + + String newRevision = mk.reset(branchRevisionId, ancestorRevisionId); + + response.setContentType("test/plain"); + response.write(newRevision); + } + } + static class GetLength implements Command { @Override Index: oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java =================================================================== --- oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java (revision 1543855) +++ oak-mk/src/main/java/org/apache/jackrabbit/mk/core/MicroKernelImpl.java (working copy) @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +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; @@ -579,6 +581,39 @@ } } + @Nonnull + @Override + public String reset(@Nonnull String branchRevisionId, + @Nonnull String ancestorRevisionId) + throws MicroKernelException { + Id branchId = Id.fromString(branchRevisionId); + Id ancestorId = Id.fromString(ancestorRevisionId); + StoredCommit commit; + try { + commit = rep.getCommit(branchId); + } catch (Exception e) { + throw new MicroKernelException(e); + } + Id baseId = commit.getBranchRootId(); + if (baseId == null) { + throw new MicroKernelException("Not a private branch: " + branchRevisionId); + } + // verify ancestorId is in fact an ancestor of branchId + while (!ancestorId.equals(branchId)) { + try { + commit = rep.getCommit(branchId); + } catch (Exception e) { + throw new MicroKernelException(e); + } + if (commit.getBranchRootId() == null) { + throw new MicroKernelException(ancestorRevisionId + " is not " + + "an ancestor revision of " + branchRevisionId); + } + branchId = commit.getParentId(); + } + return ancestorRevisionId; + } + public long getLength(String blobId) throws MicroKernelException { if (rep == null) { throw new IllegalStateException("this instance has already been disposed");