Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-06-13 09:16:46 +02:00
37 changed files with 1377 additions and 258 deletions

View File

@@ -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

View File

@@ -13,8 +13,27 @@ public abstract class RepositoryLocationResolver {
return create(type);
}
@FunctionalInterface
public interface RepositoryLocationResolverInstance<T> {
/**
* 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);
}
}

View File

@@ -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

View File

@@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable
@Override
public int hashCode()
{
return value.hashCode();
return value == null? -1: value.hashCode();
}
/**

View File

@@ -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) {

View File

@@ -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<String, Path> 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());
}
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;
@@ -66,23 +67,43 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
@Override
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
return repositoryId -> {
if (pathById.containsKey(repositoryId)) {
return (T) contextProvider.resolve(pathById.get(repositoryId));
} else {
return (T) create(repositoryId);
return new RepositoryLocationResolverInstance<T>() {
@Override
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);
}
}
@Override
public void setLocation(String repositoryId, T location) {
if (pathById.containsKey(repositoryId)) {
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
} else {
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath());
}
}
};
}
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);
} 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;
@@ -141,6 +162,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
private void setLocation(String repositoryId, Path repositoryBasePath) {
pathById.put(repositoryId, repositoryBasePath);
writePathDatabase();
}
public void refresh() {
this.read();
}

View File

@@ -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;
@@ -57,7 +61,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 +69,7 @@ class PathBasedRepositoryLocationResolverTest {
@Test
void shouldPersistInitialDirectory() {
resolver.forClass(Path.class).getLocation("newId");
resolver.forClass(Path.class).createLocation("newId");
String content = getXmlFileContent();
@@ -78,7 +82,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 +95,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 +112,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();
}
@@ -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) {

View File

@@ -17,6 +17,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;
@@ -53,7 +54,23 @@ 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<Path>() {
@Override
public Path getLocation(String repositoryId) {
return locationResolver.create(repositoryId);
}
@Override
public Path createLocation(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()));
}

View File

@@ -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,21 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe
@Override
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
return repositoryId -> (T) tempDirectory.toPath();
return new RepositoryLocationResolverInstance<T>() {
@Override
public T getLocation(String repositoryId) {
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");
}
};
}
}

View File

@@ -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<Object> 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);
}

View File

@@ -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<PluginWrapper> plugins;
private Injector injector;
public interface Factory {
ScmContextListener create(ClassLoader parent, Set<PluginWrapper> 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);
}
}

View File

@@ -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 {

View File

@@ -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,18 +112,6 @@ public class BootstrapContextListener implements ServletContextListener {
public void contextDestroyed(ServletContextEvent sce) {
contextListener.contextDestroyed(sce);
for (PluginWrapper plugin : 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;
}
@@ -151,27 +141,59 @@ public class BootstrapContextListener implements ServletContextListener {
}
private void createContextListener(File pluginDirectory) {
ClassLoader cl;
Set<PluginWrapper> 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<PluginWrapper> 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);
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<PluginWrapper> 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) {
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) {
@@ -402,7 +424,7 @@ public class BootstrapContextListener implements ServletContextListener {
private ServletContext context;
/** Field description */
private ScmContextListener contextListener;
private ServletContextListener contextListener;
/** Field description */
private boolean registered = false;

View File

@@ -0,0 +1,23 @@
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 bootstrapInjector;
public MigrationWizardContextListener(Injector bootstrapInjector) {
this.bootstrapInjector = bootstrapInjector;
}
public boolean wizardNecessary() {
return !bootstrapInjector.getInstance(XmlRepositoryV1UpdateStep.class).getRepositoriesWithoutMigrationStrategies().isEmpty();
}
@Override
protected Injector getInjector() {
return bootstrapInjector.createChildInjector(new MigrationWizardModule());
}
}

View File

@@ -0,0 +1,26 @@
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 {
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("= Open SCM-Manager in a browser to start the wizard. =");
LOG.info("= =");
LOG.info("==========================================================");
bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {});
serve("/images/*", "/styles/*", "/favicon.ico").with(WebResourceServlet.class);
serve("/*").with(MigrationWizardServlet.class);
}
}

View File

