mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-27 17:59:09 +01:00
Merged in feature/migrate_repository_v1 (pull request #257)
Feature/migrate repository v1
This commit is contained in:
@@ -59,9 +59,6 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
|
||||
|
||||
public static final String DEFAULT_VERSION_INFORMATION = "unknown";
|
||||
|
||||
public static final String DOT = ".";
|
||||
static final String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
||||
|
||||
/**
|
||||
* the logger for AbstractSimpleRepositoryHandler
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,8 @@ import java.io.File;
|
||||
*/
|
||||
public interface RepositoryDirectoryHandler extends RepositoryHandler {
|
||||
|
||||
String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
||||
|
||||
/**
|
||||
* Get the current directory of the repository for the given id.
|
||||
* @return the current directory of the given repository
|
||||
|
||||
@@ -30,7 +30,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
*/
|
||||
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
|
||||
|
||||
private static final String STORE_NAME = "repositories";
|
||||
public static final String STORE_NAME = "repository-paths";
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
||||
|
||||
@@ -95,10 +95,17 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
|
||||
@Override
|
||||
public void add(Repository repository) {
|
||||
add(repository, repositoryLocationResolver.create(repository.getId()));
|
||||
}
|
||||
|
||||
public void add(Repository repository, Object location) {
|
||||
if (!(location instanceof Path)) {
|
||||
throw new IllegalArgumentException("can only handle locations of type " + Path.class.getName() + ", not of type " + location.getClass().getName());
|
||||
}
|
||||
Repository clone = repository.clone();
|
||||
|
||||
synchronized (this) {
|
||||
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
|
||||
Path repositoryPath = (Path) location;
|
||||
|
||||
try {
|
||||
Path metadataPath = resolveDataPath(repositoryPath);
|
||||
@@ -111,10 +118,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
byId.put(repository.getId(), clone);
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean contains(Repository repository) {
|
||||
return byId.containsKey(repository.getId());
|
||||
|
||||
@@ -152,7 +152,7 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
}
|
||||
|
||||
private String getXmlFileContent() {
|
||||
Path storePath = basePath.resolve("config").resolve("repositories.xml");
|
||||
Path storePath = basePath.resolve("config").resolve("repository-paths.xml");
|
||||
|
||||
assertThat(storePath).isRegularFile();
|
||||
return content(storePath);
|
||||
|
||||
@@ -117,6 +117,6 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,6 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
initRepository();
|
||||
File path = repositoryHandler.getDirectory(repository.getId());
|
||||
assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
assertEquals(repoPath.toString()+File.separator+ RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
|
||||
File repoDirectory = new File(baseDirectory, repository.getId());
|
||||
repoPath = repoDirectory.toPath();
|
||||
// when(repoDao.getPath(repository.getId())).thenReturn(repoPath);
|
||||
return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
return new File(repoDirectory, RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
}
|
||||
|
||||
protected File baseDirectory;
|
||||
|
||||
@@ -153,7 +153,12 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
||||
*/
|
||||
private void extract(File folder) throws IOException
|
||||
{
|
||||
URL url = Resources.getResource(getZippedRepositoryResource());
|
||||
String zippedRepositoryResource = getZippedRepositoryResource();
|
||||
extract(folder, zippedRepositoryResource);
|
||||
}
|
||||
|
||||
public static void extract(File targetFolder, String zippedRepositoryResource) throws IOException {
|
||||
URL url = Resources.getResource(zippedRepositoryResource);
|
||||
ZipInputStream zip = null;
|
||||
|
||||
try
|
||||
@@ -164,7 +169,7 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
||||
|
||||
while (entry != null)
|
||||
{
|
||||
File file = new File(folder, entry.getName());
|
||||
File file = new File(targetFolder, entry.getName());
|
||||
File parent = file.getParentFile();
|
||||
|
||||
if (!parent.exists())
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
abstract class BaseMigrationStrategy implements MigrationStrategy.Instance {
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
BaseMigrationStrategy(SCMContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
Path getSourceDataPath(String name, String type) {
|
||||
return Arrays.stream(name.split("/"))
|
||||
.reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1);
|
||||
}
|
||||
|
||||
Path getTypeDependentPath(String type) {
|
||||
return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type);
|
||||
}
|
||||
|
||||
Stream<Path> listSourceDirectory(Path sourceDirectory) {
|
||||
try {
|
||||
return Files.list(sourceDirectory);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not read original directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
void createDataDirectory(Path target) {
|
||||
try {
|
||||
Files.createDirectories(target);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not create data directory " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
void moveFile(Path sourceFile, Path targetFile) {
|
||||
try {
|
||||
Files.move(sourceFile, targetFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
void copyFile(Path sourceFile, Path targetFile) {
|
||||
try {
|
||||
Files.copy(sourceFile, targetFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not copy original file from " + sourceFile + " to " + targetFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class CopyMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||
super(contextProvider);
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
Path sourceDataPath = getSourceDataPath(name, type);
|
||||
copyData(sourceDataPath, targetDataPath);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void copyData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory).forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
copyData(sourceFile, targetFile);
|
||||
} else {
|
||||
copyFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class InlineMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
@Inject
|
||||
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
|
||||
super(contextProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = getSourceDataPath(name, type);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
moveData(repositoryBasePath, targetDataPath);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory)
|
||||
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
|
||||
.forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
moveData(sourceFile, targetFile);
|
||||
} else {
|
||||
moveFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
enum MigrationStrategy {
|
||||
|
||||
COPY(CopyMigrationStrategy.class),
|
||||
MOVE(MoveMigrationStrategy.class),
|
||||
INLINE(InlineMigrationStrategy.class);
|
||||
|
||||
private Class<? extends Instance> implementationClass;
|
||||
|
||||
MigrationStrategy(Class<? extends Instance> implementationClass) {
|
||||
this.implementationClass = implementationClass;
|
||||
}
|
||||
|
||||
Instance from(Injector injector) {
|
||||
return injector.getInstance(implementationClass);
|
||||
}
|
||||
|
||||
interface Instance {
|
||||
Path migrate(String id, String name, String type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MigrationStrategyDao {
|
||||
|
||||
private final RepositoryMigrationPlan plan;
|
||||
private final ConfigurationStore<RepositoryMigrationPlan> store;
|
||||
|
||||
@Inject
|
||||
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
|
||||
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
|
||||
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
|
||||
}
|
||||
|
||||
public Optional<MigrationStrategy> get(String id) {
|
||||
return plan.get(id);
|
||||
}
|
||||
|
||||
public void set(String repositoryId, MigrationStrategy strategy) {
|
||||
plan.set(repositoryId, strategy);
|
||||
store.set(plan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
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.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
class MoveMigrationStrategy extends BaseMigrationStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class);
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||
super(contextProvider);
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path migrate(String id, String name, String type) {
|
||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||
Path targetDataPath = repositoryBasePath
|
||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||
Path sourceDataPath = getSourceDataPath(name, type);
|
||||
moveData(sourceDataPath, targetDataPath);
|
||||
deleteOldDataDir(getTypeDependentPath(type), name);
|
||||
return repositoryBasePath;
|
||||
}
|
||||
|
||||
private void deleteOldDataDir(Path rootPath, String name) {
|
||||
delete(rootPath, asList(name.split("/")));
|
||||
}
|
||||
|
||||
private void delete(Path rootPath, List<String> directories) {
|
||||
if (directories.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Path directory = rootPath.resolve(directories.get(0));
|
||||
delete(directory, directories.subList(1, directories.size()));
|
||||
try {
|
||||
Files.deleteIfExists(directory);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete source repository directory {}", directory);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||
createDataDirectory(targetDirectory);
|
||||
listSourceDirectory(sourceDirectory).forEach(
|
||||
sourceFile -> {
|
||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||
if (Files.isDirectory(sourceFile)) {
|
||||
moveData(sourceFile, targetFile);
|
||||
} else {
|
||||
moveFile(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
try {
|
||||
Files.delete(sourceDirectory);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete source repository directory {}", sourceDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "repository-migration")
|
||||
class RepositoryMigrationPlan {
|
||||
|
||||
private List<RepositoryEntry> entries;
|
||||
|
||||
RepositoryMigrationPlan() {
|
||||
this(new RepositoryEntry[0]);
|
||||
}
|
||||
|
||||
RepositoryMigrationPlan(RepositoryEntry... 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) {
|
||||
return entries.stream()
|
||||
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "entries")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
static class RepositoryEntry {
|
||||
|
||||
private String repositoryId;
|
||||
private MigrationStrategy dataMigrationStrategy;
|
||||
|
||||
RepositoryEntry() {
|
||||
}
|
||||
|
||||
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
|
||||
this.repositoryId = repositoryId;
|
||||
this.dataMigrationStrategy = dataMigrationStrategy;
|
||||
}
|
||||
|
||||
public MigrationStrategy getDataMigrationStrategy() {
|
||||
return dataMigrationStrategy;
|
||||
}
|
||||
|
||||
private void setStrategy(MigrationStrategy strategy) {
|
||||
this.dataMigrationStrategy = strategy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
/**
|
||||
* Moves an existing <code>repositories.xml</code> file to <code>repository-paths.xml</code>.
|
||||
* Note that this has to run <em>after</em> an old v1 repository database has been migrated to v2
|
||||
* (see {@link XmlRepositoryV1UpdateStep}).
|
||||
*/
|
||||
@Extension
|
||||
public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws IOException {
|
||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||
Path newRepositoryPathsFile = configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + StoreConstants.FILE_EXTENSION);
|
||||
if (Files.exists(oldRepositoriesFile)) {
|
||||
LOG.info("moving old repositories database files to repository-paths file");
|
||||
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.repository.xml";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
/**
|
||||
* Migrates SCM-Manager v1 repository data structure to SCM-Manager v2 data structure.
|
||||
* That is:
|
||||
* <ul>
|
||||
* <li>The old <code>repositories.xml</code> file is read</li>
|
||||
* <li>For each repository in this database,
|
||||
* <ul>
|
||||
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
|
||||
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
|
||||
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
|
||||
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
|
||||
* and
|
||||
* <li>the new <code>metadata.xml</code> file is created.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Extension
|
||||
public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
||||
|
||||
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final XmlRepositoryDAO repositoryDao;
|
||||
private final MigrationStrategyDao migrationStrategyDao;
|
||||
private final Injector injector;
|
||||
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryV1UpdateStep(
|
||||
SCMContextProvider contextProvider,
|
||||
XmlRepositoryDAO repositoryDao,
|
||||
MigrationStrategyDao migrationStrategyDao,
|
||||
Injector injector,
|
||||
ConfigurationEntryStoreFactory configurationEntryStoreFactory
|
||||
) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.repositoryDao = repositoryDao;
|
||||
this.migrationStrategyDao = migrationStrategyDao;
|
||||
this.injector = injector;
|
||||
this.propertyStore = configurationEntryStoreFactory
|
||||
.withType(V1Properties.class)
|
||||
.withName("repository-properties-v1")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.repository.xml";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws JAXBException {
|
||||
if (!determineV1File().exists()) {
|
||||
LOG.info("no v1 repositories database file found");
|
||||
return;
|
||||
}
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
|
||||
readV1Database(jaxbContext).ifPresent(
|
||||
v1Database -> {
|
||||
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
|
||||
v1Database.repositoryList.repositories.forEach(this::update);
|
||||
backupOldRepositoriesFile();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void backupOldRepositoriesFile() {
|
||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||
Path backupFile = configDir.resolve("repositories.xml.v1.backup");
|
||||
LOG.info("moving old repositories database files to backup file {}", backupFile);
|
||||
try {
|
||||
Files.move(oldRepositoriesFile, backupFile);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not backup old repository database file", e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private Path handleDataDirectory(V1Repository v1Repository) {
|
||||
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
|
||||
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
|
||||
}
|
||||
|
||||
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 RepositoryPermission[] createPermissions(V1Repository v1Repository) {
|
||||
if (v1Repository.permissions == null) {
|
||||
return new RepositoryPermission[0];
|
||||
}
|
||||
return v1Repository.permissions
|
||||
.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("/");
|
||||
}
|
||||
|
||||
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
|
||||
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(determineV1File());
|
||||
if (unmarshal instanceof V1RepositoryDatabase) {
|
||||
return of((V1RepositoryDatabase) unmarshal);
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
private File determineV1File() {
|
||||
File configDirectory = new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
return new File(configDirectory, "repositories" + StoreConstants.FILE_EXTENSION);
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "permissions")
|
||||
private static class V1Permission {
|
||||
private boolean groupPermission;
|
||||
private String name;
|
||||
private String type;
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class V1Property {
|
||||
private String key;
|
||||
private String value;
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "properties")
|
||||
private static class V1Properties {
|
||||
@XmlElement(name = "item")
|
||||
private List<V1Property> properties;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "repository-db")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class V1RepositoryDatabase {
|
||||
private long creationTime;
|
||||
private Long lastModified;
|
||||
@XmlElement(name = "repositories")
|
||||
private RepositoryList repositoryList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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 java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CopyMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
@Mock
|
||||
RepositoryLocationResolver locationResolver;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
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)));
|
||||
}
|
||||
|
||||
@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");
|
||||
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");
|
||||
assertThat(target.resolve("data")).exists();
|
||||
Path originalDataDir = tempDir
|
||||
.resolve("repositories")
|
||||
.resolve("git")
|
||||
.resolve("some")
|
||||
.resolve("more")
|
||||
.resolve("directories")
|
||||
.resolve("than")
|
||||
.resolve("one");
|
||||
assertDirectoriesEqual(target.resolve("data"), originalDataDir);
|
||||
}
|
||||
|
||||
private void assertDirectoriesEqual(Path targetDataDir, Path originalDataDir) {
|
||||
Stream<Path> list = null;
|
||||
try {
|
||||
list = Files.list(originalDataDir);
|
||||
} catch (IOException e) {
|
||||
fail("could not read original directory", e);
|
||||
}
|
||||
list.forEach(
|
||||
original -> {
|
||||
Path expectedTarget = targetDataDir.resolve(original.getFileName());
|
||||
assertThat(expectedTarget).exists();
|
||||
if (Files.isDirectory(original)) {
|
||||
assertDirectoriesEqual(expectedTarget, original);
|
||||
} else {
|
||||
assertThat(expectedTarget).hasSameContentAs(original);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InlineMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
|
||||
}
|
||||
|
||||
private Path resolveOldDirectory(Path tempDir) {
|
||||
return tempDir.resolve("repositories").resolve("git").resolve("some").resolve("more").resolve("directories").resolve("than").resolve("one");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.update.MigrationStrategy.INLINE;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class MigrationStrategyDaoTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
|
||||
private ConfigurationStoreFactory storeFactory;
|
||||
|
||||
@BeforeEach
|
||||
void initStore(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("any");
|
||||
|
||||
Assertions.assertThat(strategy).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNewValue() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
dao.set("id", INLINE);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||
|
||||
Assertions.assertThat(strategy).contains(INLINE);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
@BeforeEach
|
||||
void initExistingDatabase() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
dao.set("id", INLINE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindExistingValue() throws JAXBException {
|
||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||
|
||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||
|
||||
Assertions.assertThat(strategy).contains(INLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import sonia.scm.repository.update.MigrationStrategy.Instance;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MigrationStrategyMock {
|
||||
|
||||
static Injector init() {
|
||||
Map<Class, Instance> mocks = new HashMap<>();
|
||||
Injector mock = mock(Injector.class);
|
||||
when(
|
||||
mock.getInstance(any(Class.class)))
|
||||
.thenAnswer(
|
||||
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
|
||||
);
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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 java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MoveMigrationStrategyTest {
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
@Mock
|
||||
RepositoryLocationResolver locationResolver;
|
||||
|
||||
@BeforeEach
|
||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
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)));
|
||||
}
|
||||
|
||||
@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");
|
||||
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");
|
||||
assertThat(target.resolve("data")).exists();
|
||||
Path originalDataDir = tempDir
|
||||
.resolve("repositories")
|
||||
.resolve("git")
|
||||
.resolve("some");
|
||||
assertThat(originalDataDir).doesNotExist();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import sonia.scm.repository.spi.ZippedRepositoryTestBase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class V1RepositoryFileSystem {
|
||||
/**
|
||||
* Creates the following v1 repositories in the temp dir:
|
||||
* <pre>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423492071</creationDate>
|
||||
* <description>A repository with two folders.</description>
|
||||
* <id>3b91caa5-59c3-448f-920b-769aaa56b761</id>
|
||||
* <name>one/directory</name>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* </repository>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423543716</creationDate>
|
||||
* <description>A repository in deeply nested folders.</description>
|
||||
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
||||
* <name>some/more/directories/than/one</name>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* </repository>
|
||||
* <repository>
|
||||
* <properties/>
|
||||
* <contact>arthur@dent.uk</contact>
|
||||
* <creationDate>1558423440258</creationDate>
|
||||
* <description>A simple repository without directories.</description>
|
||||
* <id>454972da-faf9-4437-b682-dc4a4e0aa8eb</id>
|
||||
* <lastModified>1558425918578</lastModified>
|
||||
* <name>simple</name>
|
||||
* <permissions>
|
||||
* <groupPermission>true</groupPermission>
|
||||
* <name>mice</name>
|
||||
* <type>WRITE</type>
|
||||
* </permissions>
|
||||
* <permissions>
|
||||
* <groupPermission>false</groupPermission>
|
||||
* <name>dent</name>
|
||||
* <type>OWNER</type>
|
||||
* </permissions>
|
||||
* <permissions>
|
||||
* <groupPermission>false</groupPermission>
|
||||
* <name>trillian</name>
|
||||
* <type>READ</type>
|
||||
* </permissions>
|
||||
* <public>false</public>
|
||||
* <archived>false</archived>
|
||||
* <type>git</type>
|
||||
* <url>http://localhost:8081/scm/git/simple</url>
|
||||
* </repository>
|
||||
* </pre>
|
||||
*/
|
||||
static void createV1Home(Path tempDir) throws IOException {
|
||||
ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlRepositoryFileNameUpdateStepTest {
|
||||
|
||||
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
|
||||
|
||||
@BeforeEach
|
||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider);
|
||||
URL url = Resources.getResource("sonia/scm/repository/update/formerV2RepositoryFile.xml");
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
|
||||
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStore;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
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.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.update.MigrationStrategy.COPY;
|
||||
import static sonia.scm.repository.update.MigrationStrategy.INLINE;
|
||||
import static sonia.scm.repository.update.MigrationStrategy.MOVE;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class XmlRepositoryV1UpdateStepTest {
|
||||
|
||||
Injector injectorMock = MigrationStrategyMock.init();
|
||||
|
||||
@Mock
|
||||
SCMContextProvider contextProvider;
|
||||
@Mock
|
||||
XmlRepositoryDAO repositoryDAO;
|
||||
@Mock
|
||||
MigrationStrategyDao migrationStrategyDao;
|
||||
|
||||
ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore());
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Repository> storeCaptor;
|
||||
@Captor
|
||||
ArgumentCaptor<Path> locationCaptor;
|
||||
|
||||
XmlRepositoryV1UpdateStep updateStep;
|
||||
|
||||
@BeforeEach
|
||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createUpdateStepFromMocks() {
|
||||
updateStep = new XmlRepositoryV1UpdateStep(
|
||||
contextProvider,
|
||||
repositoryDAO,
|
||||
migrationStrategyDao,
|
||||
injectorMock,
|
||||
configurationEntryStoreFactory
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingDatabase {
|
||||
|
||||
@BeforeEach
|
||||
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void captureStoredRepositories() {
|
||||
lenient().doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture());
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewRepositories() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
verify(repositoryDAO, times(3)).add(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapAttributes() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("git");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapPermissions() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
Optional<Repository> repository = findByNamespace("git");
|
||||
|
||||
assertThat(repository.get().getPermissions())
|
||||
.hasSize(3)
|
||||
.contains(
|
||||
new RepositoryPermission("mice", "WRITE", true),
|
||||
new RepositoryPermission("dent", "OWNER", false),
|
||||
new RepositoryPermission("trillian", "READ", false)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractPropertiesFromRepositories() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
ConfigurationEntryStore<Object> store = configurationEntryStoreFactory.withType(null).withName("").build();
|
||||
assertThat(store.getAll())
|
||||
.hasSize(3);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(locationCaptor.getAllValues()).contains(targetDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForMissingMigrationStrategy() {
|
||||
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
|
||||
assertThrows(IllegalStateException.class, () -> updateStep.doUpdate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackupOldRepositoryDatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml")).doesNotExist();
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).exists();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailIfNoOldDatabaseExists() throws JAXBException {
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailIfFormerV2DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||
createFormerV2RepositoriesFile(tempDir);
|
||||
|
||||
updateStep.doUpdate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBackupFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||
createFormerV2RepositoriesFile(tempDir);
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml")).exists();
|
||||
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
|
||||
}
|
||||
|
||||
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
URL url = Resources.getResource("sonia/scm/repository/update/formerV2RepositoryFile.xml");
|
||||
Path configDir = tempDir.resolve("config");
|
||||
Files.createDirectories(configDir);
|
||||
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
|
||||
}
|
||||
|
||||
private Optional<Repository> findByNamespace(String namespace) {
|
||||
return storeCaptor.getAllValues().stream().filter(r -> r.getNamespace().equals(namespace)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<repositories creation-time="1558604015815" last-modified="1558605291416">
|
||||
<repository id="C2RRHjjeL2">repositories/C2RRHjjeL2</repository>
|
||||
</repositories>
|
||||
Binary file not shown.
Reference in New Issue
Block a user