From bacfde7cab6d569f28a89ee6d596514d39837beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 20 May 2019 16:34:22 +0200 Subject: [PATCH 01/19] Add documentation --- .../java/sonia/scm/migration/UpdateStep.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java index eaa6d8d549..ed5f60a630 100644 --- a/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateStep.java @@ -3,11 +3,80 @@ package sonia.scm.migration; import sonia.scm.plugin.ExtensionPoint; import sonia.scm.version.Version; +/** + * This is the main interface for data migration/update. Using this interface, SCM-Manager provides the possibility to + * change data structures between versions for a given type of data. + *

The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for + * example + *

+ *

+ *

The version is unrelated to other versions and therefore can be chosen freely, so that a data type can be updated + * without in various ways independent of other data types or the official version of the plugin or the core. + * A coordination between different data types and their versions is only necessary, when update steps of different data + * types rely on each other. If a update step of data type A has to run before another step for data type + * B, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}. + *

+ *

The algorithm looks something like this:
+ * Whenever the SCM-Manager starts, + *

+ *

+ */ @ExtensionPoint public interface UpdateStep { + /** + * Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not + * start up. + */ void doUpdate() throws Exception; + /** + * Declares the new version of the data type given by {@link #getAffectedDataType()}. A update step will only be + * executed, when this version is bigger than the last recorded version for its data type according to + * {@link Version#compareTo(Version)} + */ Version getTargetVersion(); + /** + * Declares the data type this update step will take care of. This should be a qualified name, like + * com.example.myPlugin.configuration. + */ String getAffectedDataType(); } From 64fcdde749ddf317b001de7e8500189bfdbaf828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 May 2019 13:09:33 +0200 Subject: [PATCH 02/19] Create v2 repository entities for old v1 repositories --- .../PathBasedRepositoryLocationResolver.java | 2 +- .../spi/ZippedRepositoryTestBase.java | 9 +- .../update/XmlRepositoryV1UpdateStep.java | 163 ++++++++++++++++++ .../update/XmlRepositoryV1UpdateStepTest.java | 135 +++++++++++++++ 4 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index cde4b3c3e2..91c653a31d 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -30,7 +30,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; */ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { - private static final String STORE_NAME = "repositories"; + private static final String STORE_NAME = "repository-paths"; private final SCMContextProvider contextProvider; private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; diff --git a/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java b/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java index c7a3b8f4e9..99f91c7926 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/ZippedRepositoryTestBase.java @@ -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()) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java new file mode 100644 index 0000000000..fe1c175960 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -0,0 +1,163 @@ +package sonia.scm.repository.update; + +import sonia.scm.SCMContextProvider; +import sonia.scm.io.FileSystem; +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.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.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static sonia.scm.version.Version.parse; + +@Extension +public class XmlRepositoryV1UpdateStep implements UpdateStep { + + private final SCMContextProvider contextProvider; + private final XmlRepositoryDAO dao; + + @Inject + public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO dao) { + this.contextProvider = contextProvider; + this.dao = dao; + } + + @Override + public Version getTargetVersion() { + return parse("0.0.1"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.repository.xml"; + } + + @Override + public void doUpdate() throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); + V1RepositoryDatabase v1Database = readV1Database(jaxbContext); + v1Database.repositoryList.repositories.forEach(this::update); + } + + private void update(V1Repository v1Repository) { + Repository repository = new Repository( + v1Repository.id, + v1Repository.type, + getNamespace(v1Repository), + getName(v1Repository), + v1Repository.contact, + v1Repository.description, + createPermissions(v1Repository)); + dao.add(repository); + } + + 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) { + 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 V1RepositoryDatabase readV1Database(JAXBContext jaxbContext) throws JAXBException { + return (V1RepositoryDatabase) jaxbContext.createUnmarshaller().unmarshal(determineV1File()); + } + + 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") + public static class V1Permission { + private boolean groupPermission; + private String name; + private String type; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "repositories") + public static class V1Repository { + private Map properties; + private String contact; + private long creationDate; + private Long lastModified; + private String description; + private String id; + private String name; + private boolean isPublic; + private boolean archived; + private String type; + private List permissions; + + @Override + public String toString() { + return "V1Repository{" + + "properties=" + properties + + ", contact='" + contact + '\'' + + ", creationDate=" + creationDate + + ", lastModified=" + lastModified + + ", description='" + description + '\'' + + ", id='" + id + '\'' + + ", name='" + name + '\'' + + ", isPublic=" + isPublic + + ", archived=" + archived + + ", type='" + type + '\'' + + '}'; + } + } + + public static class RepositoryList { + @XmlElement(name = "repository") + private List repositories; + } + + @XmlRootElement(name = "repository-db") + @XmlAccessorType(XmlAccessType.FIELD) + public static class V1RepositoryDatabase { + private long creationTime; + private Long lastModified; + @XmlElement(name = "repositories") + private RepositoryList repositoryList; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java new file mode 100644 index 0000000000..0592a2e356 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -0,0 +1,135 @@ +package sonia.scm.repository.update; + +import org.assertj.core.api.Assertions; +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.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +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.spi.ZippedRepositoryTestBase; +import sonia.scm.repository.xml.XmlRepositoryDAO; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class XmlRepositoryV1UpdateStepTest { + + @Mock + SCMContextProvider contextProvider; + @Mock + XmlRepositoryDAO dao; + + @Captor + ArgumentCaptor storeCaptor; + + @InjectMocks + XmlRepositoryV1UpdateStep updateStep; + + @BeforeEach + void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { + when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); + } + + @BeforeEach + void captureStoredRepositories() { + doNothing().when(dao).add(storeCaptor.capture()); + } + + @Test + void shouldCreateNewRepositories() throws JAXBException { + updateStep.doUpdate(); + verify(dao, times(3)).add(any()); + } + + @Test + void shouldMapAttributes() throws JAXBException { + updateStep.doUpdate(); + + Optional 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 = findByNamespace("git"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "git") + .hasFieldOrPropertyWithValue("name", "simple") + ; + } + + @Test + void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException { + updateStep.doUpdate(); + + Optional repository = findByNamespace("one"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "one") + .hasFieldOrPropertyWithValue("name", "directory") + ; + } + + @Test + void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException { + updateStep.doUpdate(); + + Optional repository = findByNamespace("some"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "some") + .hasFieldOrPropertyWithValue("name", "more_directories_than_one") + ; + } + + @Test + void shouldMapPermissions() throws JAXBException { + updateStep.doUpdate(); + + Optional 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) + ) + ; + } + + private Optional findByNamespace(String namespace) { + return storeCaptor.getAllValues().stream().filter(r -> r.getNamespace().equals(namespace)).findFirst(); + } +} From f000c944a6c1bb6b964fe9c4876228644c99eea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 May 2019 16:59:17 +0200 Subject: [PATCH 03/19] Do not fail when v1 file does not exist --- .../update/XmlRepositoryV1UpdateStep.java | 12 +- .../update/XmlRepositoryV1UpdateStepTest.java | 161 +++++++++--------- 2 files changed, 92 insertions(+), 81 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index fe1c175960..923a26fee5 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -1,7 +1,6 @@ package sonia.scm.repository.update; import sonia.scm.SCMContextProvider; -import sonia.scm.io.FileSystem; import sonia.scm.migration.UpdateStep; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; @@ -49,6 +48,9 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @Override public void doUpdate() throws JAXBException { + if (!determineV1File().exists()) { + return; + } JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); V1RepositoryDatabase v1Database = readV1Database(jaxbContext); v1Database.repositoryList.repositories.forEach(this::update); @@ -109,7 +111,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "permissions") - public static class V1Permission { + private static class V1Permission { private boolean groupPermission; private String name; private String type; @@ -117,7 +119,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") - public static class V1Repository { + private static class V1Repository { private Map properties; private String contact; private long creationDate; @@ -147,14 +149,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } } - public static class RepositoryList { + private static class RepositoryList { @XmlElement(name = "repository") private List repositories; } @XmlRootElement(name = "repository-db") @XmlAccessorType(XmlAccessType.FIELD) - public static class V1RepositoryDatabase { + private static class V1RepositoryDatabase { private long creationTime; private Long lastModified; @XmlElement(name = "repositories") diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 0592a2e356..133f7c72ec 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -1,7 +1,7 @@ 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; @@ -44,89 +44,98 @@ class XmlRepositoryV1UpdateStepTest { XmlRepositoryV1UpdateStep updateStep; @BeforeEach - void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { + void mockScmHome(@TempDirectory.TempDir Path tempDir) { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); - ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); } - @BeforeEach - void captureStoredRepositories() { - doNothing().when(dao).add(storeCaptor.capture()); + @Nested + class WithExistingDatabase { + + @BeforeEach + void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { + ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); + } + + @BeforeEach + void captureStoredRepositories() { + doNothing().when(dao).add(storeCaptor.capture()); + } + + @Test + void shouldCreateNewRepositories() throws JAXBException { + updateStep.doUpdate(); + verify(dao, times(3)).add(any()); + } + + @Test + void shouldMapAttributes() throws JAXBException { + updateStep.doUpdate(); + + Optional 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 = findByNamespace("git"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "git") + .hasFieldOrPropertyWithValue("name", "simple"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException { + updateStep.doUpdate(); + + Optional repository = findByNamespace("one"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "one") + .hasFieldOrPropertyWithValue("name", "directory"); + } + + @Test + void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException { + updateStep.doUpdate(); + + Optional repository = findByNamespace("some"); + + assertThat(repository) + .get() + .hasFieldOrPropertyWithValue("namespace", "some") + .hasFieldOrPropertyWithValue("name", "more_directories_than_one"); + } + + @Test + void shouldMapPermissions() throws JAXBException { + updateStep.doUpdate(); + + Optional 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 shouldCreateNewRepositories() throws JAXBException { - updateStep.doUpdate(); - verify(dao, times(3)).add(any()); - } - - @Test - void shouldMapAttributes() throws JAXBException { + void shouldNotFailIfNoOldDatabaseExists() throws JAXBException { updateStep.doUpdate(); - Optional 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 = findByNamespace("git"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "git") - .hasFieldOrPropertyWithValue("name", "simple") - ; - } - - @Test - void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("one"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "one") - .hasFieldOrPropertyWithValue("name", "directory") - ; - } - - @Test - void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException { - updateStep.doUpdate(); - - Optional repository = findByNamespace("some"); - - assertThat(repository) - .get() - .hasFieldOrPropertyWithValue("namespace", "some") - .hasFieldOrPropertyWithValue("name", "more_directories_than_one") - ; - } - - @Test - void shouldMapPermissions() throws JAXBException { - updateStep.doUpdate(); - - Optional 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) - ) - ; } private Optional findByNamespace(String namespace) { From 0cc31ec3c36663def0dfee4332cbea94380205a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 21 May 2019 17:24:48 +0200 Subject: [PATCH 04/19] Add missing test file --- .../sonia/scm/repository/update/scm-home.v1.zip | Bin 0 -> 13593 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scm-webapp/src/test/resources/sonia/scm/repository/update/scm-home.v1.zip diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/update/scm-home.v1.zip b/scm-webapp/src/test/resources/sonia/scm/repository/update/scm-home.v1.zip new file mode 100644 index 0000000000000000000000000000000000000000..5d936db5e264ac6182855e34a5aae82871c2c3fd GIT binary patch literal 13593 zcmchddt6NEAIC@cNl{7nE2SG1B^CNf%B7WBrOPUk(M*>aDk%xMgsilZcEjpIVcAWT zEyZ?`P3gj}ke2kLHeC>f(C?g?Y0i00bI#0}UVeGyHU9a$pYQkiKA-P*o`<`uxP&Z) z{o`8b#`(bcTbiOp31TrfFoG?-JgF4%l)U!U^Ut-f-W)TYGESmXoI+v$<<1j$ybKhO zhXf*M8(EQzC{_f67RixWfiI&Bfn_ML%mJyn1}V_L8lc|^$U1@VeIAL=!Co3 zO{lO`=SN3&Ik%is{YGz7`krz(bE<~VyE(twUUqLU z(K?&GW77B;4SEiP4?Vg%q7Hgy7HeFJ?=}nGQ@F%f;hg&WIe)9aFN+m}(}TnnchJwQ z28thnrt|u)wfV8km)Oa{KyXllI4T^%RL`eLK6SM7u~jvs}{k)1=&c1$ivL z40`JAnkRi$df)Z#*wuIAlI0oOq>k3q&Y5mC?pJc&f83@Su&BXnaZY@aZIZfgx0g|# z@{09d%@&I_*2TZ2|FtyEg{nEn-_j^IiX$wuUsZ&)If$ zlhX~`>|Z%GFZH<^H4e$gIrl=hy$y2~JBv{$`7#uWDv}R$)$lUoaEWjI^{nz|9k*1D z@B3{*>=vzERCkY(Wj&|ocRP9HI;;sx^|4ZWnCt)LvsU`FgfsqDj7K`vldg9>7<~V8 zsMGe(Wyi|Jn_bSn2vKqiPTe`jb7s?p-=?kc?(?vxe!n*{!BhT5Mp*Grt5sEtUtFA3 zVOr*VVY<24;D0?M^O+~p&c-TMZ(8=k<7i5lL&IxIT(+y_Ku)bEwSMc?6MGpaPTT3M zUGX;FrTorHy4pWFyL9MhZf6t9g++g-xz)G-!0$IiGCCxsWJ3^f2-LI$!ky2 zTJBA!PQ3Q!!A6bt#E_=m#%I%P&&uiSZIu}CkW4#LH+g$Tdrm9B>lVr?R5T`dH2OHO_udX zj(Exxgd{FmWhmq0lVu#JNSVmc^0xR8?0IL?#kb`*{e8QoG`cT*X3-b@8K#pLtQNAU zb(w?1;q#ZvJ@)RPnH*SptWG^s_I#aojj`kNDJQ48P!$}+m&C?a-0e3n^)QNQ&C~8E zQQ5F1;kf>@rovVJL$nnp7kVGhYVU05-+c1vVjQP+RAtHVVwK z)>Vo3AKa;E=(!ejJ>mx~=TF{Wery}I+D<{LvITOVXTFuQ&OoPh5Okl&XeO^1-T1p$ zcybx!K5}~o9h}q3By19`{dZoP{))nQCgu8JrIzW(0e4fcg%zovPj1_i9T)4pB83`6 ze;9Z*c(#PLyi`lUP|sa0-{sDdIjiLw3)J^-E>N;hRJ?eht3hV>X(?%!Lw)W5AlZmX|>g~O)1*I&!}r#`;7?x{0v{PTmlP2x{<6!+g#mKY8$OziATHMPyG zoK^ef(J9Gc+TUXL@3W9#VXJM@2)?ugvZF-E&=rs)xH*y*5y=Y|$81lotYz-DR4DB* zQy8pLSTL?e|DxZ!V*wf4|Jc?1gJIaz7Dnxp^nFUtiuRp8QC(NxT@cjS>uOUjtzmZM zQQ~V;S#i(84GXoewhgt>e%hC?IaAHrug#{rG4Wc2;|i5KN4%a->D2YplxUu-wWn%U zeZf3`qnc?4@9t7MeEha%KJ&@oQ?ZAeV~cXD+H>DzrJsEDe4g$t?~6?{G>jUL=*n4| zc>Y^QHTfyO;nb}-sg6TG-|X+4+EM#sez{GASdHoX&z_-|^AcX`ew2cDgwuo%LZ+D0*;^|Cl^*=7gzQK(v>^-@(>y9B ziroMu`TOBa^O>^|JsV6@SA_J??rH(GR8C(WwD8G~Zu z{ft@^bkaDB!Z~J$6a^EX8g@}|e36XsjbSt)rx>iFHi2RoE0{ag2po>EvQq@fM$k5h zs%?miZ=46~8$22~xc- zB9IwGANk@z`iK&p{4t!)sRS!V9Sfoy;=m3 zapb?U;bS+L?{EXw59s?i%f>GuA^Zb8+%N(6Rt&p-Q^EDaR>z!cQVbzHip@Zp0W435 zzS^NYg|i|=73FIN>_xIgM_i6~H%WTce9eGAAc`Ky{Zd1k zVo97EbQo+3uP2ix%@%=#^it9|dr@)7`6JC-FeWDIR0>5#cUx-_$h`K8LxU1Q+q`Llk>uLy@^aU|zAeg6h~np<6+7*g$5? zG`D4cr-iQvQ-9?%F^v<`5yyLqRO7~_K~hPkG=IB}WpvMyH%k*pIA__h1PvQwUA)c@V9+7Lw-1Jn~p5SaS3W8(MNB z^zO!$L??iA=MRa)?`;G*82wO!7MGIbiSwRKAWir_mq2s$djwjuAJIg;P7tL5?@k@9 zx>uUFQs9Oi(uHr>`L33ui*+$Yps@aibkTX`-n2uq@R=fz#n!6=Agc$xlmfEsTX(@p z7=7o?Ndo-n)8iO|Rps2kL$bI##{ZpVdVz2>^ysO@rEv^O3)(xNhER#)44lFdAfMy?=m7B? zARvrPljIqC0Hck7flQF(DTV-L?s)uMjZnB#<5*D{LZG;lA`uGjQXg%49|%w*(;*Rq z|KZmMGM6idpE=aMbw}X-L=Cp%$!BAdT5CViQZ_rD<6$B;|)f`BojDSQI2}X^$ zw;fYAcz$M~64xl?myI>-!ap0mjjRR{T;4qY~Bru&| z{~$>Nz2t*nn#hOS6ett#Ndn_ylgOQG^l<~d>za@}q^Sr+jCwD$iu+XYcLrvQ=^_GxUUA}iIzbAl1LJH zvjE9mn+;Ikia0nGt zuuvC00m!(3y9Ca-02?;=5P=>Saz$7IYJR|FbLIzZv# Date: Tue, 21 May 2019 17:26:02 +0200 Subject: [PATCH 05/19] Adapt unit test --- .../repository/xml/PathBasedRepositoryLocationResolverTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index f8b517e18b..5f758f124a 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -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); From 1674bc2e7d1c1f3996a420729b2b98ebfa7bcb89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 22 May 2019 14:28:53 +0200 Subject: [PATCH 06/19] Bootstrap migration strategies --- .../scm/repository/xml/XmlRepositoryDAO.java | 11 ++- .../update/CopyMigrationStrategy.java | 21 ++++++ .../update/InlineMigrationStrategy.java | 21 ++++++ .../repository/update/MigrationStrategy.java | 26 +++++++ .../update/MigrationStrategyDao.java | 28 +++++++ .../update/MoveMigrationStrategy.java | 21 ++++++ .../update/RepositoryMigrationPlan.java | 69 +++++++++++++++++ .../update/XmlRepositoryV1UpdateStep.java | 26 ++++++- .../update/MigrationStrategyDaoTest.java | 75 +++++++++++++++++++ .../update/MigrationStrategyMock.java | 24 ++++++ .../update/XmlRepositoryV1UpdateStepTest.java | 23 +++++- 11 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 9b3105b5ed..f506793d1a 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -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()); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java new file mode 100644 index 0000000000..6b05f02888 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.update; + +import sonia.scm.SCMContextProvider; + +import javax.inject.Inject; +import java.nio.file.Path; + +class CopyMigrationStrategy implements MigrationStrategy.Instance { + + private final SCMContextProvider contextProvider; + + @Inject + public CopyMigrationStrategy(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public Path migrate(String id, String name, String type) { + return null; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java new file mode 100644 index 0000000000..1374461bea --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.update; + +import sonia.scm.SCMContextProvider; + +import javax.inject.Inject; +import java.nio.file.Path; + +class InlineMigrationStrategy implements MigrationStrategy.Instance { + + private final SCMContextProvider contextProvider; + + @Inject + public InlineMigrationStrategy(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public Path migrate(String id, String name, String type) { + return null; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java new file mode 100644 index 0000000000..6ff631208c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategy.java @@ -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 implementationClass; + + MigrationStrategy(Class implementationClass) { + this.implementationClass = implementationClass; + } + + Instance from(Injector injector) { + return injector.getInstance(implementationClass); + } + + interface Instance { + Path migrate(String id, String name, String type); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java new file mode 100644 index 0000000000..f98e697468 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MigrationStrategyDao.java @@ -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 store; + + @Inject + public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) { + store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); + this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); + } + + public Optional get(String id) { + return plan.get(id); + } + + public void set(String repositoryId, MigrationStrategy strategy) { + plan.set(repositoryId, strategy); + store.set(plan); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java new file mode 100644 index 0000000000..c2ccb91713 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.update; + +import sonia.scm.SCMContextProvider; + +import javax.inject.Inject; +import java.nio.file.Path; + +class MoveMigrationStrategy implements MigrationStrategy.Instance { + + private final SCMContextProvider contextProvider; + + @Inject + public MoveMigrationStrategy(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public Path migrate(String id, String name, String type) { + return null; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java new file mode 100644 index 0000000000..126bca8a23 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/RepositoryMigrationPlan.java @@ -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 entries; + + RepositoryMigrationPlan() { + this(new RepositoryEntry[0]); + } + + RepositoryMigrationPlan(RepositoryEntry... entries) { + this.entries = new ArrayList<>(asList(entries)); + } + + Optional get(String repositoryId) { + return findEntry(repositoryId) + .map(RepositoryEntry::getDataMigrationStrategy); + } + + public void set(String repositoryId, MigrationStrategy strategy) { + Optional entry = findEntry(repositoryId); + if (entry.isPresent()) { + entry.get().setStrategy(strategy); + } else { + entries.add(new RepositoryEntry(repositoryId, strategy)); + } + } + + private Optional 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; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index 923a26fee5..7ce7fe463c 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -1,5 +1,8 @@ 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.UpdateStep; import sonia.scm.plugin.Extension; @@ -17,6 +20,7 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -27,13 +31,19 @@ import static sonia.scm.version.Version.parse; @Extension public class XmlRepositoryV1UpdateStep implements UpdateStep { + private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class); + private final SCMContextProvider contextProvider; - private final XmlRepositoryDAO dao; + private final XmlRepositoryDAO repositoryDao; + private final MigrationStrategyDao migrationStrategyDao; + private final Injector injector; @Inject - public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO dao) { + public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDao, MigrationStrategyDao migrationStrategyDao, Injector injector) { this.contextProvider = contextProvider; - this.dao = dao; + this.repositoryDao = repositoryDao; + this.migrationStrategyDao = migrationStrategyDao; + this.injector = injector; } @Override @@ -57,6 +67,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private void update(V1Repository v1Repository) { + Path destination = handleDataDirectory(v1Repository); Repository repository = new Repository( v1Repository.id, v1Repository.type, @@ -65,7 +76,14 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { v1Repository.contact, v1Repository.description, createPermissions(v1Repository)); - dao.add(repository); + repositoryDao.add(repository); + } + + private Path handleDataDirectory(V1Repository v1Repository) { + MigrationStrategy dataMigrationStrategy = + migrationStrategyDao.get(v1Repository.id) + .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); + return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); } private RepositoryPermission[] createPermissions(V1Repository v1Repository) { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java new file mode 100644 index 0000000000..a7365ce656 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyDaoTest.java @@ -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 strategy = dao.get("any"); + + Assertions.assertThat(strategy).isEmpty(); + } + + @Test + void shouldReturnNewValue() throws JAXBException { + MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + + dao.set("id", INLINE); + + Optional 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 strategy = dao.get("id"); + + Assertions.assertThat(strategy).contains(INLINE); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java new file mode 100644 index 0000000000..79b79abb41 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.update; + +import com.google.inject.Injector; + +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 mocks = new HashMap<>(); + Injector mock = mock(Injector.class); + when( + mock.getInstance(any(Class.class))) + .thenAnswer( + invocationOnMock -> mocks.getOrDefault(invocationOnMock.getArgument(0), mock(invocationOnMock.getArgument(0))) + ); + return mock; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 133f7c72ec..095155f5e8 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -1,5 +1,6 @@ package sonia.scm.repository.update; +import com.google.inject.Injector; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -21,21 +22,30 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import static java.util.Optional.of; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +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 dao; + XmlRepositoryDAO repositoryDAO; + @Mock() + MigrationStrategyDao migrationStrategyDao; @Captor ArgumentCaptor storeCaptor; @@ -58,13 +68,20 @@ class XmlRepositoryV1UpdateStepTest { @BeforeEach void captureStoredRepositories() { - doNothing().when(dao).add(storeCaptor.capture()); + doNothing().when(repositoryDAO).add(storeCaptor.capture()); + } + + @BeforeEach + void createMigrationPlan(@TempDirectory.TempDir Path tempDir) { + 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(dao, times(3)).add(any()); + verify(repositoryDAO, times(3)).add(any()); } @Test From 97ad3897eda38043200d7dabb3a5496ba7235fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 22 May 2019 15:31:35 +0200 Subject: [PATCH 07/19] Correct instance mocks --- .../update/XmlRepositoryV1UpdateStep.java | 2 +- .../update/MigrationStrategyMock.java | 5 +- .../update/XmlRepositoryV1UpdateStepTest.java | 74 ++++++++++++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index 7ce7fe463c..d21eb079fa 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -76,7 +76,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { v1Repository.contact, v1Repository.description, createPermissions(v1Repository)); - repositoryDao.add(repository); + repositoryDao.add(repository, destination); } private Path handleDataDirectory(V1Repository v1Repository) { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java index 79b79abb41..e0ee39880f 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MigrationStrategyMock.java @@ -1,6 +1,7 @@ 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; @@ -12,12 +13,12 @@ import static org.mockito.Mockito.when; class MigrationStrategyMock { static Injector init() { - Map mocks = new HashMap<>(); + Map mocks = new HashMap<>(); Injector mock = mock(Injector.class); when( mock.getInstance(any(Class.class))) .thenAnswer( - invocationOnMock -> mocks.getOrDefault(invocationOnMock.getArgument(0), mock(invocationOnMock.getArgument(0))) + invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class) key)) ); return mock; } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 095155f5e8..c9a64a10e4 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -49,6 +49,8 @@ class XmlRepositoryV1UpdateStepTest { @Captor ArgumentCaptor storeCaptor; + @Captor + ArgumentCaptor locationCaptor; @InjectMocks XmlRepositoryV1UpdateStep updateStep; @@ -61,6 +63,61 @@ class XmlRepositoryV1UpdateStepTest { @Nested class WithExistingDatabase { + /** + * Creates the following v1 repositories in the temp dir: + *
+     * 
+     *     
+     *     arthur@dent.uk
+     *     1558423492071
+     *     A repository with two folders.
+     *     3b91caa5-59c3-448f-920b-769aaa56b761
+     *     one/directory
+     *     false
+     *     false
+     *     git
+     * 
+     * 
+     *     
+     *     arthur@dent.uk
+     *     1558423543716
+     *     A repository in deeply nested folders.
+     *     c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f
+     *     some/more/directories/than/one
+     *     false
+     *     false
+     *     git
+     * 
+     * 
+     *     
+     *     arthur@dent.uk
+     *     1558423440258
+     *     A simple repository without directories.
+     *     454972da-faf9-4437-b682-dc4a4e0aa8eb
+     *     1558425918578
+     *     simple
+     *     
+     *         true
+     *         mice
+     *         WRITE
+     *     
+     *     
+     *         false
+     *         dent
+     *         OWNER
+     *     
+     *     
+     *         false
+     *         trillian
+     *         READ
+     *     
+     *     false
+     *     false
+     *     git
+     *     http://localhost:8081/scm/git/simple
+     * 
+     * 
+ */ @BeforeEach void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); @@ -68,11 +125,11 @@ class XmlRepositoryV1UpdateStepTest { @BeforeEach void captureStoredRepositories() { - doNothing().when(repositoryDAO).add(storeCaptor.capture()); + doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture()); } @BeforeEach - void createMigrationPlan(@TempDirectory.TempDir Path tempDir) { + 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)); @@ -81,7 +138,7 @@ class XmlRepositoryV1UpdateStepTest { @Test void shouldCreateNewRepositories() throws JAXBException { updateStep.doUpdate(); - verify(repositoryDAO, times(3)).add(any()); + verify(repositoryDAO, times(3)).add(any(), any()); } @Test @@ -147,6 +204,17 @@ class XmlRepositoryV1UpdateStepTest { new RepositoryPermission("trillian", "READ", false) ); } + + @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 From ac851d4e8ac8df63e8ea966b287ee2ea811b46ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 22 May 2019 16:05:35 +0200 Subject: [PATCH 08/19] Implement copy strategy --- .../AbstractSimpleRepositoryHandler.java | 2 +- .../update/CopyMigrationStrategy.java | 66 +++++++++++++- .../update/CopyMigrationStrategyTest.java | 89 +++++++++++++++++++ .../update/V1RepositoryFileSystem.java | 67 ++++++++++++++ .../update/XmlRepositoryV1UpdateStepTest.java | 57 +----------- 5 files changed, 222 insertions(+), 59 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/CopyMigrationStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/V1RepositoryFileSystem.java diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 875fb55617..1335001cdc 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -60,7 +60,7 @@ public abstract class AbstractSimpleRepositoryHandler path.resolve(namePart), (p1, p2) -> p1); + } + + private Path getTypeDependentPath(String type) { + return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type); + } + + private void copyData(Path sourceDirectory, Path targetDirectory) { + createDataDirectory(targetDirectory); + Stream list = listSourceDirectory(sourceDirectory); + list.forEach( + sourceFile -> { + Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); + if (Files.isDirectory(sourceFile)) { + copyData(sourceFile, targetFile); + } else { + copyFile(sourceFile, targetFile); + } + } + ); + } + + private Stream listSourceDirectory(Path sourceDirectory) { + try { + return Files.list(sourceDirectory); + } catch (IOException e) { + throw new UpdateException("could not read original directory", e); + } + } + + private 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); + } + } + + private void createDataDirectory(Path target) { + try { + Files.createDirectories(target); + } catch (IOException e) { + throw new UpdateException("could not create data directory " + target, e); + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/CopyMigrationStrategyTest.java new file mode 100644 index 0000000000..d7217bec48 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/CopyMigrationStrategyTest.java @@ -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 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); + } + } + ); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/V1RepositoryFileSystem.java b/scm-webapp/src/test/java/sonia/scm/repository/update/V1RepositoryFileSystem.java new file mode 100644 index 0000000000..146953d790 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/V1RepositoryFileSystem.java @@ -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: + *
+   * 
+   *     
+   *     arthur@dent.uk
+   *     1558423492071
+   *     A repository with two folders.
+   *     3b91caa5-59c3-448f-920b-769aaa56b761
+   *     one/directory
+   *     false
+   *     false
+   *     git
+   * 
+   * 
+   *     
+   *     arthur@dent.uk
+   *     1558423543716
+   *     A repository in deeply nested folders.
+   *     c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f
+   *     some/more/directories/than/one
+   *     false
+   *     false
+   *     git
+   * 
+   * 
+   *     
+   *     arthur@dent.uk
+   *     1558423440258
+   *     A simple repository without directories.
+   *     454972da-faf9-4437-b682-dc4a4e0aa8eb
+   *     1558425918578
+   *     simple
+   *     
+   *         true
+   *         mice
+   *         WRITE
+   *     
+   *     
+   *         false
+   *         dent
+   *         OWNER
+   *     
+   *     
+   *         false
+   *         trillian
+   *         READ
+   *     
+   *     false
+   *     false
+   *     git
+   *     http://localhost:8081/scm/git/simple
+   * 
+   * 
+ */ + static void createV1Home(Path tempDir) throws IOException { + ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index c9a64a10e4..9b455b9590 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -63,64 +63,9 @@ class XmlRepositoryV1UpdateStepTest { @Nested class WithExistingDatabase { - /** - * Creates the following v1 repositories in the temp dir: - *
-     * 
-     *     
-     *     arthur@dent.uk
-     *     1558423492071
-     *     A repository with two folders.
-     *     3b91caa5-59c3-448f-920b-769aaa56b761
-     *     one/directory
-     *     false
-     *     false
-     *     git
-     * 
-     * 
-     *     
-     *     arthur@dent.uk
-     *     1558423543716
-     *     A repository in deeply nested folders.
-     *     c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f
-     *     some/more/directories/than/one
-     *     false
-     *     false
-     *     git
-     * 
-     * 
-     *     
-     *     arthur@dent.uk
-     *     1558423440258
-     *     A simple repository without directories.
-     *     454972da-faf9-4437-b682-dc4a4e0aa8eb
-     *     1558425918578
-     *     simple
-     *     
-     *         true
-     *         mice
-     *         WRITE
-     *     
-     *     
-     *         false
-     *         dent
-     *         OWNER
-     *     
-     *     
-     *         false
-     *         trillian
-     *         READ
-     *     
-     *     false
-     *     false
-     *     git
-     *     http://localhost:8081/scm/git/simple
-     * 
-     * 
- */ @BeforeEach void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { - ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/repository/update/scm-home.v1.zip"); + V1RepositoryFileSystem.createV1Home(tempDir); } @BeforeEach From 3e46aefb4b1b9b9255bcc80f4a856854d9daa4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 22 May 2019 17:18:04 +0200 Subject: [PATCH 09/19] Implement move strategy --- .../update/MoveMigrationStrategy.java | 96 ++++++++++++++++++- .../update/MoveMigrationStrategyTest.java | 62 ++++++++++++ 2 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java index c2ccb91713..8a8a3b3823 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java @@ -1,21 +1,113 @@ package sonia.scm.repository.update; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateException; +import sonia.scm.repository.AbstractSimpleRepositoryHandler; +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.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; class MoveMigrationStrategy implements MigrationStrategy.Instance { + private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class); + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; @Inject - public MoveMigrationStrategy(SCMContextProvider contextProvider) { + public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { this.contextProvider = contextProvider; + this.locationResolver = locationResolver; } @Override public Path migrate(String id, String name, String type) { - return null; + Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path targetDataPath = repositoryBasePath + .resolve(AbstractSimpleRepositoryHandler.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 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 Path getSourceDataPath(String name, String type) { + return Arrays.stream(name.split("/")) + .reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1); + } + + private Path getTypeDependentPath(String type) { + return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type); + } + + private void moveData(Path sourceDirectory, Path targetDirectory) { + createDataDirectory(targetDirectory); + Stream list = listSourceDirectory(sourceDirectory); + list.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); + } + } + + private Stream listSourceDirectory(Path sourceDirectory) { + try { + return Files.list(sourceDirectory); + } catch (IOException e) { + throw new UpdateException("could not read original directory", e); + } + } + + private 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); + } + } + + private void createDataDirectory(Path target) { + try { + Files.createDirectories(target); + } catch (IOException e) { + throw new UpdateException("could not create data directory " + target, e); + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java new file mode 100644 index 0000000000..ce894516df --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java @@ -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(); + } +} From 31e56a667213fcbee7d525d6154d203a82b32602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 23 May 2019 15:06:40 +0200 Subject: [PATCH 10/19] Implement inline strategy --- .../update/BaseMigrationStrategy.java | 60 +++++++++++++++++++ .../update/CopyMigrationStrategy.java | 45 +------------- .../update/InlineMigrationStrategy.java | 31 ++++++++-- .../update/MoveMigrationStrategy.java | 44 +------------- .../update/InlineMigrationStrategyTest.java | 49 +++++++++++++++ 5 files changed, 141 insertions(+), 88 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/BaseMigrationStrategy.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/InlineMigrationStrategyTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/BaseMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/BaseMigrationStrategy.java new file mode 100644 index 0000000000..3ac0a6fd68 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/BaseMigrationStrategy.java @@ -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 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); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java index 857c3c3428..84835543e0 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/CopyMigrationStrategy.java @@ -1,25 +1,20 @@ package sonia.scm.repository.update; import sonia.scm.SCMContextProvider; -import sonia.scm.migration.UpdateException; import sonia.scm.repository.AbstractSimpleRepositoryHandler; 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.Arrays; -import java.util.stream.Stream; -class CopyMigrationStrategy implements MigrationStrategy.Instance { +class CopyMigrationStrategy extends BaseMigrationStrategy { - private final SCMContextProvider contextProvider; private final RepositoryLocationResolver locationResolver; @Inject public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { - this.contextProvider = contextProvider; + super(contextProvider); this.locationResolver = locationResolver; } @@ -33,19 +28,9 @@ class CopyMigrationStrategy implements MigrationStrategy.Instance { return repositoryBasePath; } - private Path getSourceDataPath(String name, String type) { - return Arrays.stream(name.split("/")) - .reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1); - } - - private Path getTypeDependentPath(String type) { - return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type); - } - private void copyData(Path sourceDirectory, Path targetDirectory) { createDataDirectory(targetDirectory); - Stream list = listSourceDirectory(sourceDirectory); - list.forEach( + listSourceDirectory(sourceDirectory).forEach( sourceFile -> { Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); if (Files.isDirectory(sourceFile)) { @@ -56,28 +41,4 @@ class CopyMigrationStrategy implements MigrationStrategy.Instance { } ); } - - private Stream listSourceDirectory(Path sourceDirectory) { - try { - return Files.list(sourceDirectory); - } catch (IOException e) { - throw new UpdateException("could not read original directory", e); - } - } - - private 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); - } - } - - private void createDataDirectory(Path target) { - try { - Files.createDirectories(target); - } catch (IOException e) { - throw new UpdateException("could not create data directory " + target, e); - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java index 1374461bea..dc4fdb067d 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/InlineMigrationStrategy.java @@ -1,21 +1,42 @@ package sonia.scm.repository.update; import sonia.scm.SCMContextProvider; +import sonia.scm.repository.AbstractSimpleRepositoryHandler; import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -class InlineMigrationStrategy implements MigrationStrategy.Instance { - - private final SCMContextProvider contextProvider; +class InlineMigrationStrategy extends BaseMigrationStrategy { @Inject public InlineMigrationStrategy(SCMContextProvider contextProvider) { - this.contextProvider = contextProvider; + super(contextProvider); } @Override public Path migrate(String id, String name, String type) { - return null; + Path repositoryBasePath = getSourceDataPath(name, type); + Path targetDataPath = repositoryBasePath + .resolve(AbstractSimpleRepositoryHandler.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); + } + } + ); } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java index 8a8a3b3823..e0abc7ad9b 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java @@ -3,7 +3,6 @@ package sonia.scm.repository.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; -import sonia.scm.migration.UpdateException; import sonia.scm.repository.AbstractSimpleRepositoryHandler; import sonia.scm.repository.RepositoryLocationResolver; @@ -11,22 +10,19 @@ import javax.inject.Inject; 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.stream.Stream; import static java.util.Arrays.asList; -class MoveMigrationStrategy implements MigrationStrategy.Instance { +class MoveMigrationStrategy extends BaseMigrationStrategy { private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class); - private final SCMContextProvider contextProvider; private final RepositoryLocationResolver locationResolver; @Inject public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { - this.contextProvider = contextProvider; + super(contextProvider); this.locationResolver = locationResolver; } @@ -58,19 +54,9 @@ class MoveMigrationStrategy implements MigrationStrategy.Instance { } } - private Path getSourceDataPath(String name, String type) { - return Arrays.stream(name.split("/")) - .reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1); - } - - private Path getTypeDependentPath(String type) { - return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type); - } - private void moveData(Path sourceDirectory, Path targetDirectory) { createDataDirectory(targetDirectory); - Stream list = listSourceDirectory(sourceDirectory); - list.forEach( + listSourceDirectory(sourceDirectory).forEach( sourceFile -> { Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); if (Files.isDirectory(sourceFile)) { @@ -86,28 +72,4 @@ class MoveMigrationStrategy implements MigrationStrategy.Instance { LOG.warn("could not delete source repository directory {}", sourceDirectory); } } - - private Stream listSourceDirectory(Path sourceDirectory) { - try { - return Files.list(sourceDirectory); - } catch (IOException e) { - throw new UpdateException("could not read original directory", e); - } - } - - private 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); - } - } - - private void createDataDirectory(Path target) { - try { - Files.createDirectories(target); - } catch (IOException e) { - throw new UpdateException("could not create data directory " + target, e); - } - } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/InlineMigrationStrategyTest.java new file mode 100644 index 0000000000..ed0d7eeb40 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/InlineMigrationStrategyTest.java @@ -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"); + } +} From 035abef465b94538632c8e23b5e6382eab9807b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 23 May 2019 15:25:25 +0200 Subject: [PATCH 11/19] Assert that the existing repositories.xml file contains v1 data --- .../update/XmlRepositoryV1UpdateStep.java | 40 +++++++++++++++---- .../update/XmlRepositoryV1UpdateStepTest.java | 12 +++++- .../update/formerV2RepositoryFile.xml | 4 ++ 3 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 scm-webapp/src/test/resources/sonia/scm/repository/update/formerV2RepositoryFile.xml diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index d21eb079fa..c1ef200335 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -24,10 +24,30 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map; +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: + *
    + *
  • The old repositories.xml file is read
  • + *
  • For each repository in this database, + *
      + *
    • a new entry in the new repository-paths.xml database is written,
    • + *
    • 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 migration-plan.xml
    • (to create this file, use {@link MigrationStrategyDao}), + * and + *
    • the new metadata.xml file is created.
    • + *
    + *
  • + *
+ */ @Extension public class XmlRepositoryV1UpdateStep implements UpdateStep { @@ -62,8 +82,9 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return; } JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); - V1RepositoryDatabase v1Database = readV1Database(jaxbContext); - v1Database.repositoryList.repositories.forEach(this::update); + readV1Database(jaxbContext).ifPresent( + v1Database -> v1Database.repositoryList.repositories.forEach(this::update) + ); } private void update(V1Repository v1Repository) { @@ -82,7 +103,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private Path handleDataDirectory(V1Repository v1Repository) { MigrationStrategy dataMigrationStrategy = migrationStrategyDao.get(v1Repository.id) - .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); + .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type); } @@ -102,12 +123,12 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private String getNamespace(V1Repository v1Repository) { String[] nameParts = getNameParts(v1Repository.name); - return nameParts.length > 1? nameParts[0]: v1Repository.type; + 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); + return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts); } private String concatPathElements(String[] nameParts) { @@ -118,8 +139,13 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { return v1Name.split("/"); } - private V1RepositoryDatabase readV1Database(JAXBContext jaxbContext) throws JAXBException { - return (V1RepositoryDatabase) jaxbContext.createUnmarshaller().unmarshal(determineV1File()); + private Optional 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() { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 9b455b9590..3b0008f3c6 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -1,5 +1,6 @@ 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; @@ -14,11 +15,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.spi.ZippedRepositoryTestBase; import sonia.scm.repository.xml.XmlRepositoryDAO; 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; @@ -165,7 +167,15 @@ class XmlRepositoryV1UpdateStepTest { @Test void shouldNotFailIfNoOldDatabaseExists() throws JAXBException { updateStep.doUpdate(); + } + @Test + void shouldNotFailIfFormerV1DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, 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")); + updateStep.doUpdate(); } private Optional findByNamespace(String namespace) { diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/update/formerV2RepositoryFile.xml b/scm-webapp/src/test/resources/sonia/scm/repository/update/formerV2RepositoryFile.xml new file mode 100644 index 0000000000..ca21ead549 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/repository/update/formerV2RepositoryFile.xml @@ -0,0 +1,4 @@ + + + repositories/C2RRHjjeL2 + From 0a0fd174489656b98d626e45de8beb6b195c06ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 23 May 2019 17:37:58 +0200 Subject: [PATCH 12/19] Verify that migration strategies are set before migration starts --- .../update/XmlRepositoryV1UpdateStep.java | 14 ++++++++++---- .../update/XmlRepositoryV1UpdateStepTest.java | 10 +++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index c1ef200335..a08f26768c 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -83,7 +83,10 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); readV1Database(jaxbContext).ifPresent( - v1Database -> v1Database.repositoryList.repositories.forEach(this::update) + v1Database -> { + v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy); + v1Database.repositoryList.repositories.forEach(this::update); + } ); } @@ -101,12 +104,15 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private Path handleDataDirectory(V1Repository v1Repository) { - MigrationStrategy dataMigrationStrategy = - migrationStrategyDao.get(v1Repository.id) - .orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name)); + 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]; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 3b0008f3c6..b7ad55a889 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -24,8 +24,10 @@ 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.doNothing; import static org.mockito.Mockito.lenient; @@ -72,7 +74,7 @@ class XmlRepositoryV1UpdateStepTest { @BeforeEach void captureStoredRepositories() { - doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture()); + lenient().doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture()); } @BeforeEach @@ -162,6 +164,12 @@ class XmlRepositoryV1UpdateStepTest { assertThat(locationCaptor.getAllValues()).contains(targetDir); } + + @Test + void shouldFailForMissingMigrationStrategy() throws JAXBException { + lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty()); + assertThrows(IllegalStateException.class, () -> updateStep.doUpdate()); + } } @Test From d307ce6df0cdada95611ffbb0bc2b3818023d4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 24 May 2019 11:55:04 +0200 Subject: [PATCH 13/19] Set version and add logging --- .../scm/repository/update/XmlRepositoryV1UpdateStep.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index a08f26768c..8c93b21822 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -68,7 +68,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @Override public Version getTargetVersion() { - return parse("0.0.1"); + return parse("2.0.0"); } @Override @@ -79,6 +79,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { @Override public void doUpdate() throws JAXBException { if (!determineV1File().exists()) { + LOG.info("no v1 repositories database file found"); return; } JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); @@ -100,6 +101,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { 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); } @@ -124,6 +126,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { } private RepositoryPermission createPermission(V1Permission v1Permission) { + LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name); return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission); } From d88b300ac8dedf7fef1e1ee68c5804cf0cd876f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 24 May 2019 11:55:32 +0200 Subject: [PATCH 14/19] Add update step to rename repositories.xml -> repository-paths.xml --- .../PathBasedRepositoryLocationResolver.java | 2 +- .../XmlRepositoryFileNameUpdateStep.java | 51 +++++++++++++++++++ .../XmlRepositoryFileNameUpdateStepTest.java | 44 ++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStepTest.java diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index 91c653a31d..a1ce516069 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -30,7 +30,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; */ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver { - private static final String STORE_NAME = "repository-paths"; + public static final String STORE_NAME = "repository-paths"; private final SCMContextProvider contextProvider; private final InitialRepositoryLocationResolver initialRepositoryLocationResolver; diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java new file mode 100644 index 0000000000..aa6641f257 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java @@ -0,0 +1,51 @@ +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; + +@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"; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStepTest.java new file mode 100644 index 0000000000..6314e02783 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStepTest.java @@ -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(); + } +} From 76c59a6dee383369d3f4166a0c82683f4e1c00f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 24 May 2019 13:55:37 +0200 Subject: [PATCH 15/19] Extract properties from v1 repositories --- .../update/XmlRepositoryV1UpdateStep.java | 32 ++++++++++++++++-- .../update/XmlRepositoryV1UpdateStepTest.java | 30 ++++++++++++++-- .../scm/repository/update/scm-home.v1.zip | Bin 13593 -> 13412 bytes 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index 8c93b21822..9f79287a19 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -9,6 +9,8 @@ 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; @@ -57,13 +59,24 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private final XmlRepositoryDAO repositoryDao; private final MigrationStrategyDao migrationStrategyDao; private final Injector injector; + private final ConfigurationEntryStore propertyStore; @Inject - public XmlRepositoryV1UpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDao, MigrationStrategyDao migrationStrategyDao, Injector injector) { + 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 @@ -103,6 +116,7 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { 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) { @@ -170,10 +184,22 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { 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 properties; + } + @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") private static class V1Repository { - private Map properties; private String contact; private long creationDate; private Long lastModified; @@ -184,11 +210,11 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { private boolean archived; private String type; private List permissions; + private V1Properties properties; @Override public String toString() { return "V1Repository{" + - "properties=" + properties + ", contact='" + contact + '\'' + ", creationDate=" + creationDate + ", lastModified=" + lastModified + diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index b7ad55a889..8c0f4bb103 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -16,6 +16,10 @@ 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; @@ -29,7 +33,6 @@ 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.doNothing; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -48,15 +51,16 @@ class XmlRepositoryV1UpdateStepTest { SCMContextProvider contextProvider; @Mock XmlRepositoryDAO repositoryDAO; - @Mock() + @Mock MigrationStrategyDao migrationStrategyDao; + ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); + @Captor ArgumentCaptor storeCaptor; @Captor ArgumentCaptor locationCaptor; - @InjectMocks XmlRepositoryV1UpdateStep updateStep; @BeforeEach @@ -64,6 +68,17 @@ class XmlRepositoryV1UpdateStepTest { when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); } + @BeforeEach + void createUpdateStepFromMocks() { + updateStep = new XmlRepositoryV1UpdateStep( + contextProvider, + repositoryDAO, + migrationStrategyDao, + injectorMock, + configurationEntryStoreFactory + ); + } + @Nested class WithExistingDatabase { @@ -154,6 +169,15 @@ class XmlRepositoryV1UpdateStepTest { ); } + @Test + void shouldExtractPropertiesFromRepositories() throws JAXBException { + updateStep.doUpdate(); + + ConfigurationEntryStore 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"); diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/update/scm-home.v1.zip b/scm-webapp/src/test/resources/sonia/scm/repository/update/scm-home.v1.zip index 5d936db5e264ac6182855e34a5aae82871c2c3fd..0d43f2f4d6a0b5f595e58cda18933e1565bbc083 100644 GIT binary patch delta 2435 zcmZ8h3s@6Z7M|P?5=fW?0to~X5&{9jD`0q8(HP|+5F!tu7F@7J5eq1a3c{9Jw_87@ zUEp*}YiSJy*P`G9Dk80qszqJ863aud6uPYyq>ADy_EV9a2}y(|-^~2~fBtjsx%1sK zpKQFf!D3|`)dB*9j#Uj#)beYiRp)!j2h>o0#re7I3ESREU)GK=>^>9rAh+_f`q7Mj z;@Vcs#3&YHR17oR`cgHU>J^+bPttU?HI9q}4hPd4*W|LF)g3+}N-b4Pt!xrhv@tJ<;~V>Hb=x;cZ;Xa4EjFgcE?@E()Hn~ zPhqezOI5n_8Y)*m%wvpe7O$bG!1;Z6y4P}t)=KGQ5zzrmZ9j+9+y{vs_) zZrT`@_oL@)`4Ru@8ygzcmAn42;#OiFzdfd6ck7jF7YACsCr_~P&cItze4>^;!n6y; zzbL+2#(F<FE4X^l;u;B zy78x4w@b8dzp;-!(3x=8EmEG{qECLd{rfaWcWLnJ0{Nr(ve2PrPZZsPrmoBDI}$J0 zZ=0N~pnso!<*ZL4WjtskV)YYgBr`7!6Qpg{DZVVcwrhfaGVDh59>w&7_=wE4{`B>- zj{n~O=}=Rfm+InZs!c34;?Z9`QVJ@UpUxZz^LF`ByZ1_GuixF(%X1ue$GxKq%a)(} zqpu}4J^r_&wsOghnk_rJ?%3V9GSpjM`Wo0-Kkn>2>D?!~x~X*HUT$T?|29j1IPile zYUYy{Ot7Q{8_xV<+FkP5>Sy^nh04rrhibZ)t5U2mYUQC{Gf2^3uyG***6_KNmBC1Ay;i|SECRW8gAu2h?Nhh_cKzo>GP8!0KtQ50ti;cpJyKMptn1aVs^+cZ(Tn;;NUo-B(1C4Px^l_z- z!seR!hwMd0EP^|pILxqT+aqh}IZCr{v4aD=?Sn&(H`|)n@g?JqNt-FuVap@!WXGen zGv-V#_vPw^+)%UL#|<&yrF;?SommiOFP+Et*n7?6dJfS6pEvu)pBUFSB`fL8U(pI01Ax!!x{PFx8plX^g&ybNnpGa*UkES2!&K+zJ1a7E5ws|E-6e zB$RnEoTmhii}ASpx%nLDm^tR;*&=dS$Pk*~N|#q4%ZdR-41a^OP$Y(Mp@4!xiZB@s zV9+RBAfErp#xOqSh><@EaW(83qrh{ah)R6Kp-|)k;UW>5w1Cy3B+4)}i9#0=-w=ClgA;gogw1x6OEc@;Rik% zWXmjJ3*1>3Y zC8jDGot{g{A&W+Syg3dUR0e3!A(P|*e1!`6^39HYiWpS;lK$0+0qj6E!X3>HQo^0+ z@L`||y*pQC13yTGp1+j#1;wCmoy>`+1x!%-1wn#8j+6qkOw1+hTcAeu^Cg`#v5ZoM z9G&MlS@3}}h{G|?28MC4drK>vp~W=>jw?l|-x>7E;A*)A=p~jg<|;zoF2E4`+0!hE Qk;1}>|4lxI6}gb?|49vj^Z)<= delta 2169 zcmY+EeN+=y8pe~0A(MoJ2_YmILikKf2qZvKC~7IjN>WrLNCn%1vXn~D0!ohvEFf-S zYqwb8^<#_eB1oz0fr_xuqNqhF?QWHirCV()7Nynd8kOp)daSxLlQ4V8Klk@M?{n|W z+`RYxl>dGK;5Gwo&bkz_SY22n3PA}+p~hQU?OdHz zv_HzX^7fI$vQp)CUaI|Ava{{En@jBN32Qe(rb+%?d)AZ3%6*!ekFrdT?^VN+lh-cX zfAF^rOR659*gNQPKIzca0-u!iuq`e?6ChNc2 z8FM=P#L|uk1G@iHdP!H|hkFi{iHA2;CttPi-nB7dbcR*Yx-9CqgI}lfM#{@SsC9hM z7Oz^j@?K@q;JH=#@=L1is{G!9{=S{{R_vvEPJv)-%f$84cjLBvUs*ytdhqt7{uJl< zk9&TYh~Rl2|MfyKF@vw7TfgQHvK<(yi6_elX)XY->X0T&7QQeR^-^a^U?S& z)Kob1r@JllonJ0#n(Q0M6ztD^x}_Lv?*8TW13%F6WT&p}&qu>$`cD_1ssH(A=accM zk;cY!th=Djwpxv4X0~W^#4K-zGQ;?AeR}n|O`Z1!zsOp1)04Q_@wh1eg|9ZMK3*>` zcwsU*A>qBlJ%jez>O9?!7x$i39N-^58}Lu<;-5o43{B$U2_CkxveW;*YZ$Q4-#Ynm zz_nw3ub0;B556+qyE^Mp-b!7^t&1_!zl`4fW9!Rf5dYkn(k~wWdgeyq#bDL2@y3Ex zn|}PLX!nZ)8(Pn+t4}_C^V8q=g}oBl<~ufEs3I)mUg;i1u#MNo z)b0F8n(y!vV3aR%^Ky5cIvcP?@LwigiYcMq%N3&@^_5o?QyU>`i-i(ekBJDV5r#mm zSOOG}2_qQRiJ}?SOG05v?FCh0G9y~;_C;Xz?q)I?+{~~O*Mm^Yb>|6i;_-CNb&O$& z_#qQ@_(a3eFe!9-Veq*ShcA8N=4ibnme#mj4m-)$pmur6mw-zWEyEgI3U?HKknPK3 zJSXk>L#jea<}(;Sg)oC5k_*jJF5^u|tqfcJR=AnmKEH1mzfZQ5;b!?FPH|4|2BUj* z0-EK?bd~(TZbdMgNv04MhEEVSh9{(E2r_v=nu2im%rb}yCgfa}oaOrAEazg1TsI|L z4pc*m2@e#`2~deSS0;2RweXgbV80E`%2j9`8}2DhL31xj7E9({giTIwt0_ic4Ny_{ zrdTC|;t)@Gi`EZ4l$7W?3Qu68G zCqYSQhYRsw8@lYmH~4(03Qj}|1$5{QK1Lo4E?g(ArvF<80TM zsB%_%L!CAeO}pfEt&P184(LK4+(!Uwb&2eUq;jAP2{~Bq4E;#!iAe#)Z&nw6H%TX*)71$kYP(B+81~DU1PK zw2i$1W};2tO$eYXI+1;eROM)eKV1$okqsGRL#JnD0 Date: Fri, 24 May 2019 14:25:35 +0200 Subject: [PATCH 16/19] Backup old repositories file --- .../update/XmlRepositoryV1UpdateStep.java | 17 ++++++++++- .../update/XmlRepositoryV1UpdateStepTest.java | 30 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java index 9f79287a19..813d42818f 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStep.java @@ -4,6 +4,7 @@ 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; @@ -22,10 +23,11 @@ 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.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -100,10 +102,23 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep { 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( diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java index 8c0f4bb103..773d7bd447 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/XmlRepositoryV1UpdateStepTest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContextProvider; @@ -190,10 +189,18 @@ class XmlRepositoryV1UpdateStepTest { } @Test - void shouldFailForMissingMigrationStrategy() throws JAXBException { + 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 @@ -202,12 +209,27 @@ class XmlRepositoryV1UpdateStepTest { } @Test - void shouldNotFailIfFormerV1DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException { + 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")); - updateStep.doUpdate(); } private Optional findByNamespace(String namespace) { From e8c37f3a5016859cff387042e24981e5eb6773e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 28 May 2019 08:12:31 +0200 Subject: [PATCH 17/19] Add doc --- .../repository/update/XmlRepositoryFileNameUpdateStep.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java index aa6641f257..765f3c6317 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/XmlRepositoryFileNameUpdateStep.java @@ -16,6 +16,11 @@ import java.nio.file.Path; import static sonia.scm.version.Version.parse; +/** + * Moves an existing repositories.xml file to repository-paths.xml. + * Note that this has to run after an old v1 repository database has been migrated to v2 + * (see {@link XmlRepositoryV1UpdateStep}). + */ @Extension public class XmlRepositoryFileNameUpdateStep implements UpdateStep { From 62447139700a86bb61a7b9bedfb276c519a74957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 3 Jun 2019 13:47:05 +0200 Subject: [PATCH 18/19] Move constant to interface --- .../scm/repository/AbstractSimpleRepositoryHandler.java | 3 --- .../sonia/scm/repository/RepositoryDirectoryHandler.java | 2 ++ .../java/sonia/scm/repository/GitRepositoryHandlerTest.java | 2 +- .../java/sonia/scm/repository/HgRepositoryHandlerTest.java | 2 +- .../java/sonia/scm/repository/SvnRepositoryHandlerTest.java | 2 +- .../scm/repository/SimpleRepositoryHandlerTestBase.java | 2 +- .../sonia/scm/repository/update/CopyMigrationStrategy.java | 4 ++-- .../sonia/scm/repository/update/InlineMigrationStrategy.java | 5 ++--- .../sonia/scm/repository/update/MoveMigrationStrategy.java | 4 ++-- 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 1335001cdc..62dfd476af 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -59,9 +59,6 @@ public abstract class AbstractSimpleRepositoryHandler Date: Mon, 3 Jun 2019 12:49:03 +0000 Subject: [PATCH 19/19] Close branch feature/migrate_repository_v1