@@ -0,0 +1,265 @@
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 com.google.common.base.Strings;
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;
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.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
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 {
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class);
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
private final MigrationStrategyDao migrationStrategyDao;
@Inject
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) {
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
this.migrationStrategyDao = migrationStrategyDao;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
doGet(req, resp, repositoryLineEntries);
}
private void doGet(HttpServletRequest req, HttpServletResponse resp, List<RepositoryLineEntry> repositoryLineEntries) {
HashMap<String, Object> model = new HashMap<>();
model.put("contextPath", req.getContextPath());
model.put("submitUrl", req.getRequestURI());
model.put("repositories", repositoryLineEntries);
model.put("strategies", getMigrationStrategies());
model.put("validationErrorsFound", repositoryLineEntries
.stream()
.anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid()));
respondWithTemplate(resp, model, "templates/repository-migration.mustache");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
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);
repositoryLineEntry.setNamespace(namespace);
String name = req.getParameter("name-" + id);
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);
}
);
Map<String, Object> model = Collections.singletonMap("contextPath", req.getContextPath());
respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache");
ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data"));
}
private List<RepositoryLineEntry> getRepositoryLineEntries() {
List<V1Repository> repositoriesWithoutMigrationStrategies =
repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies();
return repositoriesWithoutMigrationStrategies.stream()
.map(RepositoryLineEntry::new)
.sorted(comparing(RepositoryLineEntry::getPath))
.collect(Collectors.toList());
}
private MigrationStrategy[] getMigrationStrategies() {
return MigrationStrategy.values();
}
@VisibleForTesting
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
MustacheFactory mf = new DefaultMustacheFactory();
Mustache template = mf.compile(templateName);
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);
}
private static class RepositoryLineEntry {
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;
private boolean nameValid = true;
public RepositoryLineEntry(V1Repository repository) {
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);
}
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() {
return id;
}
public String getType() {
return type;
}
public String getPath() {
return path;
}
public String getNamespace() {
return namespace;
}
public String getName() {
return name;
}
public MigrationStrategy getSelectedStrategy() {
return selectedStrategy;
}
public List<RepositoryLineMigrationStrategy> getStrategies() {
return Arrays.stream(MigrationStrategy.values())
.map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s))
.collect(Collectors.toList());
}
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 void setSelectedStrategy(MigrationStrategy selectedStrategy) {
this.selectedStrategy = selectedStrategy;
}
public boolean isNamespaceInvalid() {
return !namespaceValid;
}
public boolean isNameInvalid() {
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;
}
}
}

View File

@@ -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;
@@ -7,9 +9,14 @@ 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 {
private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
@@ -19,13 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> 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) {

View File

@@ -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<Path> 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();
}
}

View File

@@ -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<Path> migrate(String id, String name, String type) {
return empty();
}
}

View File

@@ -1,29 +1,47 @@
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.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 {
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) {
public Optional<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;
return of(repositoryBasePath);
}
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))
@@ -31,11 +49,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);
}
}
}
}

View File

