From b253cd110dc785543c629c39e5732ec1e4951e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 4 Jun 2019 16:37:53 +0200 Subject: [PATCH 01/33] Initial migration servlet --- .../scm/security/DefaultCipherHandler.java | 17 +++-- .../scm/boot/BootstrapContextListener.java | 48 +++++++++--- .../MigrationWizardContextListener.java | 22 ++++++ .../scm/update/MigrationWizardModule.java | 14 ++++ .../scm/update/MigrationWizardServlet.java | 75 +++++++++++++++++++ .../update/repository/MigrationStrategy.java | 2 +- .../repository/MigrationStrategyDao.java | 2 + .../repository/XmlRepositoryV1UpdateStep.java | 75 +++++++++++++------ .../repository-migration-restart.mustache | 10 +++ .../templates/repository-migration.mustache | 38 ++++++++++ 10 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java create mode 100644 scm-webapp/src/main/resources/templates/repository-migration-restart.mustache create mode 100644 scm-webapp/src/main/resources/templates/repository-migration.mustache diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index 9c1fa590cc..8c3afbd90f 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler { * @return decrypted value */ public String decode(char[] plainKey, String value) { - String result = null; - + Base64.Decoder decoder = Base64.getUrlDecoder(); try { - byte[] encodedInput = Base64.getUrlDecoder().decode(value); + return decode(plainKey, value, decoder); + } catch (IllegalArgumentException e) { + return decode(plainKey, value, Base64.getDecoder()); + } + } + + private String decode(char[] plainKey, String value, Base64.Decoder decoder) { + try { + byte[] encodedInput = decoder.decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler { byte[] decoded = cipher.doFinal(encoded); - result = new String(decoded, ENCODING); + return new String(decoded, ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not decode string", ex); } - - return result; } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 3af8a76650..be7955ea28 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -46,6 +46,7 @@ import sonia.scm.ScmEventBusModule; import sonia.scm.ScmInitializerModule; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; +import sonia.scm.migration.UpdateException; import sonia.scm.plugin.DefaultPluginLoader; import sonia.scm.plugin.Plugin; import sonia.scm.plugin.PluginException; @@ -54,6 +55,7 @@ import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginsInternal; import sonia.scm.plugin.SmpArchive; +import sonia.scm.update.MigrationWizardContextListener; import sonia.scm.update.UpdateEngine; import sonia.scm.util.ClassLoaders; import sonia.scm.util.IOUtil; @@ -110,14 +112,16 @@ public class BootstrapContextListener implements ServletContextListener { public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - for (PluginWrapper plugin : contextListener.getPlugins()) { - ClassLoader pcl = plugin.getClassLoader(); + if (contextListener instanceof ScmContextListener) { + for (PluginWrapper plugin : ((ScmContextListener) contextListener).getPlugins()) { + ClassLoader pcl = plugin.getClassLoader(); - if (pcl instanceof Closeable) { - try { - ((Closeable) pcl).close(); - } catch (IOException ex) { - logger.warn("could not close plugin classloader", ex); + if (pcl instanceof Closeable) { + try { + ((Closeable) pcl).close(); + } catch (IOException ex) { + logger.warn("could not close plugin classloader", ex); + } } } } @@ -151,7 +155,9 @@ public class BootstrapContextListener implements ServletContextListener { } private void createContextListener(File pluginDirectory) { + try { + renameOldPluginsFolder(pluginDirectory); if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { @@ -166,14 +172,36 @@ public class BootstrapContextListener implements ServletContextListener { Injector bootstrapInjector = createBootstrapInjector(pluginLoader); - processUpdates(pluginLoader, bootstrapInjector); + MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + if (wizardContextListener.wizardNecessary()) { + contextListener = wizardContextListener; + } else { + processUpdates(pluginLoader, bootstrapInjector); + + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } } + private void renameOldPluginsFolder(File pluginDirectory) { + if (new File(pluginDirectory, "classpath.xml").exists()) { + File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1"); + boolean renamed = pluginDirectory.renameTo(backupDirectory); + if (renamed) { + logger.warn("moved old plugins directory to {}", backupDirectory); + } else { + throw new UpdateException("could not rename existing v1 plugin directory"); + } + } + } + + private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) { + return new MigrationWizardContextListener(bootstrapInjector); + } + private Injector createBootstrapInjector(PluginLoader pluginLoader) { Module scmContextListenerModule = new ScmContextListenerModule(); BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader); @@ -402,7 +430,7 @@ public class BootstrapContextListener implements ServletContextListener { private ServletContext context; /** Field description */ - private ScmContextListener contextListener; + private ServletContextListener contextListener; /** Field description */ private boolean registered = false; diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java new file mode 100644 index 0000000000..c98062c900 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java @@ -0,0 +1,22 @@ +package sonia.scm.update; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; + +public class MigrationWizardContextListener extends GuiceServletContextListener { + + private final Injector injector; + + public MigrationWizardContextListener(Injector bootstrapInjector) { + this.injector = bootstrapInjector.createChildInjector(new MigrationWizardModule()); + } + + public boolean wizardNecessary() { + return injector.getInstance(MigrationWizardServlet.class).wizardNecessary(); + } + + @Override + protected Injector getInjector() { + return injector; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java new file mode 100644 index 0000000000..3362496330 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -0,0 +1,14 @@ +package sonia.scm.update; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import java.util.List; + +class MigrationWizardModule extends ServletModule { + + @Override + protected void configureServlets() { + serve("/*").with(MigrationWizardServlet.class); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java new file mode 100644 index 0000000000..d65a8408a0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -0,0 +1,75 @@ +package sonia.scm.update; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import sonia.scm.boot.RestartEvent; +import sonia.scm.event.ScmEventBus; +import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@Singleton +class MigrationWizardServlet extends HttpServlet { + + private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; + private final MigrationStrategyDao migrationStrategyDao; + + @Inject + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) { + this.repositoryV1UpdateStep = repositoryV1UpdateStep; + this.migrationStrategyDao = migrationStrategyDao; + } + + public boolean wizardNecessary() { + return !repositoryV1UpdateStep.missingMigrationStrategies().isEmpty(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); + + resp.setStatus(200); + + HashMap model = new HashMap<>(); + + model.put("submitUrl", req.getRequestURI()); + model.put("repositories", missingMigrationStrategies); + model.put("strategies", getMigrationStrategies()); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration.mustache"); + mustache.execute(resp.getWriter(), model).flush(); + } + + private List getMigrationStrategies() { + return stream(MigrationStrategy.values()).map(Enum::name).collect(toList()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(200); + + req.getParameterMap().forEach( + (name, strategy) -> migrationStrategyDao.set(name, MigrationStrategy.valueOf(strategy[0])) + ); + + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); + mustache.execute(resp.getWriter(), new Object()).flush(); + + ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index c7bb2cba86..d4b6c5c0f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -4,7 +4,7 @@ import com.google.inject.Injector; import java.nio.file.Path; -enum MigrationStrategy { +public enum MigrationStrategy { COPY(CopyMigrationStrategy.class), MOVE(MoveMigrationStrategy.class), diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 15c931bf31..8ddb5e02df 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import javax.inject.Inject; +import javax.inject.Singleton; import java.util.Optional; +@Singleton public class MigrationStrategyDao { private final RepositoryMigrationPlan plan; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 34c08fb16b..acb28ca432 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -32,7 +32,9 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.version.Version.parse; @@ -109,6 +111,23 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ); } + public List missingMigrationStrategies() { + if (!resolveV1File().exists()) { + LOG.info("no v1 repositories database file found"); + return emptyList(); + } + try { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class); + return readV1Database(jaxbContext) + .map(v1Database -> v1Database.repositoryList.repositories.stream()) + .orElse(Stream.empty()) + .filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent()) + .collect(Collectors.toList()); + } catch (JAXBException e) { + throw new UpdateException("could not read v1 repository database", e); + } + } + private void backupOldRepositoriesFile() { Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); Path oldRepositoriesFile = configDir.resolve("repositories.xml"); @@ -126,8 +145,8 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { Repository repository = new Repository( v1Repository.id, v1Repository.type, - getNamespace(v1Repository), - getName(v1Repository), + v1Repository.getNewNamespace(), + v1Repository.getNewName(), v1Repository.contact, v1Repository.description, createPermissions(v1Repository)); @@ -142,10 +161,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) { - return migrationStrategyDao.get(v1Repository.id) + return findMigrationStrategy(v1Repository) .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); } + private Optional findMigrationStrategy(V1Repository v1Repository) { + return migrationStrategyDao.get(v1Repository.id); + } + private RepositoryPermission[] createPermissions(V1Repository v1Repository) { if (v1Repository.permissions == null) { return new RepositoryPermission[0]; @@ -161,24 +184,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission); } - private String getNamespace(V1Repository v1Repository) { - String[] nameParts = getNameParts(v1Repository.name); - return nameParts.length > 1 ? nameParts[0] : v1Repository.type; - } - - private String getName(V1Repository v1Repository) { - String[] nameParts = getNameParts(v1Repository.name); - return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); - } - - private String concatPathElements(String[] nameParts) { - return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); - } - - private String[] getNameParts(String v1Name) { - return v1Name.split("/"); - } - private Optional readV1Database(JAXBContext jaxbContext) throws JAXBException { Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File()); if (unmarshal instanceof V1RepositoryDatabase) { @@ -205,7 +210,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") - private static class V1Repository { + public static class V1Repository { private String contact; private long creationDate; private Long lastModified; @@ -218,6 +223,32 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private List permissions; private V1Properties properties; + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getNewNamespace() { + String[] nameParts = getNameParts(name); + return nameParts.length > 1 ? nameParts[0] : type; + } + + public String getNewName() { + String[] nameParts = getNameParts(name); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); + } + + private String[] getNameParts(String v1Name) { + return v1Name.split("/"); + } + + private String concatPathElements(String[] nameParts) { + return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); + } + @Override public String toString() { return "V1Repository{" + diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache new file mode 100644 index 0000000000..e47945174a --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -0,0 +1,10 @@ + + + + SCM-Manager Restart + + + +SCM-Manager will restart to migrate the data. + + diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache new file mode 100644 index 0000000000..641e718136 --- /dev/null +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -0,0 +1,38 @@ + + + + SCM-Manager Migration + + + +

SCM-Manager Migration

+You have migrated from SCM-Manager v1 to SCM-Manager v2. +
+ + + + + + + {{#repositories}} + + + + + + {{/repositories}} +
original namenew namespace/nameStrategy
+ {{name}} + + {{newNamespace}}/{{newName}} + + +
+ +
+ + From e52518a12b8eecb581f2ce3f2bf6430076ffb6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 08:21:37 +0200 Subject: [PATCH 02/33] Cleanup - Mark PathBasedRepositoryLocationResolver as singleton so that other users will get the same instance and will not overwrite the paths set by migration. - Set path kept by InlineMigrationStrategy in location resolver to store the path. - Add logging - Add type of repository to migration web page --- .../RepositoryLocationResolver.java | 3 +- .../PathBasedRepositoryLocationResolver.java | 28 ++++++++++++++----- .../repository/xml/XmlRepositoryDAOTest.java | 14 +++++++++- .../TempDirRepositoryLocationResolver.java | 13 +++++++-- .../scm/update/MigrationWizardModule.java | 12 ++++++-- .../repository/CopyMigrationStrategy.java | 5 ++++ .../repository/InlineMigrationStrategy.java | 12 +++++++- .../repository/MoveMigrationStrategy.java | 1 + .../repository/XmlRepositoryV1UpdateStep.java | 5 ++++ .../templates/repository-migration.mustache | 4 +++ .../InlineMigrationStrategyTest.java | 13 +++++++-- 11 files changed, 93 insertions(+), 17 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index bc8df673d0..8c76bdabaa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -13,8 +13,9 @@ public abstract class RepositoryLocationResolver { return create(type); } - @FunctionalInterface public interface RepositoryLocationResolverInstance { T getLocation(String repositoryId); + + void setLocation(String repositoryId, T location); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index a1ce516069..dccdbc197b 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.store.StoreConstants; import javax.inject.Inject; +import javax.inject.Singleton; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -28,6 +29,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; * * @since 2.0.0 */ +@Singleton public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { public static final String STORE_NAME = "repository-paths"; @@ -64,19 +66,26 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override protected RepositoryLocationResolverInstance create(Class type) { - return repositoryId -> { - if (pathById.containsKey(repositoryId)) { - return (T) contextProvider.resolve(pathById.get(repositoryId)); - } else { - return (T) create(repositoryId); + return new RepositoryLocationResolverInstance() { + @Override + public T getLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + return (T) create(repositoryId); + } + } + + @Override + public void setLocation(String repositoryId, T location) { + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, (Path) location); } }; } Path create(String repositoryId) { Path path = initialRepositoryLocationResolver.getPath(repositoryId); - pathById.put(repositoryId, path); - writePathDatabase(); + setLocation(repositoryId, path); Path resolvedPath = contextProvider.resolve(path); try { Files.createDirectories(resolvedPath); @@ -138,4 +147,9 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); } + + public void setLocation(String repositoryId, Path repositoryBasePath) { + pathById.put(repositoryId, repositoryBasePath); + writePathDatabase(); + } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 5b9a00aec8..36c9db33e2 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -19,6 +19,7 @@ import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.RepositoryPermission; import java.io.IOException; @@ -56,7 +57,18 @@ class XmlRepositoryDAOTest { @BeforeEach void createDAO(@TempDirectory.TempDir Path basePath) { - when(locationResolver.create(Path.class)).thenReturn(locationResolver::create); + when(locationResolver.create(Path.class)).thenReturn( + new RepositoryLocationResolver.RepositoryLocationResolverInstance() { + @Override + public Path getLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + + @Override + public void setLocation(String repositoryId, Path location) { + } + } + ); when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString())); } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index 1dfc22e15a..77b09ff707 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -1,7 +1,6 @@ package sonia.scm; import sonia.scm.repository.BasicRepositoryLocationResolver; -import sonia.scm.repository.RepositoryLocationResolver; import java.io.File; import java.nio.file.Path; @@ -16,6 +15,16 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe @Override protected RepositoryLocationResolverInstance create(Class type) { - return repositoryId -> (T) tempDirectory.toPath(); + return new RepositoryLocationResolverInstance() { + @Override + public T getLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + + @Override + public void setLocation(String repositoryId, T location) { + throw new UnsupportedOperationException("not implemented for tests"); + } + }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index 3362496330..f067fa799b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -1,14 +1,20 @@ package sonia.scm.update; import com.google.inject.servlet.ServletModule; -import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; - -import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class MigrationWizardModule extends ServletModule { + private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardModule.class); + @Override protected void configureServlets() { + LOG.info("=========================================================="); + LOG.info("= ="); + LOG.info("= STARTING MIGRATION SERVLET ="); + LOG.info("= ="); + LOG.info("=========================================================="); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index 060ed6704e..89394060f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -1,5 +1,7 @@ package sonia.scm.update.repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; import sonia.scm.repository.RepositoryDirectoryHandler; import sonia.scm.repository.RepositoryLocationResolver; @@ -10,6 +12,8 @@ import java.nio.file.Path; class CopyMigrationStrategy extends BaseMigrationStrategy { + private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class); + private final RepositoryLocationResolver locationResolver; @Inject @@ -24,6 +28,7 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); + LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath); copyData(sourceDataPath, targetDataPath); return repositoryBasePath; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 62dd67d86a..2f891fff71 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -1,7 +1,10 @@ package sonia.scm.update.repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; import sonia.scm.repository.RepositoryDirectoryHandler; +import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; import java.nio.file.Files; @@ -9,16 +12,23 @@ import java.nio.file.Path; class InlineMigrationStrategy extends BaseMigrationStrategy { + private static final Logger LOG = LoggerFactory.getLogger(InlineMigrationStrategy.class); + + private final RepositoryLocationResolver locationResolver; + @Inject - public InlineMigrationStrategy(SCMContextProvider contextProvider) { + public InlineMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { super(contextProvider); + this.locationResolver = locationResolver; } @Override public Path migrate(String id, String name, String type) { Path repositoryBasePath = getSourceDataPath(name, type); + locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); + LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath); moveData(repositoryBasePath, targetDataPath); return repositoryBasePath; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index 08d71dd376..c571b8ad4c 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -32,6 +32,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); + LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath); moveData(sourceDataPath, targetDataPath); deleteOldDataDir(getTypeDependentPath(type), name); return repositoryBasePath; diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index acb28ca432..b7037d0e05 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -157,6 +157,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private Path handleDataDirectory(V1Repository v1Repository) { MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); + LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); } @@ -231,6 +232,10 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return name; } + public String getType() { + return type; + } + public String getNewNamespace() { String[] nameParts = getNameParts(name); return nameParts.length > 1 ? nameParts[0] : type; diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 641e718136..07447bedbc 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -11,6 +11,7 @@ You have migrated from SCM-Manager v1 to SCM-Manager v2. + @@ -19,6 +20,9 @@ You have migrated from SCM-Manager v1 to SCM-Manager v2. + diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java index 6abddae3fb..fa237e78b0 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java @@ -7,11 +7,14 @@ import org.junitpioneer.jupiter.TempDirectory; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import java.io.IOException; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(TempDirectory.class) @@ -20,9 +23,14 @@ class InlineMigrationStrategyTest { @Mock SCMContextProvider contextProvider; + @Mock + PathBasedRepositoryLocationResolver locationResolver; + @Mock + RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance; @BeforeEach void mockContextProvider(@TempDirectory.TempDir Path tempDir) { + when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance); when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); } @@ -33,13 +41,14 @@ class InlineMigrationStrategyTest { @Test void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); assertThat(target).isEqualTo(resolveOldDirectory(tempDir)); + verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target); } @Test void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) { - new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); assertThat(resolveOldDirectory(tempDir).resolve("data")).exists(); } From 9a1d80327ee3528543036cfc02f0a353b426a9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 10:24:52 +0200 Subject: [PATCH 03/33] Delete old repository data directories for inline --- .../update/repository/InlineMigrationStrategy.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 2f891fff71..60f03666a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -7,6 +7,7 @@ import sonia.scm.repository.RepositoryDirectoryHandler; import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -34,6 +35,10 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { } private void moveData(Path sourceDirectory, Path targetDirectory) { + moveData(sourceDirectory, targetDirectory, false); + } + + private void moveData(Path sourceDirectory, Path targetDirectory, boolean deleteDirectory) { createDataDirectory(targetDirectory); listSourceDirectory(sourceDirectory) .filter(sourceFile -> !targetDirectory.equals(sourceFile)) @@ -41,11 +46,18 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { sourceFile -> { Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); if (Files.isDirectory(sourceFile)) { - moveData(sourceFile, targetFile); + moveData(sourceFile, targetFile, true); } else { moveFile(sourceFile, targetFile); } } ); + if (deleteDirectory) { + try { + Files.delete(sourceDirectory); + } catch (IOException e) { + LOG.warn("could not delete source repository directory {}", sourceDirectory); + } + } } } From a5c65b4e2ca1597be62a4568b135f9e56aeac57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 10:45:41 +0200 Subject: [PATCH 04/33] Store absolute path for directly set repository locations --- .../repository/xml/PathBasedRepositoryLocationResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index dccdbc197b..c8c462be36 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -78,7 +78,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override public void setLocation(String repositoryId, T location) { - PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, (Path) location); + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); } }; } @@ -148,7 +148,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); } - public void setLocation(String repositoryId, Path repositoryBasePath) { + private void setLocation(String repositoryId, Path repositoryBasePath) { pathById.put(repositoryId, repositoryBasePath); writePathDatabase(); } From c7875e7f78ba81768b30571ac65712a4647c8183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 11:52:36 +0200 Subject: [PATCH 05/33] Style pages --- .../scm/update/MigrationWizardModule.java | 4 + .../scm/update/MigrationWizardServlet.java | 4 +- .../repository-migration-restart.mustache | 24 ++++- .../templates/repository-migration.mustache | 91 ++++++++++++------- 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index f067fa799b..55b1644bb8 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -3,6 +3,8 @@ package sonia.scm.update; import com.google.inject.servlet.ServletModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.PushStateDispatcher; +import sonia.scm.WebResourceServlet; class MigrationWizardModule extends ServletModule { @@ -15,6 +17,8 @@ class MigrationWizardModule extends ServletModule { LOG.info("= STARTING MIGRATION SERVLET ="); LOG.info("= ="); LOG.info("=========================================================="); + bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); + serve("/images/*", "/styles/*").with(WebResourceServlet.class); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index d65a8408a0..7eb11cff8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -15,6 +15,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -45,6 +46,7 @@ class MigrationWizardServlet extends HttpServlet { HashMap model = new HashMap<>(); + model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); model.put("repositories", missingMigrationStrategies); model.put("strategies", getMigrationStrategies()); @@ -68,7 +70,7 @@ class MigrationWizardServlet extends HttpServlet { MustacheFactory mf = new DefaultMustacheFactory(); Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); - mustache.execute(resp.getWriter(), new Object()).flush(); + mustache.execute(resp.getWriter(), Collections.singletonMap("contextPath", req.getContextPath())).flush(); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index e47945174a..2374861fe2 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -2,9 +2,31 @@ SCM-Manager Restart + -SCM-Manager will restart to migrate the data. +
+
+
+
+
+
+
+
SCM-Manager
+
+
+
+
+
+
+
+

SCM-Manager will restart to migrate the data.

+
+
+
+
+
+
diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 07447bedbc..bfc421cd35 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -2,41 +2,66 @@ SCM-Manager Migration + -

SCM-Manager Migration

-You have migrated from SCM-Manager v1 to SCM-Manager v2. -
-
original nametype new namespace/name Strategy
{{name}} + {{type}} + {{newNamespace}}/{{newName}}
- - - - - - - {{#repositories}} - - - - - - - {{/repositories}} -
original nametypenew namespace/nameStrategy
- {{name}} - - {{type}} - - {{newNamespace}}/{{newName}} - - -
- - +
+
+
+
+
+
+
+
SCM-Manager
+
+
+
+
+
+
+
+

SCM-Manager Migration

+

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+
+ + + + + + + + {{#repositories}} + + + + + + + {{/repositories}} +
Original nameTypeNew namespace and nameStrategy
+ {{name}} + + {{type}} + + {{newNamespace}}/{{newName}} + +
+
+ +
+
+
+ +
+
+
+
+
+
From b274952fa91703ca9f983506047ba31d8cc258aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 14:27:35 +0200 Subject: [PATCH 06/33] Create explicit method to create new repository locations --- .../repository/RepositoryLocationResolver.java | 18 ++++++++++++++++++ .../PathBasedRepositoryLocationResolver.java | 15 ++++++++++++++- ...athBasedRepositoryLocationResolverTest.java | 12 ++++++------ .../repository/xml/XmlRepositoryDAOTest.java | 5 +++++ .../scm/TempDirRepositoryLocationResolver.java | 5 +++++ .../SimpleRepositoryHandlerTestBase.java | 8 +++++--- .../repository/CopyMigrationStrategy.java | 2 +- .../repository/MoveMigrationStrategy.java | 2 +- .../repository/CopyMigrationStrategyTest.java | 2 +- .../repository/MoveMigrationStrategyTest.java | 2 +- 10 files changed, 57 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 8c76bdabaa..1b7da51c4c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -14,8 +14,26 @@ public abstract class RepositoryLocationResolver { } public interface RepositoryLocationResolverInstance { + + /** + * Get the existing location for the repository. + * @param repositoryId The id of the repository. + * @throws IllegalStateException when there is no known location for the given repository. + */ T getLocation(String repositoryId); + /** + * Create a new location for the new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ + T createLocation(String repositoryId); + + /** + * Set the location of a new repository. + * @param repositoryId The id of the new repository. + * @throws IllegalStateException when there already is a location for the given repository registered. + */ void setLocation(String repositoryId, T location); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index c8c462be36..96067ba7a2 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -71,6 +71,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation public T getLocation(String repositoryId) { if (pathById.containsKey(repositoryId)) { return (T) contextProvider.resolve(pathById.get(repositoryId)); + } else { + throw new IllegalStateException("location for repository " + repositoryId + " does not exist"); + } + } + + @Override + public T createLocation(String repositoryId) { + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); } else { return (T) create(repositoryId); } @@ -78,7 +87,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation @Override public void setLocation(String repositoryId, T location) { - PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); + if (pathById.containsKey(repositoryId)) { + throw new IllegalStateException("location for repository " + repositoryId + " already exists"); + } else { + PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); + } } }; } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 5f758f124a..754b8469d5 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -57,7 +57,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldCreateInitialDirectory() { - Path path = resolver.forClass(Path.class).getLocation("newId"); + Path path = resolver.forClass(Path.class).createLocation("newId"); assertThat(path).isEqualTo(basePath.resolve("newId")); assertThat(path).isDirectory(); @@ -65,7 +65,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldPersistInitialDirectory() { - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); String content = getXmlFileContent(); @@ -78,7 +78,7 @@ class PathBasedRepositoryLocationResolverTest { long now = CREATION_TIME + 100; when(clock.millis()).thenReturn(now); - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); @@ -91,7 +91,7 @@ class PathBasedRepositoryLocationResolverTest { long now = CREATION_TIME + 100; when(clock.millis()).thenReturn(now); - resolver.forClass(Path.class).getLocation("newId"); + resolver.forClass(Path.class).createLocation("newId"); assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); assertThat(resolver.getLastModified()).isEqualTo(now); @@ -108,8 +108,8 @@ class PathBasedRepositoryLocationResolverTest { @BeforeEach void createExistingDatabase() { - resolver.forClass(Path.class).getLocation("existingId_1"); - resolver.forClass(Path.class).getLocation("existingId_2"); + resolver.forClass(Path.class).createLocation("existingId_1"); + resolver.forClass(Path.class).createLocation("existingId_2"); resolverWithExistingData = createResolver(); } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 36c9db33e2..9ab8925fdb 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -64,6 +64,11 @@ class XmlRepositoryDAOTest { return locationResolver.create(repositoryId); } + @Override + public Path createLocation(String repositoryId) { + return locationResolver.create(repositoryId); + } + @Override public void setLocation(String repositoryId, Path location) { } diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index 77b09ff707..acffe6c769 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -21,6 +21,11 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe return (T) tempDirectory.toPath(); } + @Override + public T createLocation(String repositoryId) { + return (T) tempDirectory.toPath(); + } + @Override public void setLocation(String repositoryId, T location) { throw new UnsupportedOperationException("not implemented for tests"); diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 063681bbc4..615169b58c 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -34,6 +34,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import org.junit.Test; +import org.mockito.stubbing.Answer; import sonia.scm.AbstractTestBase; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory; @@ -82,11 +83,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.create(any())).thenReturn(instanceMock); when(locationResolver.supportsLocationType(any())).thenReturn(true); - - when(instanceMock.getLocation(anyString())).then(ic -> { + Answer pathAnswer = ic -> { String id = ic.getArgument(0); return baseDirectory.toPath().resolve(id); - }); + }; + when(instanceMock.getLocation(anyString())).then(pathAnswer); + when(instanceMock.createLocation(anyString())).then(pathAnswer); handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index 89394060f5..f96413d195 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -24,7 +24,7 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { @Override public Path migrate(String id, String name, String type) { - Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index c571b8ad4c..deb8a8782b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -28,7 +28,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { @Override public Path migrate(String id, String name, String type) { - Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java index b40283ae79..d718554dfe 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java @@ -43,7 +43,7 @@ class CopyMigrationStrategyTest { void mockLocationResolver(@TempDirectory.TempDir Path tempDir) { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.forClass(Path.class)).thenReturn(instanceMock); - when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); + when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java index b55315d85f..e248f82217 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java @@ -40,7 +40,7 @@ class MoveMigrationStrategyTest { void mockLocationResolver(@TempDirectory.TempDir Path tempDir) { RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); when(locationResolver.forClass(Path.class)).thenReturn(instanceMock); - when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); + when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); } @Test From 67f731c43206d7541d6ff2ebcaf6211288a65123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 5 Jun 2019 15:39:36 +0200 Subject: [PATCH 07/33] Heed sonar hints --- .../scm/update/MigrationWizardServlet.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 7eb11cff8a..e451e60120 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -3,6 +3,8 @@ package sonia.scm.update; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; @@ -15,9 +17,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.PrintWriter; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; @@ -25,6 +29,8 @@ import static java.util.stream.Collectors.toList; @Singleton class MigrationWizardServlet extends HttpServlet { + private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class); + private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; private final MigrationStrategyDao migrationStrategyDao; @@ -42,8 +48,6 @@ class MigrationWizardServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); - resp.setStatus(200); - HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); @@ -52,8 +56,8 @@ class MigrationWizardServlet extends HttpServlet { model.put("strategies", getMigrationStrategies()); MustacheFactory mf = new DefaultMustacheFactory(); - Mustache mustache = mf.compile("templates/repository-migration.mustache"); - mustache.execute(resp.getWriter(), model).flush(); + Mustache template = mf.compile("templates/repository-migration.mustache"); + respondWithTemplate(resp, model, template); } private List getMigrationStrategies() { @@ -61,7 +65,7 @@ class MigrationWizardServlet extends HttpServlet { } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); req.getParameterMap().forEach( @@ -69,9 +73,25 @@ class MigrationWizardServlet extends HttpServlet { ); MustacheFactory mf = new DefaultMustacheFactory(); - Mustache mustache = mf.compile("templates/repository-migration-restart.mustache"); - mustache.execute(resp.getWriter(), Collections.singletonMap("contextPath", req.getContextPath())).flush(); + Mustache template = mf.compile("templates/repository-migration-restart.mustache"); + Map model = Collections.singletonMap("contextPath", req.getContextPath()); + + respondWithTemplate(resp, model, template); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + + private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { + PrintWriter writer; + try { + writer = resp.getWriter(); + } catch (IOException e) { + LOG.error("could not create writer for response", e); + resp.setStatus(500); + return; + } + template.execute(writer, model); + writer.flush(); + resp.setStatus(200); + } } From 1bcc150cb90a794bf8f7aa32d82a6e49086077ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 11:15:51 +0200 Subject: [PATCH 08/33] Remove function creep --- .../java/sonia/scm/ScmContextListener.java | 18 +++++++++++++++--- .../scm/boot/BootstrapContextListener.java | 14 -------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 5cb3f5dfa5..874052442d 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -58,6 +58,8 @@ import sonia.scm.util.IOUtil; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; +import java.io.Closeable; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; @@ -77,7 +79,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList private final ClassLoader parent; private final Set plugins; private Injector injector; - + public interface Factory { ScmContextListener create(ClassLoader parent, Set plugins); } @@ -183,6 +185,18 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList } super.contextDestroyed(servletContextEvent); + + for (PluginWrapper plugin : getPlugins()) { + ClassLoader pcl = plugin.getClassLoader(); + + if (pcl instanceof Closeable) { + try { + ((Closeable) pcl).close(); + } catch (IOException ex) { + LOG.warn("could not close plugin classloader", ex); + } + } + } } private void closeCloseables() { @@ -205,6 +219,4 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList private void destroyServletContextListeners(ServletContextEvent event) { injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event); } - - } diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index be7955ea28..40eb7b2496 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -112,20 +112,6 @@ public class BootstrapContextListener implements ServletContextListener { public void contextDestroyed(ServletContextEvent sce) { contextListener.contextDestroyed(sce); - if (contextListener instanceof ScmContextListener) { - for (PluginWrapper plugin : ((ScmContextListener) contextListener).getPlugins()) { - ClassLoader pcl = plugin.getClassLoader(); - - if (pcl instanceof Closeable) { - try { - ((Closeable) pcl).close(); - } catch (IOException ex) { - logger.warn("could not close plugin classloader", ex); - } - } - } - } - context = null; contextListener = null; } From ec538039f97662f43e520df8358b4f3f409e1963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:47:32 +0200 Subject: [PATCH 09/33] Cleanup --- .../scm/boot/BootstrapContextListener.java | 36 +++++++++++-------- .../scm/update/MigrationWizardModule.java | 2 ++ .../scm/update/MigrationWizardServlet.java | 18 +++++----- .../repository/XmlRepositoryV1UpdateStep.java | 2 +- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 40eb7b2496..76aad46b77 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -141,35 +141,43 @@ public class BootstrapContextListener implements ServletContextListener { } private void createContextListener(File pluginDirectory) { + ClassLoader cl; + Set plugins; + PluginLoader pluginLoader; try { renameOldPluginsFolder(pluginDirectory); + if (!isCorePluginExtractionDisabled()) { extractCorePlugins(context, pluginDirectory); } else { logger.info("core plugin extraction is disabled"); } - ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); + cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); - Set plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); + plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath()); - PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins); + pluginLoader = new DefaultPluginLoader(context, cl, plugins); - Injector bootstrapInjector = createBootstrapInjector(pluginLoader); - - MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); - - if (wizardContextListener.wizardNecessary()) { - contextListener = wizardContextListener; - } else { - processUpdates(pluginLoader, bootstrapInjector); - - contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); - } } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } + + Injector bootstrapInjector = createBootstrapInjector(pluginLoader); + + startEitherMigrationOrNormalServlet(cl, plugins, pluginLoader, bootstrapInjector); + } + + private void startEitherMigrationOrNormalServlet(ClassLoader cl, Set plugins, PluginLoader pluginLoader, Injector bootstrapInjector) { + MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector); + + if (wizardContextListener.wizardNecessary()) { + contextListener = wizardContextListener; + } else { + processUpdates(pluginLoader, bootstrapInjector); + contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins); + } } private void renameOldPluginsFolder(File pluginDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index 55b1644bb8..b2c4e842a3 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -16,6 +16,8 @@ class MigrationWizardModule extends ServletModule { LOG.info("= ="); LOG.info("= STARTING MIGRATION SERVLET ="); LOG.info("= ="); + LOG.info("= Open SCM-Manager in a browser to start the wizard. ="); + LOG.info("= ="); LOG.info("=========================================================="); bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); serve("/images/*", "/styles/*").with(WebResourceServlet.class); diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index e451e60120..53245565bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; @Singleton class MigrationWizardServlet extends HttpServlet { @@ -41,18 +40,19 @@ class MigrationWizardServlet extends HttpServlet { } public boolean wizardNecessary() { - return !repositoryV1UpdateStep.missingMigrationStrategies().isEmpty(); + return !repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies().isEmpty(); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - List missingMigrationStrategies = repositoryV1UpdateStep.missingMigrationStrategies(); + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + List repositoriesWithoutMigrationStrategies = + repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); - model.put("repositories", missingMigrationStrategies); + model.put("repositories", repositoriesWithoutMigrationStrategies); model.put("strategies", getMigrationStrategies()); MustacheFactory mf = new DefaultMustacheFactory(); @@ -60,10 +60,6 @@ class MigrationWizardServlet extends HttpServlet { respondWithTemplate(resp, model, template); } - private List getMigrationStrategies() { - return stream(MigrationStrategy.values()).map(Enum::name).collect(toList()); - } - @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); @@ -81,6 +77,10 @@ class MigrationWizardServlet extends HttpServlet { ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + private MigrationStrategy[] getMigrationStrategies() { + return MigrationStrategy.values(); + } + private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { PrintWriter writer; try { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index b7037d0e05..aeb74971a9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -111,7 +111,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ); } - public List missingMigrationStrategies() { + public List getRepositoriesWithoutMigrationStrategies() { if (!resolveV1File().exists()) { LOG.info("no v1 repositories database file found"); return emptyList(); From 1065899e9966c0c8c528876bcd33a45579703a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:52:10 +0200 Subject: [PATCH 10/33] Add missing test --- .../XmlRepositoryV1UpdateStepTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index 2102d296ea..b07f7f786c 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -221,6 +221,25 @@ class XmlRepositoryV1UpdateStepTest { assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist(); } + @Test + void shouldGetNoMissingStrategiesWithFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws IOException { + createFormerV2RepositoriesFile(tempDir); + + assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()).isEmpty(); + } + + @Test + void shouldFindMissingStrategies(@TempDirectory.TempDir Path tempDir) throws IOException { + V1RepositoryFileSystem.createV1Home(tempDir); + + assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()) + .extracting("id") + .contains( + "3b91caa5-59c3-448f-920b-769aaa56b761", + "c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f", + "454972da-faf9-4437-b682-dc4a4e0aa8eb"); + } + private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException { URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml"); Path configDir = tempDir.resolve("config"); From 748043f537c79d86c74abe44d1b2342e1df6c1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 12:52:23 +0200 Subject: [PATCH 11/33] Describe different migration strategies --- .../update/repository/MigrationStrategy.java | 23 +++++++++++++++---- .../templates/repository-migration.mustache | 14 ++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index d4b6c5c0f5..f3de48cfd9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -6,14 +6,27 @@ import java.nio.file.Path; public enum MigrationStrategy { - COPY(CopyMigrationStrategy.class), - MOVE(MoveMigrationStrategy.class), - INLINE(InlineMigrationStrategy.class); + COPY(CopyMigrationStrategy.class, + "Copy the repository data files to the new native location inside SCM-Manager home directory. " + + "This will keep the original directory."), + MOVE(MoveMigrationStrategy.class, + "Move the repository data files to the new native location inside SCM-Manager home directory. " + + "The original directory will be deleted."), + INLINE(InlineMigrationStrategy.class, + "Use the current directory where the repository data files are stored, but modify the directory " + + "structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " + + "subdirectory 'data' inside the current directory."); - private Class implementationClass; + private final Class implementationClass; + private final String description; - MigrationStrategy(Class implementationClass) { + MigrationStrategy(Class implementationClass, String description) { this.implementationClass = implementationClass; + this.description = description; + } + + public String getDescription() { + return description; } Instance from(Injector injector) { diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index bfc421cd35..067d4b3413 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -47,7 +47,7 @@
@@ -58,6 +58,18 @@ +
+
+

These are the different strategies:

+ + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+
From 815c5312204edb02faa3485ff56138146bba9fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 13:42:57 +0200 Subject: [PATCH 12/33] Use file system class to create repository directory --- .../xml/PathBasedRepositoryLocationResolver.java | 15 ++++++++------- .../PathBasedRepositoryLocationResolverTest.java | 6 +++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index 96067ba7a2..3055b34931 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -1,6 +1,7 @@ package sonia.scm.repository.xml; import sonia.scm.SCMContextProvider; +import sonia.scm.io.FileSystem; import sonia.scm.repository.BasicRepositoryLocationResolver; import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.InternalRepositoryException; @@ -8,8 +9,6 @@ import sonia.scm.store.StoreConstants; import javax.inject.Inject; import javax.inject.Singleton; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Map; @@ -36,6 +35,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation private final SCMContextProvider contextProvider; private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; + private final FileSystem fileSystem; private final PathDatabase pathDatabase; private final Map pathById; @@ -46,14 +46,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation private Long lastModified; @Inject - public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) { - this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC()); + public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) { + this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC()); } - public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { + PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, Clock clock) { super(Path.class); this.contextProvider = contextProvider; this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; + this.fileSystem = fileSystem; this.pathById = new ConcurrentHashMap<>(); this.clock = clock; @@ -101,8 +102,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation setLocation(repositoryId, path); Path resolvedPath = contextProvider.resolve(path); try { - Files.createDirectories(resolvedPath); - } catch (IOException e) { + fileSystem.create(resolvedPath.toFile()); + } catch (Exception e) { throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e); } return resolvedPath; diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 754b8469d5..941775d6ea 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -11,6 +11,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.io.FileSystem; import sonia.scm.repository.InitialRepositoryLocationResolver; import java.io.IOException; @@ -41,6 +43,8 @@ class PathBasedRepositoryLocationResolverTest { @Mock private Clock clock; + private final FileSystem fileSystem = new DefaultFileSystem(); + private Path basePath; private PathBasedRepositoryLocationResolver resolver; @@ -159,7 +163,7 @@ class PathBasedRepositoryLocationResolverTest { } private PathBasedRepositoryLocationResolver createResolver() { - return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock); + return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock); } private String content(Path storePath) { From 47413de44a79001eb4d09770fe0b14998a359a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 6 Jun 2019 14:50:01 +0200 Subject: [PATCH 13/33] Try to load SCM-Manager after migration --- .../repository-migration-restart.mustache | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 2374861fe2..1ff8a1baa6 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -24,9 +24,69 @@

SCM-Manager will restart to migrate the data.

+
+

+ + + + + + + + + + + + +

+
+ From 20acd4ca646febb595b3292f0c6246c7abb7785b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:02:17 +0200 Subject: [PATCH 14/33] Restructure migration page --- .../templates/repository-migration.mustache | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 067d4b3413..26b1650e68 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -21,8 +21,26 @@
-

SCM-Manager Migration

-

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+

+

SCM-Manager Migration

+

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+

+ To migrate the existing repositories you have to specify a namespace and a name for each on them + as well as a migration strategy. +

+

+ The strategies are the following: + + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+

+
+
@@ -34,7 +52,7 @@ {{#repositories}}
- {{name}} + {{type}}/{{name}} {{type}} @@ -58,18 +76,6 @@
-
-
-

These are the different strategies:

- - {{#strategies}} - - - - - {{/strategies}} -
{{name}}{{description}}
-
From 8a6b57e06c998a81000fe23c6cce8d30b351a800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:52:54 +0200 Subject: [PATCH 15/33] Add new migration strategies "delete" and "ignore" --- .../repository/CopyMigrationStrategy.java | 7 ++-- .../repository/DeleteMigrationStrategy.java | 32 +++++++++++++++++++ .../repository/IgnoreMigrationStrategy.java | 13 ++++++++ .../repository/InlineMigrationStrategy.java | 7 ++-- .../update/repository/MigrationStrategy.java | 15 +++++++-- .../repository/MoveMigrationStrategy.java | 6 ++-- .../repository/XmlRepositoryV1UpdateStep.java | 30 +++++++++-------- .../repository/CopyMigrationStrategyTest.java | 4 +-- .../InlineMigrationStrategyTest.java | 2 +- .../repository/MigrationStrategyMock.java | 10 ++++++ .../repository/MoveMigrationStrategyTest.java | 4 +-- .../XmlRepositoryV1UpdateStepTest.java | 18 ++++++++++- 12 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java index f96413d195..b66b2ea9c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/CopyMigrationStrategy.java @@ -9,6 +9,9 @@ import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.of; class CopyMigrationStrategy extends BaseMigrationStrategy { @@ -23,14 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); Path sourceDataPath = getSourceDataPath(name, type); LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath); copyData(sourceDataPath, targetDataPath); - return repositoryBasePath; + return of(repositoryBasePath); } private void copyData(Path sourceDirectory, Path targetDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java new file mode 100644 index 0000000000..cf61152613 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/DeleteMigrationStrategy.java @@ -0,0 +1,32 @@ +package sonia.scm.update.repository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +public class DeleteMigrationStrategy extends BaseMigrationStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(DeleteMigrationStrategy.class); + + @Inject + DeleteMigrationStrategy(SCMContextProvider contextProvider) { + super(contextProvider); + } + + @Override + public Optional migrate(String id, String name, String type) { + Path sourceDataPath = getSourceDataPath(name, type); + try { + IOUtil.delete(sourceDataPath.toFile(), true); + } catch (IOException e) { + LOG.warn("could not delete old repository path for repository {} with type {} and id {}", name, type, id); + } + return Optional.empty(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java new file mode 100644 index 0000000000..945538009a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/IgnoreMigrationStrategy.java @@ -0,0 +1,13 @@ +package sonia.scm.update.repository; + +import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.empty; + +public class IgnoreMigrationStrategy implements MigrationStrategy.Instance { + @Override + public Optional migrate(String id, String name, String type) { + return empty(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java index 60f03666a5..a75ff5e571 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/InlineMigrationStrategy.java @@ -10,6 +10,9 @@ import javax.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; + +import static java.util.Optional.of; class InlineMigrationStrategy extends BaseMigrationStrategy { @@ -24,14 +27,14 @@ class InlineMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = getSourceDataPath(name, type); locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath); moveData(repositoryBasePath, targetDataPath); - return repositoryBasePath; + return of(repositoryBasePath); } private void moveData(Path sourceDirectory, Path targetDirectory) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java index f3de48cfd9..46604d327e 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategy.java @@ -3,6 +3,7 @@ package sonia.scm.update.repository; import com.google.inject.Injector; import java.nio.file.Path; +import java.util.Optional; public enum MigrationStrategy { @@ -15,7 +16,13 @@ public enum MigrationStrategy { INLINE(InlineMigrationStrategy.class, "Use the current directory where the repository data files are stored, but modify the directory " + "structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " + - "subdirectory 'data' inside the current directory."); + "subdirectory 'data' inside the current directory."), + IGNORE(IgnoreMigrationStrategy.class, + "The repository will not be migrated and will not be visible inside SCM-Manager. " + + "The data files will be kept at the current location."), + DELETE(DeleteMigrationStrategy.class, + "The repository will not be migrated and will not be visible inside SCM-Manager. " + + "The data files will be deleted!"); private final Class implementationClass; private final String description; @@ -25,6 +32,10 @@ public enum MigrationStrategy { this.description = description; } + public Class getImplementationClass() { + return implementationClass; + } + public String getDescription() { return description; } @@ -34,6 +45,6 @@ public enum MigrationStrategy { } interface Instance { - Path migrate(String id, String name, String type); + Optional migrate(String id, String name, String type); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java index deb8a8782b..5b364d8bc3 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MoveMigrationStrategy.java @@ -11,8 +11,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import static java.util.Arrays.asList; +import static java.util.Optional.of; class MoveMigrationStrategy extends BaseMigrationStrategy { @@ -27,7 +29,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { } @Override - public Path migrate(String id, String name, String type) { + public Optional migrate(String id, String name, String type) { Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id); Path targetDataPath = repositoryBasePath .resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY); @@ -35,7 +37,7 @@ class MoveMigrationStrategy extends BaseMigrationStrategy { LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath); moveData(sourceDataPath, targetDataPath); deleteOldDataDir(getTypeDependentPath(type), name); - return repositoryBasePath; + return of(repositoryBasePath); } private void deleteOldDataDir(Path rootPath, String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index aeb74971a9..0d40a1b413 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -141,21 +141,25 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private void update(V1Repository v1Repository) { - Path destination = handleDataDirectory(v1Repository); - Repository repository = new Repository( - v1Repository.id, - v1Repository.type, - v1Repository.getNewNamespace(), - v1Repository.getNewName(), - v1Repository.contact, - v1Repository.description, - createPermissions(v1Repository)); - LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination); - repositoryDao.add(repository, destination); - propertyStore.put(v1Repository.id, v1Repository.properties); + Optional destination = handleDataDirectory(v1Repository); + destination.ifPresent( + newPath -> { + Repository repository = new Repository( + v1Repository.id, + v1Repository.type, + v1Repository.getNewNamespace(), + v1Repository.getNewName(), + v1Repository.contact, + v1Repository.description, + createPermissions(v1Repository)); + LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, newPath); + repositoryDao.add(repository, newPath); + propertyStore.put(v1Repository.id, v1Repository.properties); + } + ); } - private Path handleDataDirectory(V1Repository v1Repository) { + private Optional handleDataDirectory(V1Repository v1Repository) { MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java index d718554dfe..d3f18a1ff7 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java @@ -48,13 +48,13 @@ class CopyMigrationStrategyTest { @Test void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f")); } @Test void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target.resolve("data")).exists(); Path originalDataDir = tempDir .resolve("repositories") diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java index fa237e78b0..2b97c1d79b 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java @@ -41,7 +41,7 @@ class InlineMigrationStrategyTest { @Test void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(resolveOldDirectory(tempDir)); verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target); } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java index e0018f584f..c04c9477bb 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java @@ -3,10 +3,13 @@ package sonia.scm.update.repository; import com.google.inject.Injector; import sonia.scm.update.repository.MigrationStrategy.Instance; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; +import static java.util.Optional.of; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -20,6 +23,13 @@ class MigrationStrategyMock { .thenAnswer( invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class) key)) ); + + for (MigrationStrategy strategy : MigrationStrategy.values()) { + MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass()); + when(strategyMock.migrate(any(), any(), any())).thenReturn(of(Paths.get(""))); + lenient().when(mock.getInstance((Class) strategy.getImplementationClass())).thenReturn(strategyMock); + } + return mock; } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java index e248f82217..fa58eb8ea1 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java @@ -45,13 +45,13 @@ class MoveMigrationStrategyTest { @Test void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f")); } @Test void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) { - Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get(); assertThat(target.resolve("data")).exists(); Path originalDataDir = tempDir .resolve("repositories") diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index b07f7f786c..76a74993b3 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import static java.util.Optional.empty; @@ -33,10 +34,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static sonia.scm.update.repository.MigrationStrategy.COPY; +import static sonia.scm.update.repository.MigrationStrategy.DELETE; import static sonia.scm.update.repository.MigrationStrategy.INLINE; import static sonia.scm.update.repository.MigrationStrategy.MOVE; @@ -177,13 +180,26 @@ class XmlRepositoryV1UpdateStepTest { void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException { Path targetDir = tempDir.resolve("someDir"); MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class); - when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir); + when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir)); updateStep.doUpdate(); assertThat(locationCaptor.getAllValues()).contains(targetDir); } + @Test + void shouldSkipWhenStrategyGivesNoNewPath() throws JAXBException { + for (MigrationStrategy strategy : MigrationStrategy.values()) { + MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass()); + lenient().when(strategyMock.migrate(any(), any(), any())).thenReturn(empty()); + lenient().when(injectorMock.getInstance((Class) strategy.getImplementationClass())).thenReturn(strategyMock); + } + + updateStep.doUpdate(); + + assertThat(locationCaptor.getAllValues()).isEmpty(); + } + @Test void shouldFailForMissingMigrationStrategy() { lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty()); From e5809a6350e158a432215881de548e6a32be9cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:59:31 +0200 Subject: [PATCH 16/33] Sort repositories by type and name --- .../main/java/sonia/scm/update/MigrationWizardServlet.java | 5 ++++- .../scm/update/repository/XmlRepositoryV1UpdateStep.java | 4 ++++ .../main/resources/templates/repository-migration.mustache | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 53245565bd..9db24dc8eb 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -18,7 +18,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,7 +48,8 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { List repositoriesWithoutMigrationStrategies = - repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); + new ArrayList<>(repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies()); + repositoriesWithoutMigrationStrategies.sort(Comparator.comparing(XmlRepositoryV1UpdateStep.V1Repository::getPath)); HashMap model = new HashMap<>(); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 0d40a1b413..54d0d7c52f 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -240,6 +240,10 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return type; } + public String getPath() { + return type + "/" + name; + } + public String getNewNamespace() { String[] nameParts = getNameParts(name); return nameParts.length > 1 ? nameParts[0] : type; diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 26b1650e68..83d2fe94dd 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -52,7 +52,7 @@ {{#repositories}} - {{type}}/{{name}} + {{path}} {{type}} From 70de4d729260ec2d6bbc2c5a4224d602eb1859f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 13:59:50 +0200 Subject: [PATCH 17/33] Organize imports --- .../src/main/java/sonia/scm/update/MigrationWizardServlet.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 9db24dc8eb..6a11896008 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -25,8 +25,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static java.util.Arrays.stream; - @Singleton class MigrationWizardServlet extends HttpServlet { From df9a3c12dd86071293418a02de4c90008fcacb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 7 Jun 2019 14:16:23 +0200 Subject: [PATCH 18/33] First step to make name and namespace editable in migration --- .../scm/update/MigrationWizardServlet.java | 10 +++++++-- .../repository/MigrationStrategyDao.java | 4 ++-- .../repository/RepositoryMigrationPlan.java | 22 ++++++++++++++++++- .../templates/repository-migration.mustache | 13 ++++++++--- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 6a11896008..66c1006d4f 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -65,8 +66,13 @@ class MigrationWizardServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) { resp.setStatus(200); - req.getParameterMap().forEach( - (name, strategy) -> migrationStrategyDao.set(name, MigrationStrategy.valueOf(strategy[0])) + Arrays.stream(req.getParameterValues("ids")).forEach( + id -> { + String strategy = req.getParameter("strategy-" + id); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + } ); MustacheFactory mf = new DefaultMustacheFactory(); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 8ddb5e02df..07c049bc84 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -23,8 +23,8 @@ public class MigrationStrategyDao { return plan.get(id); } - public void set(String repositoryId, MigrationStrategy strategy) { - plan.set(repositoryId, strategy); + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { + plan.set(repositoryId, strategy, newNamespace, newName); store.set(plan); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index f2b2dc9788..c839eba5b0 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -28,10 +28,12 @@ class RepositoryMigrationPlan { .map(RepositoryEntry::getDataMigrationStrategy); } - public void set(String repositoryId, MigrationStrategy strategy) { + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { Optional entry = findEntry(repositoryId); if (entry.isPresent()) { entry.get().setStrategy(strategy); + entry.get().setNewNamespace(newNamespace); + entry.get().setNewName(newName); } else { entries.add(new RepositoryEntry(repositoryId, strategy)); } @@ -49,6 +51,8 @@ class RepositoryMigrationPlan { private String repositoryId; private MigrationStrategy dataMigrationStrategy; + private String newNamespace; + private String newName; RepositoryEntry() { } @@ -62,8 +66,24 @@ class RepositoryMigrationPlan { return dataMigrationStrategy; } + public String getNewNamespace() { + return newNamespace; + } + + public String getNewName() { + return newName; + } + private void setStrategy(MigrationStrategy strategy) { this.dataMigrationStrategy = strategy; } + + private void setNewNamespace(String newNamespace) { + this.newNamespace = newNamespace; + } + + private void setNewName(String newName) { + this.newName = newName; + } } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 83d2fe94dd..5a9085fb4b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -46,7 +46,8 @@ Original name Type - New namespace and name + New namespace + New name Strategy {{#repositories}} @@ -58,12 +59,15 @@ {{type}} - {{newNamespace}}/{{newName}} + + + +
- {{#strategies}} {{/strategies}} @@ -74,6 +78,9 @@ {{/repositories}} + {{#repositories}} + + {{/repositories}}
From fdb8143b766c2b42482576c82778c63104af40af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 10:27:21 +0200 Subject: [PATCH 19/33] Validate new namespace and name on migration --- .../sonia/scm/util/ValidationUtilTest.java | 7 +- .../scm/update/MigrationWizardServlet.java | 133 ++++++++++++++++-- .../templates/repository-migration.mustache | 23 +-- 3 files changed, 139 insertions(+), 24 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java index 972ed95a2d..51ff906b8f 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -147,6 +147,10 @@ public class ValidationUtilTest public void testIsRepositoryNameValid() { String[] validPaths = { "scm", + "scm-", + "scm_", + "s_cm", + "s-cm", "s", "sc", ".hiddenrepo", @@ -206,7 +210,8 @@ public class ValidationUtilTest "a/..b", "scm/main", "scm/plugins/git-plugin", - "scm/plugins/git-plugin" + "_scm", + "-scm" }; for (String path : validPaths) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 66c1006d4f..cd5f640c7b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -10,6 +10,7 @@ import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.MigrationStrategyDao; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; +import sonia.scm.util.ValidationUtil; import javax.inject.Inject; import javax.inject.Singleton; @@ -18,13 +19,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; @Singleton class MigrationWizardServlet extends HttpServlet { @@ -46,16 +47,20 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - List repositoriesWithoutMigrationStrategies = - new ArrayList<>(repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies()); - repositoriesWithoutMigrationStrategies.sort(Comparator.comparing(XmlRepositoryV1UpdateStep.V1Repository::getPath)); + List repositoryLineEntries = getRepositoryLineEntries(); + doGet(req, resp, repositoryLineEntries); + } + protected void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); model.put("submitUrl", req.getRequestURI()); - model.put("repositories", repositoriesWithoutMigrationStrategies); + model.put("repositories", repositoryLineEntries); model.put("strategies", getMigrationStrategies()); + model.put("validationErrorsFound", repositoryLineEntries + .stream() + .anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid())); MustacheFactory mf = new DefaultMustacheFactory(); Mustache template = mf.compile("templates/repository-migration.mustache"); @@ -64,16 +69,41 @@ class MigrationWizardServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - resp.setStatus(200); + List repositoryLineEntries = getRepositoryLineEntries(); - Arrays.stream(req.getParameterValues("ids")).forEach( - id -> { - String strategy = req.getParameter("strategy-" + id); - String namespace = req.getParameter("namespace-" + id); - String name = req.getParameter("name-" + id); - migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + boolean validationErrorFound = false; + for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) { + String id = repositoryLineEntry.getId(); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + repositoryLineEntry.setNamespace(namespace); + repositoryLineEntry.setName(name); + + if (!ValidationUtil.isRepositoryNameValid(namespace)) { + repositoryLineEntry.setNamespaceValid(false); + validationErrorFound = true; } - ); + if (!ValidationUtil.isRepositoryNameValid(name)) { + repositoryLineEntry.setNameValid(false); + validationErrorFound = true; + } + } + + if (validationErrorFound) { + doGet(req, resp, repositoryLineEntries); + return; + } + + repositoryLineEntries.stream() + .map(RepositoryLineEntry::getId) + .forEach( + id -> { + String strategy = req.getParameter("strategy-" + id); + String namespace = req.getParameter("namespace-" + id); + String name = req.getParameter("name-" + id); + migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name); + } + ); MustacheFactory mf = new DefaultMustacheFactory(); Mustache template = mf.compile("templates/repository-migration-restart.mustache"); @@ -81,9 +111,20 @@ class MigrationWizardServlet extends HttpServlet { respondWithTemplate(resp, model, template); + resp.setStatus(200); + ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } + private List getRepositoryLineEntries() { + List repositoriesWithoutMigrationStrategies = + repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); + return repositoriesWithoutMigrationStrategies.stream() + .map(RepositoryLineEntry::new) + .sorted(comparing(RepositoryLineEntry::getPath)) + .collect(Collectors.toList()); + } + private MigrationStrategy[] getMigrationStrategies() { return MigrationStrategy.values(); } @@ -101,4 +142,66 @@ class MigrationWizardServlet extends HttpServlet { writer.flush(); resp.setStatus(200); } + + private static class RepositoryLineEntry { + private final String id; + private final String type; + private final String path; + private String namespace; + private String name; + private boolean namespaceValid = true; + private boolean nameValid = true; + + public RepositoryLineEntry(XmlRepositoryV1UpdateStep.V1Repository repository) { + this.id = repository.getId(); + this.type = repository.getType(); + this.path = repository.getPath(); + this.namespace = repository.getNewNamespace(); + this.name = repository.getNewName(); + } + + public String getId() { + return id; + } + + public String getType() { + return type; + } + + public String getPath() { + return path; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public void setName(String name) { + this.name = name; + } + + public void setNamespaceValid(boolean namespaceValid) { + this.namespaceValid = namespaceValid; + } + + public void setNameValid(boolean nameValid) { + this.nameValid = nameValid; + } + + public boolean isNamespaceInvalid() { + return !namespaceValid; + } + + public boolean isNameInvalid() { + return !nameValid; + } + } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 5a9085fb4b..9d7e5e0a0b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -41,14 +41,24 @@


+ {{#validationErrorsFound}} +
Please correct the invalid namespaces or names below and try again.
+
+ {{/validationErrorsFound}}
- - - + + + {{#repositories}} @@ -59,10 +69,10 @@ {{type}} {{/repositories}}
Original name TypeNew namespaceNew nameStrategyNew namespace + + New name + + Strategy + +
- + - +
@@ -78,9 +88,6 @@
- {{#repositories}} - - {{/repositories}}
From 802fb3e0cf0b4ca3a490d11f6e44278a55de2b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:10:31 +0200 Subject: [PATCH 20/33] Use manually entered namespace and name --- .../scm/update/MigrationWizardServlet.java | 48 +++-- .../repository/MigrationStrategyDao.java | 2 +- .../repository/RepositoryMigrationPlan.java | 43 ++-- .../scm/update/repository/V1Permission.java | 25 +++ .../scm/update/repository/V1Repository.java | 92 +++++++++ .../repository/XmlRepositoryV1UpdateStep.java | 126 +++--------- .../update/MigrationWizardServletTest.java | 188 ++++++++++++++++++ .../repository/MigrationStrategyDaoTest.java | 38 ++-- .../XmlRepositoryV1UpdateStepTest.java | 60 ++---- 9 files changed, 427 insertions(+), 195 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java create mode 100644 scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index cd5f640c7b..f26b1afac5 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -3,12 +3,14 @@ package sonia.scm.update; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; import sonia.scm.util.ValidationUtil; @@ -19,6 +21,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -62,9 +65,7 @@ class MigrationWizardServlet extends HttpServlet { .stream() .anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid())); - MustacheFactory mf = new DefaultMustacheFactory(); - Mustache template = mf.compile("templates/repository-migration.mustache"); - respondWithTemplate(resp, model, template); + respondWithTemplate(resp, model, "templates/repository-migration.mustache"); } @Override @@ -76,6 +77,7 @@ class MigrationWizardServlet extends HttpServlet { String id = repositoryLineEntry.getId(); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); + String strategy = req.getParameter("strategy-" + id); repositoryLineEntry.setNamespace(namespace); repositoryLineEntry.setName(name); @@ -105,19 +107,15 @@ class MigrationWizardServlet extends HttpServlet { } ); - MustacheFactory mf = new DefaultMustacheFactory(); - Mustache template = mf.compile("templates/repository-migration-restart.mustache"); Map model = Collections.singletonMap("contextPath", req.getContextPath()); - respondWithTemplate(resp, model, template); - - resp.setStatus(200); + respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache"); ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); } private List getRepositoryLineEntries() { - List repositoriesWithoutMigrationStrategies = + List repositoriesWithoutMigrationStrategies = repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies(); return repositoriesWithoutMigrationStrategies.stream() .map(RepositoryLineEntry::new) @@ -129,7 +127,11 @@ class MigrationWizardServlet extends HttpServlet { return MigrationStrategy.values(); } - private void respondWithTemplate(HttpServletResponse resp, Map model, Mustache template) { + @VisibleForTesting + void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) { + MustacheFactory mf = new DefaultMustacheFactory(); + Mustache template = mf.compile(templateName); + PrintWriter writer; try { writer = resp.getWriter(); @@ -152,12 +154,30 @@ class MigrationWizardServlet extends HttpServlet { private boolean namespaceValid = true; private boolean nameValid = true; - public RepositoryLineEntry(XmlRepositoryV1UpdateStep.V1Repository repository) { + public RepositoryLineEntry(V1Repository repository) { this.id = repository.getId(); this.type = repository.getType(); - this.path = repository.getPath(); - this.namespace = repository.getNewNamespace(); - this.name = repository.getNewName(); + this.path = repository.getType() + "/" + repository.getName(); + this.namespace = computeNewNamespace(repository); + this.name = computeNewName(repository); + } + + private static String computeNewNamespace(V1Repository v1Repository) { + String[] nameParts = getNameParts(v1Repository.getName()); + return nameParts.length > 1 ? nameParts[0] : v1Repository.getType(); + } + + private static String computeNewName(V1Repository v1Repository) { + String[] nameParts = getNameParts(v1Repository.getName()); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); + } + + private static String[] getNameParts(String v1Name) { + return v1Name.split("/"); + } + + private static String concatPathElements(String[] nameParts) { + return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); } public String getId() { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java index 07c049bc84..5670ce458a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java @@ -19,7 +19,7 @@ public class MigrationStrategyDao { this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); } - public Optional get(String id) { + public Optional get(String id) { return plan.get(id); } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index c839eba5b0..3c4bf36342 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -13,53 +13,50 @@ import static java.util.Arrays.asList; @XmlRootElement(name = "repository-migration") class RepositoryMigrationPlan { - private List entries; + private List entries; RepositoryMigrationPlan() { - this(new RepositoryEntry[0]); + this(new RepositoryMigrationEntry[0]); } - RepositoryMigrationPlan(RepositoryEntry... entries) { + RepositoryMigrationPlan(RepositoryMigrationEntry... entries) { this.entries = new ArrayList<>(asList(entries)); } - Optional get(String repositoryId) { - return findEntry(repositoryId) - .map(RepositoryEntry::getDataMigrationStrategy); - } - - public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { - Optional entry = findEntry(repositoryId); - if (entry.isPresent()) { - entry.get().setStrategy(strategy); - entry.get().setNewNamespace(newNamespace); - entry.get().setNewName(newName); - } else { - entries.add(new RepositoryEntry(repositoryId, strategy)); - } - } - - private Optional findEntry(String repositoryId) { + Optional get(String repositoryId) { return entries.stream() .filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId)) .findFirst(); } + public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) { + Optional entry = get(repositoryId); + if (entry.isPresent()) { + entry.get().setStrategy(strategy); + entry.get().setNewNamespace(newNamespace); + entry.get().setNewName(newName); + } else { + entries.add(new RepositoryMigrationEntry(repositoryId, strategy, newNamespace, newName)); + } + } + @XmlRootElement(name = "entries") @XmlAccessorType(XmlAccessType.FIELD) - static class RepositoryEntry { + static class RepositoryMigrationEntry { private String repositoryId; private MigrationStrategy dataMigrationStrategy; private String newNamespace; private String newName; - RepositoryEntry() { + RepositoryMigrationEntry() { } - RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) { + RepositoryMigrationEntry(String repositoryId, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { this.repositoryId = repositoryId; this.dataMigrationStrategy = dataMigrationStrategy; + this.newNamespace = newNamespace; + this.newName = newName; } public MigrationStrategy getDataMigrationStrategy() { diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java new file mode 100644 index 0000000000..97426f1a83 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Permission.java @@ -0,0 +1,25 @@ +package sonia.scm.update.repository; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "permissions") +class V1Permission { + private boolean groupPermission; + private String name; + private String type; + + public boolean isGroupPermission() { + return groupPermission; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java new file mode 100644 index 0000000000..256e2ad397 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/V1Repository.java @@ -0,0 +1,92 @@ +package sonia.scm.update.repository; + +import sonia.scm.update.properties.V1Properties; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "repositories") +public class V1Repository { + private String contact; + private long creationDate; + private Long lastModified; + private String description; + private String id; + private String name; + private boolean isPublic; + private boolean archived; + private String type; + private List permissions; + private V1Properties properties; + + public V1Repository() { + } + + public V1Repository(String id, String type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getContact() { + return contact; + } + + public long getCreationDate() { + return creationDate; + } + + public Long getLastModified() { + return lastModified; + } + + public String getDescription() { + return description; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean isArchived() { + return archived; + } + + public List getPermissions() { + return permissions; + } + + public V1Properties getProperties() { + return properties; + } + + @Override + public String toString() { + return "V1Repository{" + + ", contact='" + contact + '\'' + + ", creationDate=" + creationDate + + ", lastModified=" + lastModified + + ", description='" + description + '\'' + + ", id='" + id + '\'' + + ", name='" + name + '\'' + + ", isPublic=" + isPublic + + ", archived=" + archived + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 54d0d7c52f..2f16dc74f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -28,7 +28,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -104,7 +103,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); readV1Database(jaxbContext).ifPresent( v1Database -> { - v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy); + v1Database.repositoryList.repositories.forEach(this::readMigrationEntry); v1Database.repositoryList.repositories.forEach(this::update); backupOldRepositoriesFile(); } @@ -141,52 +140,56 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private void update(V1Repository v1Repository) { - Optional destination = handleDataDirectory(v1Repository); + RepositoryMigrationPlan.RepositoryMigrationEntry repositoryMigrationEntry = readMigrationEntry(v1Repository); + Optional destination = handleDataDirectory(v1Repository, repositoryMigrationEntry.getDataMigrationStrategy()); + LOG.info("using strategy {} to migrate repository {} with id {} using new namespace {} and name {}", + repositoryMigrationEntry.getDataMigrationStrategy().getClass(), + v1Repository.getName(), + v1Repository.getId(), + repositoryMigrationEntry.getNewNamespace(), + repositoryMigrationEntry.getNewName()); destination.ifPresent( newPath -> { Repository repository = new Repository( - v1Repository.id, - v1Repository.type, - v1Repository.getNewNamespace(), - v1Repository.getNewName(), - v1Repository.contact, - v1Repository.description, + v1Repository.getId(), + v1Repository.getType(), + repositoryMigrationEntry.getNewNamespace(), + repositoryMigrationEntry.getNewName(), + v1Repository.getContact(), + v1Repository.getDescription(), createPermissions(v1Repository)); - LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, newPath); + LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.getName(), newPath); repositoryDao.add(repository, newPath); - propertyStore.put(v1Repository.id, v1Repository.properties); + propertyStore.put(v1Repository.getId(), v1Repository.getProperties()); } ); } - private Optional handleDataDirectory(V1Repository v1Repository) { - MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository); - LOG.info("using strategy {} to migrate repository {} with id {}", dataMigrationStrategy.getClass(), v1Repository.name, v1Repository.id); - return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); + private Optional handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) { + return dataMigrationStrategy + .from(injector) + .migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType()); } - private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) { + private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) { return findMigrationStrategy(v1Repository) - .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); + .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.getId() + " and name " + v1Repository.getName())); } - private Optional findMigrationStrategy(V1Repository v1Repository) { - return migrationStrategyDao.get(v1Repository.id); + private Optional findMigrationStrategy(V1Repository v1Repository) { + return migrationStrategyDao.get(v1Repository.getId()); } private RepositoryPermission[] createPermissions(V1Repository v1Repository) { - if (v1Repository.permissions == null) { - return new RepositoryPermission[0]; - } - return v1Repository.permissions + return v1Repository.getPermissions() .stream() .map(this::createPermission) .toArray(RepositoryPermission[]::new); } private RepositoryPermission createPermission(V1Permission v1Permission) { - LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name); - return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission); + LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName()); + return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission()); } private Optional readV1Database(JAXBContext jaxbContext) throws JAXBException { @@ -205,79 +208,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { ).toFile(); } - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "permissions") - private static class V1Permission { - private boolean groupPermission; - private String name; - private String type; - } - - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "repositories") - public static class V1Repository { - private String contact; - private long creationDate; - private Long lastModified; - private String description; - private String id; - private String name; - private boolean isPublic; - private boolean archived; - private String type; - private List permissions; - private V1Properties properties; - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getType() { - return type; - } - - public String getPath() { - return type + "/" + name; - } - - public String getNewNamespace() { - String[] nameParts = getNameParts(name); - return nameParts.length > 1 ? nameParts[0] : type; - } - - public String getNewName() { - String[] nameParts = getNameParts(name); - return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); - } - - private String[] getNameParts(String v1Name) { - return v1Name.split("/"); - } - - private String concatPathElements(String[] nameParts) { - return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_")); - } - - @Override - public String toString() { - return "V1Repository{" + - ", contact='" + contact + '\'' + - ", creationDate=" + creationDate + - ", lastModified=" + lastModified + - ", description='" + description + '\'' + - ", id='" + id + '\'' + - ", name='" + name + '\'' + - ", isPublic=" + isPublic + - ", archived=" + archived + - ", type='" + type + '\'' + - '}'; - } - } - private static class RepositoryList { @XmlElement(name = "repository") private List repositories; diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java new file mode 100644 index 0000000000..e40179eb7f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -0,0 +1,188 @@ +package sonia.scm.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.update.repository.MigrationStrategy; +import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.V1Repository; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MigrationWizardServletTest { + + @Mock + XmlRepositoryV1UpdateStep updateStep; + @Mock + MigrationStrategyDao migrationStrategyDao; + + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + + String renderedTemplateName; + Map renderedModel; + + MigrationWizardServlet servlet; + + @BeforeEach + void initServlet() { + servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) { + @Override + void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) { + renderedTemplateName = templateName; + renderedModel = model; + } + }; + } + + @Test + void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "simple")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("git"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("simple"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "two/dirs")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("two"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("dirs"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "more/than/two/dirs")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespace") + .contains("more"); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("name") + .contains("than_two_dirs"); + } + + @Test + void shouldUseTypeAndNameAsPath() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("path") + .contains("git/name"); + } + + @Test + void shouldKeepId() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("id") + .contains("id"); + } + + @Test + void shouldNotBeInvalidAtFirstRequest() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + servlet.doGet(request, response); + + assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(false); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespaceInvalid") + .contains(false); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("nameInvalid") + .contains(false); + } + + @Test + void shouldValidateNamespaceAndNameOnPost() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + doReturn("invalid namespace").when(request).getParameter("namespace-id"); + doReturn("invalid name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(true); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("namespaceInvalid") + .contains(true); + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("nameInvalid") + .contains(true); + } + + @Test + void shouldStoreValidMigration() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + doReturn("namespace").when(request).getParameter("namespace-id"); + doReturn("name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + verify(migrationStrategyDao).set("id", MigrationStrategy.COPY, "namespace", "name"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java index e3fd4457b6..5829f5b98f 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java @@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; import javax.xml.bind.JAXBException; import java.nio.file.Path; @@ -37,23 +35,31 @@ class MigrationStrategyDaoTest { } @Test - void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException { + void shouldReturnEmptyOptionalWhenStoreIsEmpty() { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - Optional strategy = dao.get("any"); + Optional entry = dao.get("any"); - Assertions.assertThat(strategy).isEmpty(); + Assertions.assertThat(entry).isEmpty(); } @Test - void shouldReturnNewValue() throws JAXBException { + void shouldReturnNewValue() { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - dao.set("id", INLINE); + dao.set("id", INLINE, "space", "name"); - Optional strategy = dao.get("id"); + Optional entry = dao.get("id"); - Assertions.assertThat(strategy).contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy) + .contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace) + .contains("space"); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName) + .contains("name"); } @Nested @@ -62,16 +68,24 @@ class MigrationStrategyDaoTest { void initExistingDatabase() throws JAXBException { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - dao.set("id", INLINE); + dao.set("id", INLINE, "space", "name"); } @Test void shouldFindExistingValue() throws JAXBException { MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); - Optional strategy = dao.get("id"); + Optional entry = dao.get("id"); - Assertions.assertThat(strategy).contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy) + .contains(INLINE); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace) + .contains("space"); + Assertions.assertThat(entry) + .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName) + .contains("name"); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index 76a74993b3..6c977a3cb6 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -11,6 +11,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.xml.XmlRepositoryDAO; @@ -25,7 +26,6 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Optional; import static java.util.Optional.empty; @@ -38,9 +38,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static sonia.scm.update.repository.MigrationStrategy.COPY; -import static sonia.scm.update.repository.MigrationStrategy.DELETE; -import static sonia.scm.update.repository.MigrationStrategy.INLINE; import static sonia.scm.update.repository.MigrationStrategy.MOVE; @ExtendWith(MockitoExtension.class) @@ -92,9 +89,14 @@ class XmlRepositoryV1UpdateStepTest { @BeforeEach void createMigrationPlan() { - lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE)); - lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY)); - lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE)); + Answer planAnswer = invocation -> { + String id = invocation.getArgument(0).toString(); + return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, MOVE, "namespace-" + id, "name-" + id)); + }; + + lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenAnswer(planAnswer); + lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenAnswer(planAnswer); + lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenAnswer(planAnswer); } @Test @@ -107,56 +109,20 @@ class XmlRepositoryV1UpdateStepTest { void shouldMapAttributes() throws JAXBException { updateStep.doUpdate(); - Optional repository = findByNamespace("git"); + Optional repository = findByNamespace("namespace-3b91caa5-59c3-448f-920b-769aaa56b761"); assertThat(repository) .get() .hasFieldOrPropertyWithValue("type", "git") .hasFieldOrPropertyWithValue("contact", "arthur@dent.uk") - .hasFieldOrPropertyWithValue("description", "A simple repository without directories."); - } - - @Test - void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("git"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "git") - .hasFieldOrPropertyWithValue("name", "simple"); - } - - @Test - void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("one"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "one") - .hasFieldOrPropertyWithValue("name", "directory"); - } - - @Test - void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("some"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "some") - .hasFieldOrPropertyWithValue("name", "more_directories_than_one"); + .hasFieldOrPropertyWithValue("description", "A repository with two folders."); } @Test void shouldMapPermissions() throws JAXBException { updateStep.doUpdate(); - Optional repository = findByNamespace("git"); + Optional repository = findByNamespace("namespace-454972da-faf9-4437-b682-dc4a4e0aa8eb"); assertThat(repository.get().getPermissions()) .hasSize(3) @@ -179,7 +145,7 @@ class XmlRepositoryV1UpdateStepTest { @Test void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException { Path targetDir = tempDir.resolve("someDir"); - MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class); + MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class); when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir)); updateStep.doUpdate(); From d9fc1f9aee63ad6b4bc6d1ebce1f7d8f260971a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:27:12 +0200 Subject: [PATCH 21/33] Add "change all" button for strategies --- .../templates/repository-migration.mustache | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 9d7e5e0a0b..11d853a2b8 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -58,6 +58,16 @@ Strategy +
Change all: +
+
+ +
+
{{#repositories}} @@ -77,7 +87,7 @@
- {{#strategies}} {{/strategies}} @@ -96,4 +106,15 @@
+ From 00a2be2245338d67a188a579f22e75028eb3e881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:33:34 +0200 Subject: [PATCH 22/33] Check for empty (that is: null) permissions --- .../src/main/java/sonia/scm/security/PermissionDescriptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index 8d95131ee6..3ea7737b4e 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable @Override public int hashCode() { - return value.hashCode(); + return value == null? -1: value.hashCode(); } /** From a456dd5e42a40dc2941c59a0247096c5277d67b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 13:39:14 +0200 Subject: [PATCH 23/33] Check for null values --- .../sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index 2f16dc74f6..ad20b59529 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -181,6 +181,9 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private RepositoryPermission[] createPermissions(V1Repository v1Repository) { + if (v1Repository.getPermissions() == null) { + return new RepositoryPermission[0]; + } return v1Repository.getPermissions() .stream() .map(this::createPermission) From a6caa03d86bc794c33c4abee9ba81bcd4a5380c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 11 Jun 2019 14:34:44 +0200 Subject: [PATCH 24/33] Create migration module only if necessary --- .../sonia/scm/update/MigrationWizardContextListener.java | 9 +++++---- .../java/sonia/scm/update/MigrationWizardServlet.java | 7 +------ .../sonia/scm/update/MigrationWizardServletTest.java | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java index c98062c900..6929c8b10a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardContextListener.java @@ -2,21 +2,22 @@ package sonia.scm.update; import com.google.inject.Injector; import com.google.inject.servlet.GuiceServletContextListener; +import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; public class MigrationWizardContextListener extends GuiceServletContextListener { - private final Injector injector; + private final Injector bootstrapInjector; public MigrationWizardContextListener(Injector bootstrapInjector) { - this.injector = bootstrapInjector.createChildInjector(new MigrationWizardModule()); + this.bootstrapInjector = bootstrapInjector; } public boolean wizardNecessary() { - return injector.getInstance(MigrationWizardServlet.class).wizardNecessary(); + return !bootstrapInjector.getInstance(XmlRepositoryV1UpdateStep.class).getRepositoriesWithoutMigrationStrategies().isEmpty(); } @Override protected Injector getInjector() { - return injector; + return bootstrapInjector.createChildInjector(new MigrationWizardModule()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index f26b1afac5..bf89382b1a 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -44,17 +44,13 @@ class MigrationWizardServlet extends HttpServlet { this.migrationStrategyDao = migrationStrategyDao; } - public boolean wizardNecessary() { - return !repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies().isEmpty(); - } - @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { List repositoryLineEntries = getRepositoryLineEntries(); doGet(req, resp, repositoryLineEntries); } - protected void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { + private void doGet(HttpServletRequest req, HttpServletResponse resp, List repositoryLineEntries) { HashMap model = new HashMap<>(); model.put("contextPath", req.getContextPath()); @@ -77,7 +73,6 @@ class MigrationWizardServlet extends HttpServlet { String id = repositoryLineEntry.getId(); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); - String strategy = req.getParameter("strategy-" + id); repositoryLineEntry.setNamespace(namespace); repositoryLineEntry.setName(name); diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index e40179eb7f..612e614f6f 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -157,7 +157,6 @@ class MigrationWizardServletTest { ); doReturn("invalid namespace").when(request).getParameter("namespace-id"); doReturn("invalid name").when(request).getParameter("name-id"); - doReturn("COPY").when(request).getParameter("strategy-id"); servlet.doPost(request, response); From 001dd8eefe49db06f6bec5ef70c0b0e13329d554 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 08:29:28 +0200 Subject: [PATCH 25/33] listen to restart events in every stage, not only development --- scm-core/src/main/java/sonia/scm/boot/RestartEvent.java | 8 +++++--- .../main/java/sonia/scm/boot/BootstrapContextFilter.java | 9 ++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java index 244bc8f03c..9aab8d18ae 100644 --- a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java +++ b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java @@ -33,14 +33,16 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Stage; import sonia.scm.event.Event; /** * This event can be used to force a restart of the webapp context. The restart * event is useful during plugin development, because we don't have to restart - * the whole server, to see our changes. The restart event can only be used in - * stage {@link Stage#DEVELOPMENT}. + * the whole server, to see our changes. The restart event could also be used + * to install or upgrade plugins. + * + * But the restart event should be used carefully, because the whole context + * will be restarted and that process could take some time. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java index aec8e2d653..cf4ec33f1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -37,8 +37,6 @@ import com.github.legman.Subscribe; import com.google.inject.servlet.GuiceFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.SCMContext; -import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; import javax.servlet.FilterConfig; @@ -99,11 +97,8 @@ public class BootstrapContextFilter extends GuiceFilter initGuice(); - if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT) - { - logger.info("register for restart events"); - ScmEventBus.getInstance().register(this); - } + logger.info("register for restart events"); + ScmEventBus.getInstance().register(this); } public void initGuice() throws ServletException { From 24d91a4764857ae389e69e2c473240ff0a624208 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 10:39:49 +0200 Subject: [PATCH 26/33] use mustache template inheritance to reduce duplications between templates --- .../main/resources/templates/layout.mustache | 33 +++ .../repository-migration-restart.mustache | 90 +++----- .../templates/repository-migration.mustache | 192 ++++++++---------- 3 files changed, 154 insertions(+), 161 deletions(-) create mode 100644 scm-webapp/src/main/resources/templates/layout.mustache diff --git a/scm-webapp/src/main/resources/templates/layout.mustache b/scm-webapp/src/main/resources/templates/layout.mustache new file mode 100644 index 0000000000..1132b95a30 --- /dev/null +++ b/scm-webapp/src/main/resources/templates/layout.mustache @@ -0,0 +1,33 @@ + + + + {{$title}}SCM-Manager{{/title}} + + + + +
+ +
+
+
+
+
SCM-Manager
+
+
+
+
+ +
+
+
+

{{$title}}SCM-Manager{{/title}}

+ {{$content}}{{/content}} +
+
+
+ +
+{{$script}}{{/script}} + + diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 1ff8a1baa6..5bda93c7c8 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -1,36 +1,13 @@ - - - - SCM-Manager Restart - - - - -
-
-
-
-
-
-
-
SCM-Manager
-
-
-
-
-
-
-
-

SCM-Manager will restart to migrate the data.

-
-
-
-

- - +{{ + - - -

-
-
-
-
-
- - - + request.onload = function () { + if (this.readyState == 4 && this.status == 200 && this.response.toString().indexOf("_links") > 0) { + location.href = '{{ contextPath }}'; + } + }; + + request.send(); + }, + 3000 + ); + +{{/script}} + +{{/layout}} diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 11d853a2b8..d322bfa42b 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -1,111 +1,93 @@ - - - - SCM-Manager Migration - - - - -
-
-
-
-
-
-
-
SCM-Manager
+{{< layout}} + +{{$title}}SCM-Manager Migration{{/title}} + +{{$content}} +

You have migrated from SCM-Manager v1 to SCM-Manager v2.

+ +

+ To migrate the existing repositories you have to specify a namespace and a name for each on them + as well as a migration strategy. +

+ +

+ The strategies are the following: +

+ + + {{#strategies}} + + + + + {{/strategies}} +
{{name}}{{description}}
+ +
+ + {{#validationErrorsFound}} +
Please correct the invalid namespaces or names below and try again.
+
+ {{/validationErrorsFound}} + +
+ + + + + + + + {{/repositories}} +
Original nameTypeNew namespace + + New name + + Strategy + +
Change all: +
+
+
- - -
-
-
-

-

SCM-Manager Migration

-

You have migrated from SCM-Manager v1 to SCM-Manager v2.

-

- To migrate the existing repositories you have to specify a namespace and a name for each on them - as well as a migration strategy. -

-

- The strategies are the following: - + + + {{#repositories}} + + + + + + - - - + {{/strategies}} -
+ {{path}} + + {{type}} + + + + + +
+
+
{{name}}{{description}}
-

+ +
-
- {{#validationErrorsFound}} -
Please correct the invalid namespaces or names below and try again.
-
- {{/validationErrorsFound}} - - - - - - - - - - {{#repositories}} - - - - - - - - {{/repositories}} -
Original nameTypeNew namespace - - New name - - Strategy - -
Change all: -
-
- -
-
-
- {{path}} - - {{type}} - - - - - -
-
- -
-
-
- - - - - - - - + +
+ + +{{/content}} + +{{$script}} - +{{/script}} + +{{/ layout}} From c159d209d65b59031626519fd2933d7df69fb634 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 10:51:57 +0200 Subject: [PATCH 27/33] increase compatibility of javascript migration code * replace const with var * replace forEach with a for of loop * use === instead of == --- .../templates/repository-migration-restart.mustache | 4 ++-- .../resources/templates/repository-migration.mustache | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache index 5bda93c7c8..006cc77e2a 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration-restart.mustache @@ -48,12 +48,12 @@ {{$script}} From dd61ec8e0a80768cdd520030b39643ad0f046ae0 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:00:14 +0200 Subject: [PATCH 28/33] added favicon to migration wizard pages --- .../src/main/java/sonia/scm/update/MigrationWizardModule.java | 2 +- scm-webapp/src/main/resources/templates/layout.mustache | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java index b2c4e842a3..4b357b6d96 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardModule.java @@ -20,7 +20,7 @@ class MigrationWizardModule extends ServletModule { LOG.info("= ="); LOG.info("=========================================================="); bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {}); - serve("/images/*", "/styles/*").with(WebResourceServlet.class); + serve("/images/*", "/styles/*", "/favicon.ico").with(WebResourceServlet.class); serve("/*").with(MigrationWizardServlet.class); } } diff --git a/scm-webapp/src/main/resources/templates/layout.mustache b/scm-webapp/src/main/resources/templates/layout.mustache index 1132b95a30..b656984eac 100644 --- a/scm-webapp/src/main/resources/templates/layout.mustache +++ b/scm-webapp/src/main/resources/templates/layout.mustache @@ -3,6 +3,7 @@ {{$title}}SCM-Manager{{/title}} + From 614c61a00c8ed0963b34359250bac19372a32341 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:36:15 +0200 Subject: [PATCH 29/33] keep select migration strategy in case of an error --- .../scm/update/MigrationWizardServlet.java | 46 ++++++++++++++++++- .../templates/repository-migration.mustache | 2 +- .../update/MigrationWizardServletTest.java | 37 +++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index bf89382b1a..b0949e82f0 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -4,6 +4,7 @@ import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.boot.RestartEvent; @@ -71,9 +72,16 @@ class MigrationWizardServlet extends HttpServlet { boolean validationErrorFound = false; for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) { String id = repositoryLineEntry.getId(); + + String strategy = req.getParameter("strategy-" + id); + if (!Strings.isNullOrEmpty(strategy)) { + repositoryLineEntry.setSelectedStrategy(MigrationStrategy.valueOf(strategy)); + } + String namespace = req.getParameter("namespace-" + id); - String name = req.getParameter("name-" + id); repositoryLineEntry.setNamespace(namespace); + + String name = req.getParameter("name-" + id); repositoryLineEntry.setName(name); if (!ValidationUtil.isRepositoryNameValid(namespace)) { @@ -144,6 +152,7 @@ class MigrationWizardServlet extends HttpServlet { private final String id; private final String type; private final String path; + private MigrationStrategy selectedStrategy; private String namespace; private String name; private boolean namespaceValid = true; @@ -153,6 +162,7 @@ class MigrationWizardServlet extends HttpServlet { this.id = repository.getId(); this.type = repository.getType(); this.path = repository.getType() + "/" + repository.getName(); + this.selectedStrategy = MigrationStrategy.COPY; this.namespace = computeNewNamespace(repository); this.name = computeNewName(repository); } @@ -195,6 +205,17 @@ class MigrationWizardServlet extends HttpServlet { return name; } + public MigrationStrategy getSelectedStrategy() { + return selectedStrategy; + } + + public List getStrategies() { + return Arrays.asList(MigrationStrategy.values()) + .stream() + .map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s)) + .collect(Collectors.toList()); + } + public void setNamespace(String namespace) { this.namespace = namespace; } @@ -211,6 +232,10 @@ class MigrationWizardServlet extends HttpServlet { this.nameValid = nameValid; } + public void setSelectedStrategy(MigrationStrategy selectedStrategy) { + this.selectedStrategy = selectedStrategy; + } + public boolean isNamespaceInvalid() { return !namespaceValid; } @@ -219,4 +244,23 @@ class MigrationWizardServlet extends HttpServlet { return !nameValid; } } + + private static class RepositoryLineMigrationStrategy { + + private final String name; + private final boolean selected; + + private RepositoryLineMigrationStrategy(String name, boolean selected) { + this.name = name; + this.selected = selected; + } + + public String getName() { + return name; + } + + public boolean isSelected() { + return selected; + } + } } diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index de4f59a441..8b6c685fc6 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -74,7 +74,7 @@
diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 612e614f6f..fed4a3b6c8 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -157,6 +157,7 @@ class MigrationWizardServletTest { ); doReturn("invalid namespace").when(request).getParameter("namespace-id"); doReturn("invalid name").when(request).getParameter("name-id"); + doReturn("COPY").when(request).getParameter("strategy-id"); servlet.doPost(request, response); @@ -171,6 +172,42 @@ class MigrationWizardServletTest { .contains(true); } + @Test + void shouldKeepSelectedMigrationStrategy() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + doReturn("we need an").when(request).getParameter("namespace-id"); + doReturn("error for this test").when(request).getParameter("name-id"); + doReturn("INLINE").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("selectedStrategy") + .contains(MigrationStrategy.INLINE); + } + + @Test + void shouldUseCopyWithoutMigrationStrategy() { + when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( + Collections.singletonList(new V1Repository("id", "git", "name")) + ); + + doReturn("we need an").when(request).getParameter("namespace-id"); + doReturn("error for this test").when(request).getParameter("name-id"); + doReturn("").when(request).getParameter("strategy-id"); + + servlet.doPost(request, response); + + assertThat(renderedModel.get("repositories")) + .asList() + .extracting("selectedStrategy") + .contains(MigrationStrategy.COPY); + } + @Test void shouldStoreValidMigration() { when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn( From fb384cd95de9430886e24d47a3f1dc16db010407 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 11:51:50 +0000 Subject: [PATCH 30/33] Close branch bugfix/check_for_empty_permissions From 249ee68986e9f79bc2a7c6a73584ba281adbd3da Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 14:19:38 +0200 Subject: [PATCH 31/33] use Arrays.stream instead of Arrays.asList(..).stream() --- .../src/main/java/sonia/scm/update/MigrationWizardServlet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index b0949e82f0..479778852b 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -210,8 +210,7 @@ class MigrationWizardServlet extends HttpServlet { } public List getStrategies() { - return Arrays.asList(MigrationStrategy.values()) - .stream() + return Arrays.stream(MigrationStrategy.values()) .map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s)) .collect(Collectors.toList()); } From c491092c0c82cf277ecaad20912223faef77593f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 14:20:20 +0200 Subject: [PATCH 32/33] use for..in loop instead of for..of to increase compatibility --- .../main/resources/templates/repository-migration.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache index 8b6c685fc6..9c76438667 100644 --- a/scm-webapp/src/main/resources/templates/repository-migration.mustache +++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache @@ -93,8 +93,8 @@ var changeAllSelector = document.getElementById('changeAll'); changeAllSelector.onchange = function () { var strategySelects = document.getElementsByClassName('strategy-select'); - for (var strategySelect of strategySelects) { - strategySelect.value = changeAllSelector.value; + for (var index in strategySelects) { + strategySelects[index].value = changeAllSelector.value; } }; }); From 34b1047d39e461ac34026c68c33595a63f441911 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Jun 2019 13:40:00 +0000 Subject: [PATCH 33/33] Close branch feature/migration_servlet