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 ba...@apache.org on 2015/07/15 23:41:03 UTC

svn commit: r1691280 - in /jackrabbit/oak/trunk/oak-upgrade/src: main/java/org/apache/jackrabbit/oak/upgrade/ main/java/org/apache/jackrabbit/oak/upgrade/nodestate/ main/java/org/apache/jackrabbit/oak/upgrade/security/ test/java/org/apache/jackrabbit/o...

Author: baedke
Date: Wed Jul 15 21:41:03 2015
New Revision: 1691280

URL: http://svn.apache.org/r1691280
Log:
OAK-2619: Repeated upgrades

Added support for incremental upgrades.

Added:
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java
Modified:
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java

Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java?rev=1691280&r1=1691279&r2=1691280&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java Wed Jul 15 21:41:03 2015
@@ -17,11 +17,11 @@
 package org.apache.jackrabbit.oak.upgrade;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.of;
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Lists.newArrayListWithCapacity;
 import static com.google.common.collect.Maps.newHashMap;
 import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
-import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONSTORAGE;
 import static org.apache.jackrabbit.core.RepositoryImpl.ACTIVITIES_NODE_ID;
 import static org.apache.jackrabbit.core.RepositoryImpl.ROOT_NODE_ID;
 import static org.apache.jackrabbit.core.RepositoryImpl.VERSION_STORAGE_NODE_ID;
@@ -33,6 +33,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -73,6 +74,7 @@ import org.apache.jackrabbit.oak.api.Com
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
@@ -85,7 +87,6 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.nodetype.TypeEditorProvider;
 import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent;
 import org.apache.jackrabbit.oak.plugins.nodetype.write.ReadWriteNodeTypeManager;
-import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeBuilder;
 import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl;
 import org.apache.jackrabbit.oak.security.SecurityProviderImpl;
 import org.apache.jackrabbit.oak.spi.commit.CommitHook;
@@ -107,6 +108,7 @@ import org.apache.jackrabbit.oak.spi.sta
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier;
 import org.apache.jackrabbit.oak.upgrade.security.GroupEditorProvider;
 import org.apache.jackrabbit.oak.upgrade.security.RestrictionEditorProvider;
 import org.apache.jackrabbit.spi.Name;