@@ -3,17 +3,41 @@ package sonia.scm.update.repository;
import com.google.inject.Injector;
import java.nio.file.Path;
import java.util.Optional;
enum MigrationStrategy {
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."),
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 Class<? extends Instance> implementationClass;
private final Class<? extends Instance> implementationClass;
private final String description;
MigrationStrategy(Class<? extends Instance> implementationClass) {
MigrationStrategy(Class<? extends Instance> implementationClass, String description) {
this.implementationClass = implementationClass;
this.description = description;
}
public Class<? extends Instance> getImplementationClass() {
return implementationClass;
}
public String getDescription() {
return description;
}
Instance from(Injector injector) {
@@ -21,6 +45,6 @@ enum MigrationStrategy {
}
interface Instance {
Path migrate(String id, String name, String type);
Optional<Path> migrate(String id, String name, String type);
}
}

View File

@@ -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;
@@ -17,12 +19,12 @@ public class MigrationStrategyDao {
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<MigrationStrategy> get(String id) {
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
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);
}
}

View File

@@ -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,14 +29,15 @@ class MoveMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> 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("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) {

View File

@@ -13,57 +13,74 @@ import static java.util.Arrays.asList;
@XmlRootElement(name = "repository-migration")
class RepositoryMigrationPlan {
private List<RepositoryEntry> entries;
private List<RepositoryMigrationEntry> entries;
RepositoryMigrationPlan() {
this(new RepositoryEntry[0]);
this(new RepositoryMigrationEntry[0]);
}
RepositoryMigrationPlan(RepositoryEntry... entries) {
RepositoryMigrationPlan(RepositoryMigrationEntry... entries) {
this.entries = new ArrayList<>(asList(entries));
}
Optional<MigrationStrategy> get(String repositoryId) {
return findEntry(repositoryId)
.map(RepositoryEntry::getDataMigrationStrategy);
}
public void set(String repositoryId, MigrationStrategy strategy) {
Optional<RepositoryEntry> entry = findEntry(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
} else {
entries.add(new RepositoryEntry(repositoryId, strategy));
}
}
private Optional<RepositoryEntry> findEntry(String repositoryId) {
Optional<RepositoryMigrationEntry> 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<RepositoryMigrationEntry> 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() {
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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<V1Permission> 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<V1Permission> 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 + '\'' +
'}';
}
}

View File

@@ -28,11 +28,12 @@ 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;
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;
@@ -102,13 +103,30 @@ 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();
}
);
}
public List<V1Repository> getRepositoriesWithoutMigrationStrategies() {
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");
@@ -122,61 +140,59 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
}
private void update(V1Repository v1Repository) {
Path destination = handleDataDirectory(v1Repository);
Repository repository = new Repository(
v1Repository.id,
v1Repository.type,
getNamespace(v1Repository),
getName(v1Repository),
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);
RepositoryMigrationPlan.RepositoryMigrationEntry repositoryMigrationEntry = readMigrationEntry(v1Repository);
Optional<Path> 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.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.getName(), newPath);
repositoryDao.add(repository, newPath);
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
}
);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
private Optional<Path> handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) {
return dataMigrationStrategy
.from(injector)
.migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType());
}
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) {
return findMigrationStrategy(v1Repository)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.getId() + " and name " + v1Repository.getName()));
}
private Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> findMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.getId());
}
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
if (v1Repository.permissions == null) {
if (v1Repository.getPermissions() == 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);
}
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("/");
LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName());
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission());
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
@@ -195,45 +211,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")
private 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<V1Permission> permissions;
private V1Properties 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 + '\'' +
'}';
}
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>{{$title}}SCM-Manager{{/title}}</title>
<link rel="stylesheet" type="text/css" href="{{ contextPath }}/styles/scm.css">
<link rel="shortcut icon" href="{{ contextPath }}/favicon.ico">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div class="App">
<section class="hero is-dark is-small">
<div class="hero-body">
<div class="container">
<div class="columns is-vcentered">
<div class="column"><img src="{{ contextPath }}/images/logo.png" alt="SCM-Manager"></div>
</div>
</div>
</div>
</section>
<div class="main">
<section class="section">
<div class="container">
<h1 class="title">{{$title}}SCM-Manager{{/title}}</h1>
{{$content}}<!-- no content defined -->{{/content}}
</div>
</section>
</div>
</div>
{{$script}}<!-- no script defined -->{{/script}}
</body>
</html>

View File

@@ -0,0 +1,68 @@
{{<layout}}
{{$title}}SCM-Manager will restart to migrate the data{{/title}}
{{$content}}
<p class="has-text-centered">
<svg width="200px" version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<path fill="#33B2E8" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite"/>
</path>
<path fill="#33B2E8" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 50 50"
to="-360 50 50"
repeatCount="indefinite"/>
</path>
<path fill="#33B2E8" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
L82,35.7z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite"/>
</path>
</svg>
</p>
{{/content}}
{{$script}}
<script>
setInterval(function () {
var request = new XMLHttpRequest();
request.open('GET', '{{ contextPath }}/api/v2/', true);
request.onload = function () {
if (this.readyState === 4 && this.status === 200 && this.response.toString().indexOf("_links") > 0) {
location.href = '{{ contextPath }}';
}
};
request.send();
},
3000
);
</script>
{{/script}}
{{/layout}}

View File

