Merged in feature/migrate_repository_v1 (pull request #257)

Feature/migrate repository v1
This commit is contained in:
Sebastian Sdorra
2019-06-03 12:49:03 +00:00
29 changed files with 1334 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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