@@ -506,6 +508,12 @@ public class RepositoryUpgrade {
         logger.debug("Registering custom non-aggregated privileges");
         for (Privilege privilege : registry.getRegisteredPrivileges()) {
             String privilegeName = privilege.getName();
+
+            if (hasPrivilege(pMgr, privilegeName)) {
+                logger.debug("Privilege {} already exists", privilegeName);
+                continue;
+            }
+
             if (PrivilegeBits.BUILT_IN.containsKey(privilegeName) || JCR_ALL.equals(privilegeName)) {
                 // Ignore built in privileges as those have been installed by the PrivilegesInitializer already
                 logger.debug("Built-in privilege -> ignore.");
@@ -557,6 +565,16 @@ public class RepositoryUpgrade {
         }
     }
 
+    private boolean hasPrivilege(PrivilegeManager pMgr, String privilegeName) throws RepositoryException {
+        final Privilege[] registeredPrivileges = pMgr.getRegisteredPrivileges();
+        for (Privilege registeredPrivilege : registeredPrivileges) {
+            if (registeredPrivilege.getName().equals(privilegeName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static boolean allAggregatesRegistered(PrivilegeManager privilegeManager, List<String> aggrNames) {
         for (String name : aggrNames) {
             try {
@@ -694,16 +712,18 @@ public class RepositoryUpgrade {
         NodeBuilder system = builder.child(JCR_SYSTEM);
 
         logger.info("Copying version histories");
-        copyState(system, JCR_VERSIONSTORAGE, new JackrabbitNodeState(
+        copyState(system, "/jcr:system/jcr:versionStorage", new JackrabbitNodeState(
                 pm, root, uriToPrefix, VERSION_STORAGE_NODE_ID,
                 "/jcr:system/jcr:versionStorage",
-                workspaceName, versionablePaths, copyBinariesByReference, skipOnError));
+                workspaceName, versionablePaths, copyBinariesByReference, skipOnError),
+                true);
 
         logger.info("Copying activities");
-        copyState(system, "jcr:activities", new JackrabbitNodeState(
+        copyState(system, "/jcr:system/jcr:activities", new JackrabbitNodeState(
                 pm, root, uriToPrefix, ACTIVITIES_NODE_ID,
                 "/jcr:system/jcr:activities",
-                workspaceName, versionablePaths, copyBinariesByReference, skipOnError));
+                workspaceName, versionablePaths, copyBinariesByReference, skipOnError),
+                true);
     }
 
     private String copyWorkspace(
@@ -725,40 +745,30 @@ public class RepositoryUpgrade {
         for (ChildNodeEntry child : state.getChildNodeEntries()) {
             String childName = child.getName();
             if (!JCR_SYSTEM.equals(childName)) {
-                logger.info("Copying subtree /{}", childName);
-                copyState(builder, childName, child.getNodeState());
+                final String path = PathUtils.concat("/", childName);
+                logger.info("Copying subtree {}", path);
+                copyState(builder, path, child.getNodeState(), false);
             }
         }
 
         return workspaceName;
     }
 
-    private void copyState(NodeBuilder parent, String name, NodeState state) {
-        if (parent instanceof SegmentNodeBuilder) {
-            parent.setChildNode(name, state);
-        } else {
-            setChildNode(parent, name, state);
-        }
-    }
-
-    /**
-     * NodeState are copied by value by recursing down the complete tree
-     * This is a temporary approach for OAK-1760 for 1.0 branch.
-     */
-    private void setChildNode(NodeBuilder parent, String name, NodeState state) {
+    private void copyState(NodeBuilder targetParent, String path, NodeState source, boolean merge) {
+        final String name = PathUtils.getName(path);
         // OAK-1589: maximum supported length of name for DocumentNodeStore
         // is 150 bytes. Skip the sub tree if the the name is too long
         if (name.length() > 37 && name.getBytes(Charsets.UTF_8).length > 150) {
-            logger.warn("Node name too long. Skipping {}", state);
+            logger.warn("Node name too long. Skipping {}", source);
             return;
         }
-        NodeBuilder builder = parent.setChildNode(name);
-        for (PropertyState property : state.getProperties()) {
-            builder.setProperty(property);
-        }
-        for (ChildNodeEntry child : state.getChildNodeEntries()) {
-            setChildNode(builder, child.getName(), child.getNodeState());
-        }
+        NodeBuilder target = targetParent.child(name);
+        NodeStateCopier.copyNodeState(
+                source,
+                target,
+                path,
+                merge ? of(path) : Collections.<String>emptySet()
+        );
     }
 
     private static class LoggingCompositeHook implements CommitHook {

Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java?rev=1691280&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java Wed Jul 15 21:41:03 2015
@@ -0,0 +1,173 @@
+/*
+ * 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.upgrade.nodestate;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import java.util.Collections;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * The NodeStateCopier and NodeStateCopier.Builder classes allow
+ * recursively copying a NodeState to a NodeBuilder.
+ * <br>
+ * The copy algorithm is optimized for copying nodes between two
+ * different NodeStore instances, i.e. where comparing NodeStates
+ * is imprecise and/or expensive.
+ * <br>
+ * The algorithm does a post-order traversal. I.e. it copies
+ * changed leaf-nodes first.
+ * <br>
+ * The work for a traversal without any differences between
+ * {@code source} and {@code target} is equivalent to the single
+ * execution of a naive equals implementation.
+ */
+public class NodeStateCopier {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NodeStateCopier.class);
+
+
+    private NodeStateCopier() {
+        // no instances
+    }
+
+    /**
+     * Shorthand method to copy one NodeStore to another. The changes in the
+     * target NodeStore are automatically persisted.
+     *
+     * @param source NodeStore to copy from.
+     * @param target NodeStore to copy to.
+     * @throws CommitFailedException
+     */
+    public static boolean copyNodeStore(@Nonnull final NodeStore source, @Nonnull final NodeStore target)
+            throws CommitFailedException {
+        final NodeBuilder builder = checkNotNull(target).getRoot().builder();
+        final boolean hasChanges = copyNodeState(checkNotNull(source).getRoot(), builder, "/", Collections.<String>emptySet());
+        if (hasChanges) {
+            source.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        }
+        return hasChanges;
+    }
+
+    /**
+     * Copies all changed properties from the source NodeState to the target
+     * NodeBuilder instance.
+     *
+     * @param source The NodeState to copy from.
+     * @param target The NodeBuilder to copy to.
+     * @return Whether changes were made or not.
+     */
+    public static boolean copyProperties(NodeState source, NodeBuilder target) {
+        boolean hasChanges = false;
+
+        // remove removed properties
+        for (final PropertyState property : target.getProperties()) {
+            final String name = property.getName();
+            if (!source.hasProperty(name)) {
+                target.removeProperty(name);
+                hasChanges = true;
+            }
+        }
+
+        // add new properties and change changed properties
+        for (PropertyState property : source.getProperties()) {
+            if (!property.equals(target.getProperty(property.getName()))) {
+                target.setProperty(property);
+                hasChanges = true;
+            }
+        }
+        return hasChanges;
+    }
+
+    /**
+     * Recursively copies the source NodeState to the target NodeBuilder.
+     * <br>
+     * Nodes that exist in the {@code target} but not in the {@code source}
+     * are removed, unless they are descendants of one of the {@code mergePaths}.
+     * This is determined by checking if the {@code currentPath} is a descendant
+     * of any of the {@code mergePaths}.
+     * <br>
+     * <b>Note:</b> changes are not persisted.
+     *
+     * @param source NodeState to copy from
+     * @param target NodeBuilder to copy to
+     * @param currentPath The path of both the source and target arguments.
+     * @param mergePaths A Set of paths under which existing nodes should be
+     *                   preserved, even if the do not exist in the source.
+     * @return An indication of whether there were changes or not.
+     */
+    public static boolean copyNodeState(@Nonnull final NodeState source, @Nonnull final NodeBuilder target,
+                                        @Nonnull final String currentPath, @Nonnull final Set<String> mergePaths) {
+
+
+        boolean hasChanges = false;
+
+        // delete deleted children
+        for (final String childName : target.getChildNodeNames()) {
+            if (!source.hasChildNode(childName) && !isMerge(PathUtils.concat(currentPath, childName), mergePaths)) {
+                target.setChildNode(childName, EmptyNodeState.MISSING_NODE);
+                hasChanges = true;
+            }
+        }
+
+        for (ChildNodeEntry child : source.getChildNodeEntries()) {
+            final String childName = child.getName();
+            final NodeState childSource = child.getNodeState();
+            if (!target.hasChildNode(childName)) {
+                // add new children
+                target.setChildNode(childName, childSource);
+                hasChanges = true;
+            } else {
+                // recurse into existing children
+                final NodeBuilder childTarget = target.getChildNode(childName);
+                final String childPath = PathUtils.concat(currentPath, childName);
+                hasChanges = copyNodeState(childSource, childTarget, childPath, mergePaths) || hasChanges;
+            }
+        }
+
+        hasChanges = copyProperties(source, target) || hasChanges;
+
+        if (hasChanges) {
+            LOG.trace("Node {} has changes", target);
+        }
+
+        return hasChanges;
+    }
+
+    private static boolean isMerge(String path, Set<String> mergePaths) {
+        for (String mergePath : mergePaths) {
+            if (PathUtils.isAncestor(mergePath, path) || mergePath.equals(path)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java?rev=1691280&r1=1691279&r2=1691280&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java Wed Jul 15 21:41:03 2015
@@ -108,7 +108,7 @@ class GroupEditor extends DefaultEditor
 
     @Override
     public Editor childNodeChanged(String name, NodeState before, NodeState after) {
-        throw new IllegalStateException("changed node during upgrade copy not expected: " + state.path + "/" + name);
+        return null;
     }
 
     @Override

Modified: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java?rev=1691280&r1=1691279&r2=1691280&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java Wed Jul 15 21:41:03 2015
@@ -19,12 +19,14 @@
 package org.apache.jackrabbit.oak.upgrade;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 
 import javax.jcr.Credentials;
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
+import javax.jcr.Session;
 import javax.jcr.SimpleCredentials;
 
 import org.apache.commons.io.FileUtils;
@@ -40,63 +42,118 @@ import org.apache.jackrabbit.oak.stats.C
 import org.junit.Before;
 import org.junit.BeforeClass;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 public abstract class AbstractRepositoryUpgradeTest {
 
     protected static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray());
 
-    private static Repository targetRepository;
+    private static NodeStore targetNodeStore;
+
+    private static File testDirectory;
 
     @BeforeClass
-    public static void init() {
+    public static void init() throws InterruptedException {
         // ensure that we create a new repository for the next test
-        targetRepository = null;
+        targetNodeStore = null;
+        testDirectory = createTestDirectory();
+    }
+
+    protected static File createTestDirectory() throws InterruptedException {
+        final File dir = new File("target", "upgrade-" + Clock.SIMPLE.getTimeIncreasing());
+        FileUtils.deleteQuietly(dir);
+        return dir;
+    }
+
+    protected NodeStore createTargetNodeStore() {
+        return new SegmentNodeStore();
     }
 
     @Before
     public synchronized void upgradeRepository() throws Exception {
-        if (targetRepository == null) {
-            File directory = new File(
-                    "target", "upgrade-" + Clock.SIMPLE.getTimeIncreasing());
-            FileUtils.deleteQuietly(directory);
-
+        if (targetNodeStore == null) {
+            File directory = getTestDirectory();
             File source = new File(directory, "source");
             source.mkdirs();
-
-            InputStream repoConfig = getRepositoryConfig();
-            RepositoryConfig config;
-            if (repoConfig == null) {
-                config = RepositoryConfig.install(source);
-            } else {
-                OutputStream out = FileUtils.openOutputStream(new File(source, "repository.xml"));
-                IOUtils.copy(repoConfig, out);
-                out.close();
-                repoConfig.close();
-                config = RepositoryConfig.create(source);
-            }
-            RepositoryImpl repository = RepositoryImpl.create(config);
+            RepositoryImpl repository = createSourceRepository(source);
             try {
                 createSourceContent(repository);
             } finally {
                 repository.shutdown();
             }
-            NodeStore target = new SegmentNodeStore();
-            RepositoryUpgrade.copy(source, target);
-            targetRepository = new Jcr(new Oak(target)).createRepository();
+            final NodeStore target = getTargetNodeStore();
+            doUpgradeRepository(source, target);
+            targetNodeStore = target;
         }
     }
 
-    public InputStream getRepositoryConfig() {
+    protected synchronized NodeStore getTargetNodeStore() {
+        if (targetNodeStore == null) {
+            targetNodeStore = createTargetNodeStore();
+        }
+        return targetNodeStore;
+    }
+
+    protected static File getTestDirectory() {
+        return testDirectory;
+    }
+
+    protected RepositoryImpl createSourceRepository(File repositoryHome) throws IOException, RepositoryException {
+        InputStream repoConfig = getRepositoryConfig();
+        RepositoryConfig config;
+        if (repoConfig == null) {
+            config = RepositoryConfig.install(repositoryHome);
+        } else {
+            OutputStream out = FileUtils.openOutputStream(new File(repositoryHome, "repository.xml"));
+            IOUtils.copy(repoConfig, out);
+            out.close();
+            repoConfig.close();
+            config = RepositoryConfig.create(repositoryHome);
+        }
+        return RepositoryImpl.create(config);
+    }
+
+
+    protected void doUpgradeRepository(File source, NodeStore target)throws RepositoryException{
+        RepositoryUpgrade.copy(source, target);
+    }
+
+    public InputStream getRepositoryConfig(){
         return null;
     }
 
-    public Repository getTargetRepository() {
-        return targetRepository;
+    public Repository getTargetRepository(){
+        return new Jcr(new Oak(targetNodeStore)).createRepository();
     }
 
-    public JackrabbitSession createAdminSession() throws RepositoryException {
-        return (JackrabbitSession) getTargetRepository().login(CREDENTIALS);
+    public JackrabbitSession createAdminSession()throws RepositoryException{
+        return(JackrabbitSession)getTargetRepository().login(CREDENTIALS);
     }
 
     protected abstract void createSourceContent(Repository repository) throws Exception;
 
+    protected void assertExisting(final String... paths) throws RepositoryException {
+        final Session session = createAdminSession();
+        try {
+            for (final String path : paths) {
+                final String relPath = path.substring(1);
+                assertTrue("node " + path + " should exist", session.getRootNode().hasNode(relPath));
+            }
+        } finally {
+            session.logout();
+        }
+    }
+
+    protected void assertMissing(final String... paths) throws RepositoryException {
+        final Session session = createAdminSession();
+        try {
+            for (final String path : paths) {
+                final String relPath = path.substring(1);
+                assertFalse("node " + path + " should not exist", session.getRootNode().hasNode(relPath));
+            }
+        } finally {
+            session.logout();
+        }
+    }
 }

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java?rev=1691280&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java Wed Jul 15 21:41:03 2015
@@ -0,0 +1,221 @@
+/*
+ * 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.upgrade;
+
+import org.apache.jackrabbit.api.JackrabbitWorkspace;
+import org.apache.jackrabbit.api.security.authorization.PrivilegeManager;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Test case to simulate an incremental upgrade, where a source repository is
+ * copied to target initially. Then some modifications are made in the source
+ * repository and these are (incrementally) copied to the target repository.
+ * <br>
+ * The expectation is that in the end the state in the target repository is
+ * identical to the state in the source repository, with the exception of any
+ * initial content that the upgrade tool created.
+ */
+public class RepeatedRepositoryUpgradeTest extends AbstractRepositoryUpgradeTest {
+
+    private static boolean upgradeComplete;
+    private static FileStore fileStore;
+
+    @Override
+    protected NodeStore createTargetNodeStore() {
+        return new SegmentNodeStore(fileStore);
+    }
+
+    @BeforeClass
+    public static void initialize() {
+        final File dir = new File(getTestDirectory(), "segments");
+        dir.mkdirs();
+        try {
+            fileStore = FileStore.newFileStore(dir).withMaxFileSize(128).create();
+            upgradeComplete = false;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @AfterClass
+    public static void cleanup() {
+        fileStore.close();
+        fileStore = null;
+    }
+
+    @Before
+    public synchronized void upgradeRepository() throws Exception {
+        if (!upgradeComplete) {
+            final File sourceDir = new File(getTestDirectory(), "jackrabbit2");
+
+            sourceDir.mkdirs();
+
+            RepositoryImpl source = createSourceRepository(sourceDir);
+
+            try {
+                createSourceContent(source);
+            } finally {
+                source.shutdown();
+            }
+
+            final NodeStore target = getTargetNodeStore();
+            doUpgradeRepository(sourceDir, target);
+            fileStore.flush();
+
+            // re-create source repo
+            source = createSourceRepository(sourceDir);
+            try {
+                modifySourceContent(source);
+            } finally {
+                source.shutdown();
+            }
+
+            doUpgradeRepository(sourceDir, target);
+            fileStore.flush();
+
+            upgradeComplete = true;
+        }
+    }
+
+    @Override
+    protected void doUpgradeRepository(File source, NodeStore target) throws RepositoryException {
+        final RepositoryConfig config = RepositoryConfig.create(source);
+        final RepositoryContext context = RepositoryContext.create(config);
+        try {
+            final RepositoryUpgrade repositoryUpgrade = new RepositoryUpgrade(context, target);
+            repositoryUpgrade.copy(new RepositoryInitializer() {
+                @Override
+                public void initialize(@Nonnull NodeBuilder builder) {
+                    builder.child("foo").child("bar");
+                }
+            });
+        } finally {
+            context.getRepository().shutdown();
+        }
+    }
+
+    @Override
+    protected void createSourceContent(Repository repository) throws RepositoryException {
+        Session session = null;
+        try {
+            session = repository.login(CREDENTIALS);
+
+            registerCustomPrivileges(session);
+
+            JcrUtils.getOrCreateByPath("/content/child1/grandchild1", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/content/child1/grandchild2", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/content/child1/grandchild3", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/content/child2/grandchild1", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/content/child2/grandchild2", "nt:unstructured", session);
+
+            session.save();
+        } finally {
+            if (session != null && session.isLive()) {
+                session.logout();
+            }
+        }
+    }
+
+    private void modifySourceContent(Repository repository) throws RepositoryException {
+        Session session = null;
+        try {
+            session = repository.login(CREDENTIALS);
+
+            JcrUtils.getOrCreateByPath("/content/child2/grandchild3", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/content/child3", "nt:unstructured", session);
+
+            final Node child1 = JcrUtils.getOrCreateByPath("/content/child1", "nt:unstructured", session);
+            child1.remove();
+
+            session.save();
+        } finally {
+            if (session != null && session.isLive()) {
+                session.logout();
+            }
+        }
+    }
+
+    private void registerCustomPrivileges(Session session) throws RepositoryException {
+        final JackrabbitWorkspace workspace = (JackrabbitWorkspace) session.getWorkspace();
+
+        final NamespaceRegistry registry = workspace.getNamespaceRegistry();
+        registry.registerNamespace("test", "http://www.example.org/");
+
+        final PrivilegeManager privilegeManager = workspace.getPrivilegeManager();
+        privilegeManager.registerPrivilege("test:privilege", false, null);
+        privilegeManager.registerPrivilege(
+                "test:aggregate", false, new String[]{"jcr:read", "test:privilege"});
+    }
+
+    @Test
+    public void shouldReflectSourceAfterModifications() throws Exception {
+
+        assertExisting(
+                "/",
+                "/content",
+                "/content/child2",
+                "/content/child2/grandchild1",
+                "/content/child2/grandchild2",
+                "/content/child2/grandchild3",
+                "/content/child3"
+        );
+
+        assertMissing(
+                "/content/child1"
+        );
+    }
+
+    @Test
+    public void shouldContainCustomInitializerContent() throws Exception {
+        assertExisting(
+                "/foo",
+                "/foo/bar"
+        );
+    }
+
+    @Test
+    public void shouldContainUpgradeInitializedContent() throws Exception {
+        assertExisting(
+                "/rep:security",
+                "/oak:index"
+        );
+    }
+
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java?rev=1691280&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java Wed Jul 15 21:41:03 2015
@@ -0,0 +1,183 @@
+/*
+ * 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.upgrade.nodestate;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Test;
+
+import static com.google.common.collect.ImmutableSet.of;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.commit;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.create;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.createNodeStoreWithContent;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.expectDifference;
+
+public class NodeStateCopierTest {
+
+    private final PropertyState primaryType =
+            createProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+    @Test
+    public void shouldMergeIdenticalContent() throws CommitFailedException {
+        final NodeStore source = createPrefilledNodeStore();
+        final NodeStore target = createPrefilledNodeStore();
+
+        final NodeState before = target.getRoot();
+        NodeStateCopier.copyNodeStore(source, target);
+        final NodeState after = target.getRoot();
+
+        expectDifference().strict().verify(before, after);
+        expectDifference().strict().verify(source.getRoot(), after);
+    }
+
+    private NodeStore createPrefilledNodeStore() throws CommitFailedException {
+        final NodeStore store = createNodeStoreWithContent();
+        final NodeBuilder builder = store.getRoot().builder();
+        create(builder, "/excluded");
+        create(builder, "/a", primaryType, createProperty("name", "a"));
+        create(builder, "/a/b", primaryType, createProperty("name", "b"));
+        create(builder, "/a/b/excluded");
+        create(builder, "/a/b/c", primaryType, createProperty("name", "c"));
+        create(builder, "/a/b/c/d", primaryType);
+        create(builder, "/a/b/c/e", primaryType);
+        create(builder, "/a/b/c/f", primaryType);
+        commit(store, builder);
+        return store;
+    }
+
+    @Test
+    public void shouldRespectMergePaths() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/content/foo/en", "/content/bar/en");
+        final NodeStore target = createNodeStoreWithContent("/content/foo/de");
+
+        final NodeBuilder builder = target.getRoot().builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content"));
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .childNodeAdded("/content/foo/de")
+                .childNodeChanged("/content", "/content/foo")
+                .verify(source.getRoot(), after);
+    }
+
+
+    @Test
+    public void shouldDeleteExistingNodes() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/content/foo");
+        final NodeStore target = createNodeStoreWithContent("/content/bar");
+
+        final NodeState before = target.getRoot();
+        final NodeBuilder builder = before.builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", ImmutableSet.<String>of());
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .childNodeAdded("/content/foo")
+                .childNodeChanged("/content")
+                .childNodeDeleted("/content/bar")
+                .verify(before, after);
+    }
+
+    @Test
+    public void shouldDeleteExistingPropertyIfMissingInSource() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/a");
+        final NodeStore target = createNodeStoreWithContent();
+        NodeBuilder builder = target.getRoot().builder();
+        create(builder, "/a", primaryType);
+        commit(target, builder);
+
+        final NodeState before = target.getRoot();
+        builder = before.builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", ImmutableSet.<String>of());
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .propertyDeleted("/a/jcr:primaryType")
+                .childNodeChanged("/a")
+                .verify(before, after);
+    }
+
+    @Test
+    public void shouldNotDeleteExistingNodesIfMerged() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/content/foo");
+        final NodeStore target = createNodeStoreWithContent("/content/bar");
+
+        final NodeState before = target.getRoot();
+        final NodeBuilder builder = before.builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content/bar"));
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .childNodeAdded("/content/foo")
+                .childNodeChanged("/content")
+                .verify(before, after);
+    }
+
+    @Test
+    public void shouldNotDeleteExistingNodesIfDescendantsOfMerged() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/content/foo");
+        final NodeStore target = createNodeStoreWithContent("/content/bar");
+
+        final NodeState before = target.getRoot();
+        final NodeBuilder builder = before.builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content"));
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .childNodeAdded("/content/foo")
+                .childNodeChanged("/content")
+                .verify(before, after);
+    }
+
+    @Test
+    public void shouldIgnoreNonMatchingMergePaths() throws CommitFailedException {
+        final NodeStore source = createNodeStoreWithContent("/content/foo");
+        final NodeStore target = createNodeStoreWithContent("/content/bar");
+
+        final NodeState before = target.getRoot();
+        final NodeBuilder builder = before.builder();
+        NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/con"));
+        commit(target, builder);
+        final NodeState after = target.getRoot();
+
+        expectDifference()
+                .strict()
+                .childNodeAdded("/content/foo")
+                .childNodeChanged("/content")
+                .childNodeDeleted("/content/bar")
+                .verify(before, after);
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java?rev=1691280&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java Wed Jul 15 21:41:03 2015
@@ -0,0 +1,205 @@
+/*
+ * 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.upgrade.util;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.DefaultValidator;
+import org.apache.jackrabbit.oak.spi.commit.EditorDiff;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.commit.Validator;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import static org.junit.Assert.assertEquals;
+
+public class NodeStateTestUtils {
+
+    private NodeStateTestUtils() {
+        // no instances
+    }
+
+    public static NodeStore createNodeStoreWithContent(String... paths) throws CommitFailedException {
+        final SegmentNodeStore store = new SegmentNodeStore();
+        final NodeBuilder builder = store.getRoot().builder();
+        for (String path : paths) {
+            create(builder, path);
+        }
+        commit(store, builder);
+        return store;
+    }
+
+    public static void create(NodeBuilder rootBuilder, String path, PropertyState... properties) {
+        final NodeBuilder builder = createOrGetBuilder(rootBuilder, path);
+        for (PropertyState property : properties) {
+            builder.setProperty(property);
+        }
+    }
+
+    public static void commit(NodeStore store, NodeBuilder rootBuilder) throws CommitFailedException {
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    public static NodeBuilder createOrGetBuilder(NodeBuilder builder, String path) {
+        NodeBuilder current = builder;
+        for (final String name : PathUtils.elements(path)) {
+            current = current.child(name);
+        }
+        return current;
+    }
+
+    public static ExpectedDifference expectDifference() {
+        return new ExpectedDifference();
+    }
+
+    public static class ExpectedDifference {
+
+        private ExpectedDifference() {
+        }
+
+        private final Map<String, Set<String>> expected = new HashMap<String, Set<String>>();
+
+        public void verify(NodeState before, NodeState after) {
+            final Map<String, Set<String>> actual = TestValidator.compare(before, after);
+            for (String type : expected.keySet()) {
+                if (!actual.containsKey(type)) {
+                    actual.put(type, Collections.<String>emptySet());
+                }
+                assertEquals(type, expected.get(type), actual.get(type));
+            }
+        }
+
+        public ExpectedDifference propertyAdded(String... paths) {
+            return expect("propertyAdded", paths);
+        }
+
+        public ExpectedDifference propertyChanged(String... paths) {
+            return expect("propertyChanged", paths);
+        }
+
+        public ExpectedDifference propertyDeleted(String... paths) {
+            return expect("propertyDeleted", paths);
+        }
+
+        public ExpectedDifference childNodeAdded(String... paths) {
+            return expect("childNodeAdded", paths);
+        }
+
+        public ExpectedDifference childNodeChanged(String... paths) {
+            return expect("childNodeChanged", paths);
+        }
+
+        public ExpectedDifference childNodeDeleted(String... paths) {
+            return expect("childNodeDeleted", paths);
+        }
+
+        public ExpectedDifference strict() {
+            return this.propertyAdded()
+                    .propertyChanged()
+                    .propertyDeleted()
+                    .childNodeAdded()
+                    .childNodeChanged()
+                    .childNodeDeleted();
+        }
+
+        private ExpectedDifference expect(String type, String... paths) {
+            if (!expected.containsKey(type)) {
+                expected.put(type, new TreeSet<String>());
+            }
+            Collections.addAll(expected.get(type), paths);
+            return this;
+        }
+    }
+
+    private static class TestValidator extends DefaultValidator {
+
+        final Map<String, Set<String>> actual = new HashMap<String, Set<String>>();
+
+        String path = "/";
+
+        public static Map<String, Set<String>> compare(NodeState before, NodeState after) {
+            final TestValidator validator = new TestValidator();
+            EditorDiff.process(validator, before, after);
+            return validator.actual;
+        }
+
+        @Override
+        public void leave(NodeState before, NodeState after) throws CommitFailedException {
+            path = PathUtils.getParentPath(path);
+        }
+
+        @Override
+        public void propertyAdded(PropertyState after) throws CommitFailedException {
+            record("propertyAdded", PathUtils.concat(path, after.getName()));
+        }
+
+        @Override
+        public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException {
+            record("propertyChanged", PathUtils.concat(path, after.getName()));
+        }
+
+        @Override
+        public void propertyDeleted(PropertyState before) throws CommitFailedException {
+            record("propertyDeleted", PathUtils.concat(path, before.getName()));
+        }
+
+        @Override
+        public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
+            path = PathUtils.concat(path, name);
+            record("childNodeAdded", path);
+            return this;
+        }
+
+        @Override
+        public Validator childNodeChanged(String name, NodeState before, NodeState after)
+                throws CommitFailedException {
+            // make sure not to record false positives (inefficient for large trees)
+            if (!before.equals(after)) {
+                path = PathUtils.concat(path, name);
+                record("childNodeChanged", path);
+                return this;
+            }
+            return null;
+        }
+
+        @Override
+        public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException {
+            path = PathUtils.concat(path, name);
+            record("childNodeDeleted", path);
+            return this;
+        }
+
+        private void record(String type, String path) {
+            if (!actual.containsKey(type)) {
+                actual.put(type, new TreeSet<String>());
+            }
+            actual.get(type).add(path);
+        }
+    }
+}



Re: svn commit: r1691280 - in /jackrabbit/oak/trunk/oak-upgrade/src: main/java/org/apache/jackrabbit/oak/upgrade/ main/java/org/apache/jackrabbit/oak/upgrade/nodestate/ main/java/org/apache/jackrabbit/oak/upgrade/security/ test/java/org/apache/jackrabbit/o...

Posted by Manfred Baedke <ma...@gmail.com>.
Thanks Michael, I didn't know.

On 17.07.15 10:27, Michael Dürig wrote:
>
>
> On 15.7.15 11:41 , baedke@apache.org wrote:
>> Author: baedke
>> Date: Wed Jul 15 21:41:03 2015
>> New Revision: 1691280
>>
>> URL:http://svn.apache.org/r1691280
>> Log:
>> OAK-2619: Repeated upgrades
>>
>> Added support for incremental upgrades.
>
> AFAICS this is mostly based on a patch from Julian Sedding. I think it 
> is good practice to give people who send patches credits in the commit 
> message once a patch gets applied. Something along the lines of 
> "credits to ... for the/a (initial) patch).
>
> Michael


Re: svn commit: r1691280 - in /jackrabbit/oak/trunk/oak-upgrade/src: main/java/org/apache/jackrabbit/oak/upgrade/ main/java/org/apache/jackrabbit/oak/upgrade/nodestate/ main/java/org/apache/jackrabbit/oak/upgrade/security/ test/java/org/apache/jackrabbit/o...

Posted by Michael Dürig <md...@apache.org>.

On 15.7.15 11:41 , baedke@apache.org wrote:
> Author: baedke
> Date: Wed Jul 15 21:41:03 2015
> New Revision: 1691280
>
> URL:http://svn.apache.org/r1691280
> Log:
> OAK-2619: Repeated upgrades
>
> Added support for incremental upgrades.

AFAICS this is mostly based on a patch from Julian Sedding. I think it 
is good practice to give people who send patches credits in the commit 
message once a patch gets applied. Something along the lines of "credits 
to ... for the/a (initial) patch).

Michael