@@ -0,0 +1,104 @@
{{< layout}}
{{$title}}SCM-Manager Migration{{/title}}
{{$content}}
<h2 class="subtitle">You have migrated from SCM-Manager v1 to SCM-Manager v2.</h2>
<p>
To migrate the existing repositories you have to specify a namespace and a name for each on them
as well as a migration strategy.
</p>
<p>
The strategies are the following:
</p>
<table class="table">
{{#strategies}}
<tr>
<th>{{name}}</th>
<td>{{description}}</td>
</tr>
{{/strategies}}
</table>
<hr>
{{#validationErrorsFound}}
<div class="notification is-danger">Please correct the invalid namespaces or names below and try again.</div>
<hr>
{{/validationErrorsFound}}
<form action="{{submitUrl}}" method="post">
<table class="card-table table is-hoverable is-fullwidth">
<tr>
<th>Original name</th>
<th>Type</th>
<th>New namespace
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The namespace of the repository. This will be part op the url. The new namespace must consist of letters, digits, dots, dashes and underscores (it must not start with a dash or a underscore)."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
</th>
<th>New name
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The name of the repository. This will be part op the url. The new name must consist of letters, digits, dots, dashes and underscores (it must not start with a dash or a underscore)."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
</th>
<th>Strategy
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The strategy used to migrate the data directory of the repository. See above for the means of the different strategies."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
<br>Change all:
<div class="field">
<div class="control select">
<select id="changeAll">
{{#strategies}}
<option>{{name}}</option>
{{/strategies}}
</select>
</div>
</div>
</th>
</tr>
{{#repositories}}
<tr>
<td>
{{path}}
</td>
<td>
{{type}}
</td>
<td>
<input class="input {{#namespaceInvalid}}is-danger{{/namespaceInvalid}}" type="text" name="namespace-{{id}}" value="{{namespace}}">
</td>
<td>
<input class="input {{#nameInvalid}}is-danger{{/nameInvalid}}" type="text" name="name-{{id}}" value="{{name}}">
</td>
<td>
<div class="field">
<div class="control select">
<select class="strategy-select" name="strategy-{{id}}">
{{#strategies}}
<option{{#selected}} selected{{/selected}}>{{name}}</option>
{{/strategies}}
</select>
</div>
</div>
</td>
</tr>
{{/repositories}}
</table>
<button class="button is-primary" type="submit">Submit</button>
</form>
{{/content}}
{{$script}}
<script>
document.addEventListener("DOMContentLoaded", function() {
var changeAllSelector = document.getElementById('changeAll');
changeAllSelector.onchange = function () {
var strategySelects = document.getElementsByClassName('strategy-select');
for (var index in strategySelects) {
strategySelects[index].value = changeAllSelector.value;
}
};
});
</script>
{{/script}}
{{/ layout}}

View File

@@ -0,0 +1,224 @@
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<String, Object> renderedModel;
MigrationWizardServlet servlet;
@BeforeEach
void initServlet() {
servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) {
@Override
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> 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 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(
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");
}
}

View File

@@ -43,18 +43,18 @@ 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
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")

View File

@@ -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").get();
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();
}

View File

@@ -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<MigrationStrategy> strategy = dao.get("any");
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> 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<MigrationStrategy> strategy = dao.get("id");
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> 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<MigrationStrategy> strategy = dao.get("id");
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> 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");
}
}
}

View File

@@ -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<Instance>) 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<MigrationStrategy.Instance>) strategy.getImplementationClass())).thenReturn(strategyMock);
}
return mock;
}
}

View File

@@ -40,18 +40,18 @@ 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
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")

View File

@@ -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;
@@ -33,11 +34,10 @@ 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.INLINE;
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@@ -89,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<Object> 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
@@ -104,56 +109,20 @@ class XmlRepositoryV1UpdateStepTest {
void shouldMapAttributes() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
Optional<Repository> 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> repository = findByNamespace("git");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "git")
.hasFieldOrPropertyWithValue("name", "simple");
}
@Test
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("one");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "one")
.hasFieldOrPropertyWithValue("name", "directory");
}
@Test
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> 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> repository = findByNamespace("git");
Optional<Repository> repository = findByNamespace("namespace-454972da-faf9-4437-b682-dc4a4e0aa8eb");
assertThat(repository.get().getPermissions())
.hasSize(3)
@@ -176,14 +145,27 @@ class XmlRepositoryV1UpdateStepTest {
@Test
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);
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class);
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<MigrationStrategy.Instance>) strategy.getImplementationClass())).thenReturn(strategyMock);
}
updateStep.doUpdate();
assertThat(locationCaptor.getAllValues()).isEmpty();
}
@Test
void shouldFailForMissingMigrationStrategy() {
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
@@ -221,6 +203,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");