mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-11 14:23:46 +02:00
Feature repository specific data migration (#1526)
This adds a new migration mechanism for repository data. Instead of using UpdateSteps for all data migrations, repository data shall from now on be implemented with RepositoryUpdateSteps. The general logic stays the same. Executed updates are stored with the repository. Doing this, we can now execute updates on imported repositories without touching other data. This way we can import repositories even though they were exported with older versions of SCM-Manager or a plugin.
This commit is contained in:
@@ -45,7 +45,9 @@ Wechseln Sie über den Schalter oben rechts auf die Importseite und füllen Sie
|
||||
|
||||
Das gewählte Repository wird zum SCM-Manager hinzugefügt und sämtliche Repository Daten inklusive aller Branches und Tags werden importiert.
|
||||
Zusätzlich zum normalen Repository Import gibt es die Möglichkeit ein Repository Archiv mit Metadaten zu importieren.
|
||||
Dieses Repository Archiv muss von einem anderen SCM-Manager exportiert worden sein und wird beim Importieren auf Kompatibilität der Daten überprüft.
|
||||
Dieses Repository Archiv muss von einem anderen SCM-Manager exportiert worden sein und wird vor dem Import auf
|
||||
Kompatibilität der Daten überprüft (der SCM-Manager und alle installierten Plugins müssen mindestens die Version des
|
||||
exportierenden Systems haben).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ Das Ausgabeformat des Repository kann über die angebotenen Optionen verändert
|
||||
* `Standard`: Werden keine Optionen ausgewählt, wird das Repository im Standard Format exportiert.
|
||||
Git und Mercurial werden dabei als `Tar Archiv` exportiert und Subversion nutzt das `Dump` Format.
|
||||
* `Komprimieren`: Das Ausgabeformat wird zusätzlich mit `GZip` komprimiert, um die Dateigröße zu verringern.
|
||||
* `Mit Metadaten`: Statt dem Standard-Format wird ein Repository Archiv exportiert, welches außer dem Repository noch weitere Metadaten enthält.
|
||||
* `Mit Metadaten`: Statt dem Standard-Format wird ein Repository Archiv exportiert, welches außer dem Repository noch
|
||||
weitere Metadaten enthält. Für diesen Export sollte sichergestellt werden, dass alle installierten Plugins aktuell sind.
|
||||
Ein Import eines so exportierten Repositories ist nur in einem SCM-Manager mit derselben oder einer neueren Version
|
||||
möglich. Dieses gilt ebenso für alle installierten Plugins.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@ Just use the Switcher on top right to navigate to the import page and fill the i
|
||||
|
||||
Your repository will be added to SCM-Manager and all repository data including all branches and tags will be imported.
|
||||
In addition to the normal repository import, there is the possibility to import a repository archive with metadata.
|
||||
This repository archive must have been exported from another SCM manager and is checked for data compatibility during import.
|
||||
This repository archive must have been exported from another SCM-Manager and is checked for data compatibility before
|
||||
import (the SCM-Manager and all its installed plugins have to have at least the versions of the system the export has
|
||||
been created on).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ The output format of the repository can be changed via the offered options:
|
||||
* `Standard`: If no options are selected, the repository will be exported in the standard format.
|
||||
Git and Mercurial are exported as `Tar archive` and Subversion uses the `Dump` format.
|
||||
* `Compress`: The output format is additionally compressed with `GZip` to reduce the file size.
|
||||
* `With metadata`: Instead of the standard format a repository archive is exported, which contains additional metadata besides the repository.
|
||||
* `With metadata`: Instead of the standard format, a repository archive is exported, which contains additional metadata
|
||||
besides the repository. When you use this, please make sure all installed plugins are up to date. An import of
|
||||
such an export is possible only in an SCM-Manager with the same or a newer version. The same is valid for all
|
||||
installed plugins.
|
||||
|
||||

|
||||
|
||||
|
||||
2
gradle/changelog/repository_specific_migration.yaml
Normal file
2
gradle/changelog/repository_specific_migration.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Repository data can be migrated independently to enable the import of dumps from older versions ([#1526](https://github.com/scm-manager/scm-manager/pull/1526))
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.migration;
|
||||
|
||||
/**
|
||||
* Data for the repository, whose data that should be migrated.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public final class RepositoryUpdateContext {
|
||||
|
||||
private final String repositoryId;
|
||||
|
||||
public RepositoryUpdateContext(String repositoryId) {
|
||||
this.repositoryId = repositoryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the repository, whose data should be migrated.
|
||||
*/
|
||||
public String getRepositoryId() {
|
||||
return repositoryId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.migration;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
/**
|
||||
* This is the main interface for "repository specific" data migration/update. Using this interface, SCM-Manager
|
||||
* provides the possibility to change data structures between versions for a given type of data. This class should be
|
||||
* used only for repository specific data (eg. the store is created with a call of <code>forRepository</code> in a store
|
||||
* factory. To migrate global data, use a {@link UpdateStep}.
|
||||
* <p>For information about {@link #getAffectedDataType()} and {@link #getTargetVersion()}, see the package
|
||||
* documentation.</p>
|
||||
*
|
||||
* @see sonia.scm.migration
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@ExtensionPoint
|
||||
public interface RepositoryUpdateStep extends UpdateStepTarget {
|
||||
/**
|
||||
* Implement this to update the data to the new version for a specific repository. If any {@link Exception} is thrown,
|
||||
* SCM-Manager will not start up.
|
||||
*
|
||||
* @param repositoryUpdateContext A context providing specifics about the repository, whose data should be migrated
|
||||
* (eg. it id).
|
||||
*/
|
||||
@SuppressWarnings("java:S112") // we suppress this one, because an implementation should feel free to throw any exception it deems necessary
|
||||
void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception;
|
||||
}
|
||||
@@ -25,85 +25,24 @@
|
||||
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.
|
||||
* <p>The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for
|
||||
* example
|
||||
* <ul>
|
||||
* <li><code>com.example.myPlugin.configuration</code></li> for data in plugins, or
|
||||
* <li><code>com.cloudogu.scm.repository</code></li> for core data structures.
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>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 <i>A</i> has to run <b>before</b> another step for data type
|
||||
* <i>B</i>, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}.
|
||||
* </p>
|
||||
* <p>The algorithm looks something like this:<br>
|
||||
* Whenever the SCM-Manager starts,
|
||||
* <ul>
|
||||
* <li>it creates a so called <i>bootstrap guice context</i>, that contains
|
||||
* <ul>
|
||||
* <li>a {@link sonia.scm.security.KeyGenerator},</li>
|
||||
* <li>the {@link sonia.scm.repository.RepositoryLocationResolver},</li>
|
||||
* <li>an {@link sonia.scm.update.RepositoryUpdateIterator},</li>
|
||||
* <li>the {@link sonia.scm.io.FileSystem},</li>
|
||||
* <li>the {@link sonia.scm.security.CipherHandler},</li>
|
||||
* <li>a {@link sonia.scm.store.ConfigurationStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.ConfigurationEntryStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.DataStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.BlobStoreFactory}, and</li>
|
||||
* <li>the {@link sonia.scm.plugin.PluginLoader}.</li>
|
||||
* </ul>
|
||||
* Mind, that there are no DAOs, Managers or the like available at this time!
|
||||
* </li>
|
||||
* <li>It then checks whether there are instances of this interface that have not run before, that is either
|
||||
* <ul>
|
||||
* <li>their version number given by {@link #getTargetVersion()} is bigger than the last recorded target version of an
|
||||
* executed update step for the data type given by {@link #getAffectedDataType()}, or
|
||||
* </li>
|
||||
* <li>there is no version number known for the given data type.
|
||||
* </li>
|
||||
* </ul>
|
||||
* These are the <i>relevant</i> update steps.
|
||||
* </li>
|
||||
* <li>These relevant update steps are then sorted ascending by their target version given by
|
||||
* {@link #getTargetVersion()}.
|
||||
* </li>
|
||||
* <li>Finally, these sorted steps are executed one after another calling {@link #doUpdate()} of each step, updating the
|
||||
* version for the data type accordingly.
|
||||
* </li>
|
||||
* <li>If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.</li>
|
||||
* <li>If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will
|
||||
* not record the version number of this update step.
|
||||
* </li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>Mind that an implementation of this class has to be annotated with {@link sonia.scm.plugin.Extension}, so that the
|
||||
* step will be found. </p>
|
||||
* This is the main interface for "global" data migration/update. Using this interface, SCM-Manager provides the
|
||||
* possibility to change data structures between versions for a given type of data. This class should be used only
|
||||
* for global data, that is not repository specific (eg. the store is created without a call of
|
||||
* <code>forRepository</code> in a store factory. To migrate repository specific data, use a
|
||||
* {@link RepositoryUpdateStep}.
|
||||
* <p>For information about {@link #getAffectedDataType()} and {@link #getTargetVersion()}, see the package
|
||||
* documentation.</p>
|
||||
*
|
||||
* @see sonia.scm.migration
|
||||
*/
|
||||
@ExtensionPoint
|
||||
public interface UpdateStep {
|
||||
public interface UpdateStep extends UpdateStepTarget {
|
||||
/**
|
||||
* Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not
|
||||
* start up.
|
||||
*/
|
||||
@SuppressWarnings("java:S112") // we suppress this one, because an implementation should feel free to throw any exception it deems necessary
|
||||
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
|
||||
* <code>com.example.myPlugin.configuration</code>.
|
||||
*/
|
||||
String getAffectedDataType();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.migration;
|
||||
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
/**
|
||||
* Base class for update steps, telling the target version and the affected data type.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public interface UpdateStepTarget {
|
||||
/**
|
||||
* 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
|
||||
* <code>com.example.myPlugin.configuration</code>.
|
||||
*/
|
||||
String getAffectedDataType();
|
||||
}
|
||||
96
scm-core/src/main/java/sonia/scm/migration/package-info.java
Normal file
96
scm-core/src/main/java/sonia/scm/migration/package-info.java
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This package holds classes for data migrations between different versions of SCM-Manager or its plugins. The central
|
||||
* classes for this are {@link sonia.scm.migration.UpdateStep} and {@link sonia.scm.migration.RepositoryUpdateStep}.
|
||||
* Implementations of these classes tell, what data they migrate
|
||||
* ({@link sonia.scm.migration.UpdateStepTarget#getAffectedDataType()}) and to what version this data type is migrated
|
||||
* ({@link sonia.scm.migration.UpdateStepTarget#getTargetVersion()}).
|
||||
* <p>The data type provided by {@link sonia.scm.migration.UpdateStepTarget#getAffectedDataType()} can be an arbitrary
|
||||
* string, but it is considered a best practice to use a qualified name, for example
|
||||
* <ul>
|
||||
* <li><code>com.example.myPlugin.configuration</code></li> for data in plugins, or
|
||||
* <li><code>com.cloudogu.scm.repository</code></li> for core data structures.
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>The version provided by {@link sonia.scm.migration.UpdateStepTarget#getTargetVersion()} 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 <i>A</i> has to run <b>before</b> another step for data type
|
||||
* <i>B</i>, the version number of the second step has to be greater in regards to
|
||||
* {@link sonia.scm.version.Version#compareTo(Version)}.
|
||||
* </p>
|
||||
* <p>The algorithm looks something like this:<br>
|
||||
* Whenever the SCM-Manager starts,
|
||||
* <ul>
|
||||
* <li>it creates a so called <i>bootstrap guice context</i>, that contains
|
||||
* <ul>
|
||||
* <li>a {@link sonia.scm.security.KeyGenerator},</li>
|
||||
* <li>the {@link sonia.scm.repository.RepositoryLocationResolver},</li>
|
||||
* <li>an {@link sonia.scm.update.RepositoryUpdateIterator},</li>
|
||||
* <li>the {@link sonia.scm.io.FileSystem},</li>
|
||||
* <li>the {@link sonia.scm.security.CipherHandler},</li>
|
||||
* <li>a {@link sonia.scm.store.ConfigurationStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.ConfigurationEntryStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.DataStoreFactory},</li>
|
||||
* <li>a {@link sonia.scm.store.BlobStoreFactory}, and</li>
|
||||
* <li>the {@link sonia.scm.plugin.PluginLoader}.</li>
|
||||
* </ul>
|
||||
* Mind, that there are no DAOs, Managers or the like available at this time!
|
||||
* </li>
|
||||
* <li>It then checks whether there are instances of this interface that have not run before, that is either
|
||||
* <ul>
|
||||
* <li>their version number given by {@link sonia.scm.migration.UpdateStepTarget#getTargetVersion()} is bigger than the
|
||||
* last recorded target version of an executed update step for the data type given by
|
||||
* {@link sonia.scm.migration.UpdateStepTarget#getAffectedDataType()}, or
|
||||
* </li>
|
||||
* <li>there is no version number known for the given data type.
|
||||
* </li>
|
||||
* </ul>
|
||||
* These are the <i>relevant</i> update steps.
|
||||
* </li>
|
||||
* <li>These relevant update steps are then sorted ascending by their target version given by
|
||||
* {@link sonia.scm.migration.UpdateStepTarget#getTargetVersion()}.
|
||||
* </li>
|
||||
* <li>Finally, these sorted steps are executed one after another calling
|
||||
* {@link sonia.scm.migration.UpdateStep#doUpdate()} of each global step and
|
||||
* {@link sonia.scm.migration.RepositoryUpdateStep#doUpdate(sonia.scm.migration.RepositoryUpdateContext)}</li> for each
|
||||
* repository for repository specific update steps, updating the version for the data type accordingly (if there are
|
||||
* both, global and repository specific update steps for a data type and target version, the global step is called
|
||||
* first), then
|
||||
* </li>
|
||||
* <li>If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.</li>
|
||||
* <li>If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will
|
||||
* not record the version number of this update step.
|
||||
* </li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>Mind that an implementation of this class has to be annotated with {@link sonia.scm.plugin.Extension}, so that the
|
||||
* step will be found. </p>
|
||||
*/
|
||||
package sonia.scm.migration;
|
||||
|
||||
import sonia.scm.version.Version;
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.noContext;
|
||||
|
||||
/**
|
||||
* This exception is thrown if the repository import fails.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public class IncompatibleEnvironmentForImportException extends ExceptionWithContext {
|
||||
|
||||
private static final String CODE = "5GSO9ZkzX1";
|
||||
|
||||
public IncompatibleEnvironmentForImportException() {
|
||||
super(noContext(), "Incompatible SCM-Manager environment. Cannot import file. See previous logs for more detail.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import sonia.scm.migration.UpdateException;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -39,4 +41,35 @@ public interface RepositoryUpdateIterator {
|
||||
* @since 2.13.0
|
||||
*/
|
||||
void forEachRepository(Consumer<String> repositoryIdConsumer);
|
||||
|
||||
/**
|
||||
* Equivalent to {@link #forEachRepository(Consumer)} with the difference, that you can throw exceptions in the given
|
||||
* update code, that will then be wrapped in a {@link UpdateException}.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
default void updateEachRepository(Updater updater) {
|
||||
forEachRepository(
|
||||
repositoryId -> {
|
||||
try {
|
||||
updater.update(repositoryId);
|
||||
} catch (Exception e) {
|
||||
throw new UpdateException("failed to update repository with id " + repositoryId, e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple callback with the id of an existing repository with the possibility to throw exceptions.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
interface Updater {
|
||||
/**
|
||||
* Implements the update logic for a single repository, denoted by its id.
|
||||
*/
|
||||
@SuppressWarnings("java:S112") // We explicitly want to allow arbitrary exceptions here
|
||||
void update(String repositoryId) throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -38,10 +38,13 @@ public class InMemoryConfigurationEntryStoreFactory implements ConfigurationEntr
|
||||
@Override
|
||||
public <T> ConfigurationEntryStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
String name = storeParameters.getName();
|
||||
if (storeParameters.getRepositoryId() != null) {
|
||||
name = name + "-" + storeParameters.getRepositoryId();
|
||||
}
|
||||
return get(name);
|
||||
}
|
||||
|
||||
public <T> InMemoryConfigurationEntryStore<T> get(String name) {
|
||||
return stores.computeIfAbsent(name, x -> new InMemoryConfigurationEntryStore());
|
||||
return stores.computeIfAbsent(name, x -> new InMemoryConfigurationEntryStore<T>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"fullImport": {
|
||||
"title": "SCM-Manager Repository Archiv",
|
||||
"helpText": "Wählen Sie das Repository Archiv aus. Das Archiv muss von einer SCM-Manager Instanz exportiert worden sein."
|
||||
"helpText": "Wählen Sie das Repository Archiv aus. Das Archiv muss von einer SCM-Manager Instanz exportiert worden sein. Diese Instanz und alle installierten Plugins müssen dieselbe oder eine neuere Version haben."
|
||||
},
|
||||
"pending": {
|
||||
"subtitle": "Repository wird importiert...",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"fullExport": {
|
||||
"label": "Mit Metadaten (Experimentell)",
|
||||
"helpText": "Zusätzlich zum Repository Dump werden Metadaten zum Repository und zur SCM-Instanz exportiert. Gespeicherte Passwörter funktionieren nicht bei einem Import in andere SCM-Manager Instanzen. Dieses Feature ist noch experimentell. Es sollte (noch) nicht für Backups genutzt werden!"
|
||||
"helpText": "Zusätzlich zum Repository Dump werden Metadaten zum Repository und zur SCM-Instanz exportiert. Installierte Plugins sollten nach Möglichkeit in der neuesten Version installiert sein. Gespeicherte Passwörter funktionieren nicht bei einem Import in andere SCM-Manager Instanzen. Dieses Feature ist noch experimentell. Es sollte (noch) nicht für Backups genutzt werden!"
|
||||
},
|
||||
"exportButton": "Repository exportieren",
|
||||
"exportStarted": "Der Repository Export wurde gestartet. Abhängig von der Größe des Repository kann dies einige Momente dauern."
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"fullImport": {
|
||||
"title": "SCM-Manager Repository Archive",
|
||||
"helpText": "Select the archive file which should be imported. The archive must have been exported from an SCM-Manager instance."
|
||||
"helpText": "Select the archive file which should be imported. The archive must have been exported from an SCM-Manager instance. This instance and all installed plugins have to be of the same or a newer version."
|
||||
},
|
||||
"pending": {
|
||||
"subtitle": "Importing Repository...",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"fullExport": {
|
||||
"label": "With metadata (Experimental)",
|
||||
"helpText": "In addition to the repository dump, metadata about the repository and SCM instance is exported. Stored passwords will not work if this is imported in other instances of SCM-Manager. This feature is still experimental. Do not use this as a backup mechanism (yet)!"
|
||||
"helpText": "In addition to the repository dump, metadata about the repository and SCM instance is exported. If possible, ensure that installed plugins are up to date. However, stored passwords will not work if this is imported in other instances of SCM-Manager. This feature is still experimental. Do not use this as a backup mechanism (yet)!"
|
||||
},
|
||||
"exportButton": "Export Repository",
|
||||
"exportStarted": "The repository export was started. Depending on the repository size this may take a while."
|
||||
|
||||
@@ -33,8 +33,10 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.IncompatibleEnvironmentForImportException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.update.UpdateEngine;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXB;
|
||||
@@ -51,22 +53,26 @@ import static sonia.scm.importexport.FullScmRepositoryExporter.STORE_DATA_FILE_N
|
||||
|
||||
public class FullScmRepositoryImporter {
|
||||
|
||||
@SuppressWarnings("java:S115") // we like this name here
|
||||
private static final int _1_MB = 1000000;
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final ScmEnvironmentCompatibilityChecker compatibilityChecker;
|
||||
private final TarArchiveRepositoryStoreImporter storeImporter;
|
||||
private final UpdateEngine updateEngine;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryImporter(RepositoryServiceFactory serviceFactory,
|
||||
RepositoryManager repositoryManager,
|
||||
ScmEnvironmentCompatibilityChecker compatibilityChecker,
|
||||
TarArchiveRepositoryStoreImporter storeImporter) {
|
||||
TarArchiveRepositoryStoreImporter storeImporter,
|
||||
UpdateEngine updateEngine) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.compatibilityChecker = compatibilityChecker;
|
||||
this.storeImporter = storeImporter;
|
||||
this.updateEngine = updateEngine;
|
||||
}
|
||||
|
||||
public Repository importFromStream(Repository repository, InputStream inputStream) {
|
||||
@@ -113,6 +119,7 @@ public class FullScmRepositoryImporter {
|
||||
// Inside the repository tar archive stream is another tar archive.
|
||||
// The nested tar archive is wrapped in another TarArchiveInputStream inside the storeImporter
|
||||
storeImporter.importFromTarArchive(repository, tais);
|
||||
updateEngine.update(repository.getId());
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
@@ -148,10 +155,7 @@ public class FullScmRepositoryImporter {
|
||||
if (environmentEntry.getName().equals(SCM_ENVIRONMENT_FILE_NAME) && !environmentEntry.isDirectory() && environmentEntry.getSize() < _1_MB) {
|
||||
boolean validEnvironment = compatibilityChecker.check(JAXB.unmarshal(new NoneClosingInputStream(tais), ScmEnvironment.class));
|
||||
if (!validEnvironment) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.noContext(),
|
||||
"Incompatible SCM-Manager environment. Could not import file."
|
||||
);
|
||||
throw new IncompatibleEnvironmentForImportException();
|
||||
}
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
|
||||
@@ -29,9 +29,9 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -54,10 +54,10 @@ public class ScmEnvironmentCompatibilityChecker {
|
||||
}
|
||||
|
||||
private boolean isCoreVersionCompatible(String currentCoreVersion, String coreVersionFromImport) {
|
||||
boolean compatible = currentCoreVersion.equals(coreVersionFromImport);
|
||||
boolean compatible = Version.parse(currentCoreVersion).isNewerOrEqual(coreVersionFromImport);
|
||||
if (!compatible) {
|
||||
LOG.info(
|
||||
"SCM-Manager version is not compatible with dump. Dump can only be imported with SCM-Manager version: {}; you are running version {}",
|
||||
"SCM-Manager version is not compatible with dump. Dump can only be imported with SCM-Manager version {} or newer; you are running version {}.",
|
||||
coreVersionFromImport,
|
||||
currentCoreVersion
|
||||
);
|
||||
@@ -73,9 +73,9 @@ public class ScmEnvironmentCompatibilityChecker {
|
||||
|
||||
for (EnvironmentPluginDescriptor plugin : environment.getPlugins().getPlugin()) {
|
||||
Optional<PluginInformation> matchingInstalledPlugin = findMatchingInstalledPlugin(currentlyInstalledPlugins, plugin);
|
||||
if (isPluginIncompatible(plugin, matchingInstalledPlugin)) {
|
||||
if (matchingInstalledPlugin.isPresent() && isPluginIncompatible(plugin, matchingInstalledPlugin.get())) {
|
||||
LOG.info(
|
||||
"The installed plugin \"{}\" with version \"{}\" doesn't match the plugin data version \"{}\" from the SCM-Manager environment the dump was created with.",
|
||||
"The installed plugin \"{}\" with version \"{}\" is older than the plugin data version \"{}\" from the SCM-Manager environment the dump was created with. Please update the plugin.",
|
||||
matchingInstalledPlugin.get().getName(),
|
||||
matchingInstalledPlugin.get().getVersion(),
|
||||
plugin.getVersion()
|
||||
@@ -86,8 +86,8 @@ public class ScmEnvironmentCompatibilityChecker {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isPluginIncompatible(EnvironmentPluginDescriptor plugin, Optional<PluginInformation> matchingInstalledPlugin) {
|
||||
return matchingInstalledPlugin.isPresent() && isPluginVersionIncompatible(plugin.getVersion(), matchingInstalledPlugin.get().getVersion());
|
||||
private boolean isPluginIncompatible(EnvironmentPluginDescriptor plugin, PluginInformation matchingInstalledPlugin) {
|
||||
return isPluginVersionIncompatible(plugin.getVersion(), matchingInstalledPlugin.getVersion());
|
||||
}
|
||||
|
||||
private Optional<PluginInformation> findMatchingInstalledPlugin(List<PluginInformation> currentlyInstalledPlugins, EnvironmentPluginDescriptor plugin) {
|
||||
@@ -98,6 +98,6 @@ public class ScmEnvironmentCompatibilityChecker {
|
||||
}
|
||||
|
||||
private boolean isPluginVersionIncompatible(String previousPluginVersion, String installedPluginVersion) {
|
||||
return !installedPluginVersion.equals(previousPluginVersion);
|
||||
return Version.parse(installedPluginVersion).isOlder(previousPluginVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.lifecycle.modules;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import sonia.scm.migration.RepositoryUpdateStep;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
@@ -40,9 +41,14 @@ public class UpdateStepModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder<UpdateStep> updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
|
||||
Multibinder<RepositoryUpdateStep> repositoryUdateStepBinder = Multibinder.newSetBinder(binder(), RepositoryUpdateStep.class);
|
||||
pluginLoader
|
||||
.getExtensionProcessor()
|
||||
.byExtensionPoint(UpdateStep.class)
|
||||
.forEach(stepClass -> updateStepBinder.addBinding().to(stepClass));
|
||||
pluginLoader
|
||||
.getExtensionProcessor()
|
||||
.byExtensionPoint(RepositoryUpdateStep.class)
|
||||
.forEach(stepClass -> repositoryUdateStepBinder.addBinding().to(stepClass));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,22 +21,28 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.migration.RepositoryUpdateContext;
|
||||
import sonia.scm.migration.RepositoryUpdateStep;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.migration.UpdateStepTarget;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.stream.Stream.concat;
|
||||
|
||||
public class UpdateEngine {
|
||||
|
||||
@@ -44,56 +50,84 @@ public class UpdateEngine {
|
||||
|
||||
private static final String STORE_NAME = "executedUpdates";
|
||||
|
||||
private final List<UpdateStep> steps;
|
||||
private final List<UpdateStepWrapper> steps;
|
||||
private final ConfigurationEntryStore<UpdateVersionInfo> store;
|
||||
private final ConfigurationEntryStoreFactory storeFactory;
|
||||
private final RepositoryUpdateIterator repositoryUpdateIterator;
|
||||
|
||||
@Inject
|
||||
public UpdateEngine(Set<UpdateStep> steps, ConfigurationEntryStoreFactory storeFactory) {
|
||||
this.steps = sortSteps(steps);
|
||||
public UpdateEngine(
|
||||
Set<UpdateStep> globalSteps,
|
||||
Set<RepositoryUpdateStep> repositorySteps,
|
||||
ConfigurationEntryStoreFactory storeFactory,
|
||||
RepositoryUpdateIterator repositoryUpdateIterator
|
||||
) {
|
||||
this.storeFactory = storeFactory;
|
||||
this.repositoryUpdateIterator = repositoryUpdateIterator;
|
||||
this.store = storeFactory.withType(UpdateVersionInfo.class).withName(STORE_NAME).build();
|
||||
this.steps = sortSteps(globalSteps, repositorySteps);
|
||||
}
|
||||
|
||||
private List<UpdateStep> sortSteps(Set<UpdateStep> steps) {
|
||||
private List<UpdateStepWrapper> sortSteps(Set<UpdateStep> globalSteps, Set<RepositoryUpdateStep> repositorySteps) {
|
||||
LOG.trace("sorting available update steps:");
|
||||
List<UpdateStep> sortedSteps = steps.stream()
|
||||
List<UpdateStepWrapper> sortedSteps =
|
||||
concat(
|
||||
globalSteps.stream().filter(this::notRunYet).map(GlobalUpdateStepWrapper::new),
|
||||
repositorySteps.stream().map(RepositoryUpdateStepWrapper::new))
|
||||
.sorted(
|
||||
Comparator
|
||||
.comparing(UpdateStep::getTargetVersion)
|
||||
.thenComparing(this::isCoreUpdateStep)
|
||||
.reversed())
|
||||
.comparing(UpdateStepWrapper::getTargetVersion)
|
||||
.thenComparing(UpdateStepWrapper::isGlobalUpdateStep)
|
||||
.thenComparing(UpdateEngine::isCoreUpdateStep)
|
||||
.reversed()
|
||||
)
|
||||
.collect(toList());
|
||||
sortedSteps.forEach(step -> LOG.trace("{} for version {}", step.getAffectedDataType(), step.getTargetVersion()));
|
||||
return sortedSteps;
|
||||
}
|
||||
|
||||
private boolean isCoreUpdateStep(UpdateStep updateStep) {
|
||||
return updateStep instanceof CoreUpdateStep;
|
||||
private static boolean isCoreUpdateStep(UpdateStepWrapper updateStep) {
|
||||
return updateStep.isCoreUpdate();
|
||||
}
|
||||
|
||||
public void update() {
|
||||
steps
|
||||
.stream()
|
||||
.filter(this::notRunYet)
|
||||
.forEach(this::execute);
|
||||
steps.forEach(this::execute);
|
||||
}
|
||||
|
||||
private void execute(UpdateStep updateStep) {
|
||||
public void update(String repositoryId) {
|
||||
steps.forEach(step -> execute(step, repositoryId));
|
||||
}
|
||||
|
||||
private void execute(UpdateStepWrapper updateStep) {
|
||||
try {
|
||||
LOG.info("running update step for type {} and version {} (class {})",
|
||||
updateStep.getAffectedDataType(),
|
||||
updateStep.getTargetVersion(),
|
||||
updateStep.getClass().getName()
|
||||
);
|
||||
updateStep.doUpdate();
|
||||
} catch (Exception e) {
|
||||
throw new UpdateException(
|
||||
String.format(
|
||||
format(
|
||||
"could not execute update for type %s to version %s in %s",
|
||||
updateStep.getAffectedDataType(),
|
||||
updateStep.getTargetVersion(),
|
||||
updateStep.getClass()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void execute(UpdateStepWrapper updateStep, String repositoryId) {
|
||||
try {
|
||||
updateStep.doUpdate(repositoryId);
|
||||
} catch (Exception e) {
|
||||
throw new UpdateException(
|
||||
format(
|
||||
"could not execute update for type %s to version %s in %s for repository id %s",
|
||||
updateStep.getAffectedDataType(),
|
||||
updateStep.getTargetVersion(),
|
||||
updateStep.getClass(),
|
||||
repositoryId),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeNewVersion(ConfigurationEntryStore<UpdateVersionInfo> store, UpdateStepTarget updateStep) {
|
||||
UpdateVersionInfo newVersionInfo = new UpdateVersionInfo(updateStep.getTargetVersion().getParsedVersion());
|
||||
store.put(updateStep.getAffectedDataType(), newVersionInfo);
|
||||
}
|
||||
@@ -103,6 +137,10 @@ public class UpdateEngine {
|
||||
updateStep.getAffectedDataType(),
|
||||
updateStep.getTargetVersion()
|
||||
);
|
||||
return notRunYet(this.store, updateStep);
|
||||
}
|
||||
|
||||
private boolean notRunYet(ConfigurationEntryStore<UpdateVersionInfo> store, UpdateStepTarget updateStep) {
|
||||
UpdateVersionInfo updateVersionInfo = store.get(updateStep.getAffectedDataType());
|
||||
if (updateVersionInfo == null) {
|
||||
LOG.trace("no updates for type {} run yet; step will be executed", updateStep.getAffectedDataType());
|
||||
@@ -116,4 +154,128 @@ public class UpdateEngine {
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private abstract static class UpdateStepWrapper implements UpdateStepTarget {
|
||||
|
||||
private final UpdateStepTarget delegate;
|
||||
|
||||
protected UpdateStepWrapper(UpdateStepTarget delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return delegate.getTargetVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return delegate.getAffectedDataType();
|
||||
}
|
||||
|
||||
abstract boolean isGlobalUpdateStep();
|
||||
|
||||
abstract boolean isCoreUpdate();
|
||||
|
||||
@SuppressWarnings("java:S112") // we explicitly want to allow all kinds of exceptions here
|
||||
abstract void doUpdate() throws Exception;
|
||||
|
||||
@SuppressWarnings("java:S112") // we explicitly want to allow all kinds of exceptions here
|
||||
abstract void doUpdate(String repositoryId) throws Exception;
|
||||
}
|
||||
|
||||
private class GlobalUpdateStepWrapper extends UpdateStepWrapper {
|
||||
private final UpdateStep delegate;
|
||||
private final boolean coreUpdate;
|
||||
|
||||
protected GlobalUpdateStepWrapper(UpdateStep delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
this.coreUpdate = delegate instanceof CoreUpdateStep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return delegate.getTargetVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return delegate.getAffectedDataType();
|
||||
}
|
||||
|
||||
public boolean isCoreUpdate() {
|
||||
return coreUpdate;
|
||||
}
|
||||
|
||||
public boolean isGlobalUpdateStep() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void doUpdate() throws Exception {
|
||||
LOG.info("running update step for type {} and version {} (class {})",
|
||||
delegate.getAffectedDataType(),
|
||||
delegate.getTargetVersion(),
|
||||
delegate.getClass().getName()
|
||||
);
|
||||
delegate.doUpdate();
|
||||
storeNewVersion(store, delegate);
|
||||
}
|
||||
|
||||
void doUpdate(String repositoryId) {
|
||||
// nothing to do for repositories here
|
||||
}
|
||||
}
|
||||
|
||||
private class RepositoryUpdateStepWrapper extends UpdateStepWrapper {
|
||||
|
||||
private final RepositoryUpdateStep delegate;
|
||||
|
||||
public RepositoryUpdateStepWrapper(RepositoryUpdateStep delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobalUpdateStep() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isCoreUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
void doUpdate() {
|
||||
repositoryUpdateIterator.updateEachRepository(this::doUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
void doUpdate(String repositoryId) throws Exception {
|
||||
if (notRunYet(repositoryId)) {
|
||||
LOG.info("running update step for type {} and version {} (class {}) for repository id {}",
|
||||
delegate.getAffectedDataType(),
|
||||
delegate.getTargetVersion(),
|
||||
delegate.getClass().getName(),
|
||||
repositoryId
|
||||
);
|
||||
delegate.doUpdate(new RepositoryUpdateContext(repositoryId));
|
||||
storeNewVersion(storeForRepository(repositoryId), delegate);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean notRunYet(String repositoryId) {
|
||||
LOG.trace("checking whether to run update step for type {} and version {} on repository id {}",
|
||||
delegate.getAffectedDataType(),
|
||||
delegate.getTargetVersion(),
|
||||
repositoryId
|
||||
);
|
||||
return UpdateEngine.this.notRunYet(storeForRepository(repositoryId), delegate);
|
||||
}
|
||||
|
||||
private ConfigurationEntryStore<UpdateVersionInfo> storeForRepository(String repositoryId) {
|
||||
return storeFactory.withType(UpdateVersionInfo.class).withName(STORE_NAME).forRepository(repositoryId).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +334,10 @@
|
||||
"8YR7aawFW1": {
|
||||
"displayName": "Aktuelles Passwort falsch",
|
||||
"description": "Das aktuelle Passwort ist falsch. Bitte versuchen Sie es noch einmal."
|
||||
},
|
||||
"5GSO9ZkzX1": {
|
||||
"displayName": "Inkompatible Umgebung",
|
||||
"description": "Die Version dieses SCM-Managers oder eines der installierten Plugins ist zu alt für den Import des Dumps. Bitte installieren Sie die neuesten Versionen. Nähere Informationen finden sich im Log."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -331,6 +331,10 @@
|
||||
"displayName": "Repository is being exported",
|
||||
"description": "The repository is being exported and therefore must not be modified."
|
||||
},
|
||||
"5GSO9ZkzX1": {
|
||||
"displayName": "Incompatible environment",
|
||||
"description": "The version of this SCM-Manager or one of its plugins is too old for the import of the given dump. Please update to the latest versions. See the log for more information."
|
||||
},
|
||||
"8YR7aawFW1": {
|
||||
"displayName": "Wrong current password",
|
||||
"description": "The current password is wrong. Please try again."
|
||||
|
||||
@@ -27,6 +27,7 @@ package sonia.scm.importexport;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.io.Resources;
|
||||
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.junit.jupiter.api.io.TempDir;
|
||||
@@ -39,9 +40,11 @@ import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.IncompatibleEnvironmentForImportException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.api.UnbundleCommandBuilder;
|
||||
import sonia.scm.update.UpdateEngine;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -49,16 +52,19 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
class FullScmRepositoryImporterTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold("svn");
|
||||
@@ -75,6 +81,8 @@ class FullScmRepositoryImporterTest {
|
||||
private ScmEnvironmentCompatibilityChecker compatibilityChecker;
|
||||
@Mock
|
||||
private TarArchiveRepositoryStoreImporter storeImporter;
|
||||
@Mock
|
||||
private UpdateEngine updateEngine;
|
||||
|
||||
@InjectMocks
|
||||
private FullScmRepositoryImporter fullImporter;
|
||||
@@ -89,29 +97,57 @@ class FullScmRepositoryImporterTest {
|
||||
void shouldNotImportRepositoryIfFileNotExists(@TempDir Path temp) throws IOException {
|
||||
File emptyFile = new File(temp.resolve("empty").toString());
|
||||
Files.touch(emptyFile);
|
||||
assertThrows(ImportFailedException.class, () -> fullImporter.importFromStream(REPOSITORY, new FileInputStream(emptyFile)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfScmEnvironmentIsIncompatible() {
|
||||
when(compatibilityChecker.check(any())).thenReturn(false);
|
||||
|
||||
FileInputStream inputStream = new FileInputStream(emptyFile);
|
||||
assertThrows(
|
||||
ImportFailedException.class,
|
||||
() -> fullImporter.importFromStream(REPOSITORY, Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream())
|
||||
() -> fullImporter.importFromStream(REPOSITORY, inputStream)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldImportScmRepositoryArchive() throws IOException {
|
||||
when(compatibilityChecker.check(any())).thenReturn(true);
|
||||
when(repositoryManager.create(eq(REPOSITORY), any())).thenReturn(REPOSITORY);
|
||||
void shouldFailIfScmEnvironmentIsIncompatible() throws IOException {
|
||||
when(compatibilityChecker.check(any())).thenReturn(false);
|
||||
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream());
|
||||
assertThat(repository).isEqualTo(REPOSITORY);
|
||||
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
|
||||
verify(repositoryManager).modify(REPOSITORY);
|
||||
Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions();
|
||||
assertThat(updatedPermissions).hasSize(2);
|
||||
InputStream importStream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
assertThrows(
|
||||
IncompatibleEnvironmentForImportException.class,
|
||||
() -> fullImporter.importFromStream(REPOSITORY, importStream)
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithValidEnvironment {
|
||||
|
||||
@BeforeEach
|
||||
void setUpEnvironment() {
|
||||
when(compatibilityChecker.check(any())).thenReturn(true);
|
||||
when(repositoryManager.create(eq(REPOSITORY), any())).thenAnswer(invocation -> {
|
||||
invocation.getArgument(1, Consumer.class).accept(REPOSITORY);
|
||||
return REPOSITORY;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldImportScmRepositoryArchive() throws IOException {
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, stream);
|
||||
|
||||
assertThat(repository).isEqualTo(REPOSITORY);
|
||||
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
|
||||
verify(repositoryManager).modify(REPOSITORY);
|
||||
Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions();
|
||||
assertThat(updatedPermissions).hasSize(2);
|
||||
verify(unbundleCommandBuilder).unbundle((InputStream) argThat(argument -> argument.getClass().equals(FullScmRepositoryImporter.NoneClosingInputStream.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTriggerUpdateForImportedRepository() throws IOException {
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
|
||||
fullImporter.importFromStream(REPOSITORY, stream);
|
||||
|
||||
verify(updateEngine).update(REPOSITORY.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -61,7 +62,7 @@ class ScmEnvironmentCompatibilityCheckerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrueIfEnvironmentIsCompatible() {
|
||||
void shouldReturnTrueIfEnvironmentIsSame() {
|
||||
when(scmContextProvider.getVersion()).thenReturn("2.0.0");
|
||||
ImmutableList<EnvironmentPluginDescriptor> plugins = ImmutableList.of(
|
||||
new EnvironmentPluginDescriptor("scm-first-plugin", "1.0.0"),
|
||||
@@ -74,6 +75,20 @@ class ScmEnvironmentCompatibilityCheckerTest {
|
||||
assertThat(compatible).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrueIfEnvironmentIsNewer() {
|
||||
when(scmContextProvider.getVersion()).thenReturn("2.1.0");
|
||||
ImmutableList<EnvironmentPluginDescriptor> plugins = ImmutableList.of(
|
||||
new EnvironmentPluginDescriptor("scm-first-plugin", "0.9.0"),
|
||||
new EnvironmentPluginDescriptor("scm-second-plugin", "1.0.1")
|
||||
);
|
||||
ScmEnvironment env = createScmEnvironment("2.0.0", "linux", "64", plugins);
|
||||
|
||||
boolean compatible = checker.check(env);
|
||||
|
||||
assertThat(compatible).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalseIfCoreVersionIncompatible() {
|
||||
when(scmContextProvider.getVersion()).thenReturn("2.0.0");
|
||||
@@ -124,5 +139,4 @@ class ScmEnvironmentCompatibilityCheckerTest {
|
||||
scmEnvironment.setPlugins(environmentPluginsDescriptor);
|
||||
return scmEnvironment;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.migration.RepositoryUpdateContext;
|
||||
import sonia.scm.migration.RepositoryUpdateStep;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
@@ -33,16 +36,35 @@ import sonia.scm.version.Version;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static java.util.Collections.singleton;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
class UpdateEngineTest {
|
||||
|
||||
ConfigurationEntryStoreFactory storeFactory = new InMemoryConfigurationEntryStoreFactory();
|
||||
RepositoryUpdateIterator repositoryUpdateIterator = mock(RepositoryUpdateIterator.class, CALLS_REAL_METHODS);
|
||||
|
||||
List<String> processedUpdates = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void mockRepositories() {
|
||||
doAnswer(invocation -> {
|
||||
Consumer<String> consumer = invocation.getArgument(0, Consumer.class);
|
||||
consumer.accept("42");
|
||||
consumer.accept("1337");
|
||||
return null;
|
||||
}).when(repositoryUpdateIterator).forEachRepository(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessStepsInCorrectOrder() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
@@ -51,13 +73,45 @@ class UpdateEngineTest {
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
assertThat(processedUpdates)
|
||||
.containsExactly("test:1.1.0", "test:1.1.1", "test:1.2.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessStepsInCorrectOrderWithRepositoryUpdates() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
|
||||
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
assertThat(processedUpdates)
|
||||
.containsExactly("test:1.1.0-42", "test:1.1.0-1337", "test:1.1.1-42", "test:1.1.1-1337", "test:1.2.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessStepsForSingleRepository() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
|
||||
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.0"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update("1337");
|
||||
|
||||
assertThat(processedUpdates)
|
||||
.containsExactly("test:1.1.0-1337", "test:1.1.1-1337");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessCoreStepsBeforeOther() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
@@ -65,7 +119,7 @@ class UpdateEngineTest {
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
updateSteps.add(new CoreFixedVersionUpdateStep("core", "1.2.0"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
assertThat(processedUpdates)
|
||||
@@ -73,41 +127,73 @@ class UpdateEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunStepsOnlyOnce() {
|
||||
void shouldProcessGlobalStepsBeforeRepository() {
|
||||
Set<UpdateStep> updateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
Set<RepositoryUpdateStep> repositoryUpdateSteps = singleton(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
assertThat(processedUpdates)
|
||||
.containsExactly("test:1.2.0", "test:1.2.0-42", "test:1.2.0-1337");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunGlobalStepsOnlyOnce() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
|
||||
updateEngine.update();
|
||||
UpdateEngine firstUpdateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
firstUpdateEngine.update();
|
||||
|
||||
processedUpdates.clear();
|
||||
|
||||
updateEngine.update();
|
||||
UpdateEngine secondUpdateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
secondUpdateEngine.update();
|
||||
|
||||
assertThat(processedUpdates).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunRepositoryStepsOnlyOnce() {
|
||||
LinkedHashSet<RepositoryUpdateStep> repositoryUpdateSteps = new LinkedHashSet<>();
|
||||
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
|
||||
|
||||
UpdateEngine firstUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
|
||||
firstUpdateEngine.update();
|
||||
|
||||
processedUpdates.clear();
|
||||
|
||||
repositoryUpdateSteps.add(new FixedVersionUpdateStep("test", "1.2.0"));
|
||||
UpdateEngine secondUpdateEngine = new UpdateEngine(emptySet(), repositoryUpdateSteps, storeFactory, repositoryUpdateIterator);
|
||||
secondUpdateEngine.update();
|
||||
|
||||
assertThat(processedUpdates)
|
||||
.containsExactly("test:1.2.0-42", "test:1.2.0-1337");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunStepsForDifferentTypesIndependently() {
|
||||
LinkedHashSet<UpdateStep> updateSteps = new LinkedHashSet<>();
|
||||
|
||||
updateSteps.add(new FixedVersionUpdateStep("test", "1.1.1"));
|
||||
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, storeFactory);
|
||||
UpdateEngine updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
processedUpdates.clear();
|
||||
|
||||
updateSteps.add(new FixedVersionUpdateStep("other", "1.1.1"));
|
||||
|
||||
updateEngine = new UpdateEngine(updateSteps, storeFactory);
|
||||
updateEngine = new UpdateEngine(updateSteps, emptySet(), storeFactory, repositoryUpdateIterator);
|
||||
updateEngine.update();
|
||||
|
||||
assertThat(processedUpdates).containsExactly("other:1.1.1");
|
||||
}
|
||||
|
||||
class FixedVersionUpdateStep implements UpdateStep {
|
||||
class FixedVersionUpdateStep implements UpdateStep, RepositoryUpdateStep {
|
||||
private final String type;
|
||||
private final String version;
|
||||
|
||||
@@ -130,6 +216,11 @@ class UpdateEngineTest {
|
||||
public void doUpdate() {
|
||||
processedUpdates.add(type + ":" + version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception {
|
||||
processedUpdates.add(type + ":" + version + "-" + repositoryUpdateContext.getRepositoryId());
|
||||
}
|
||||
}
|
||||
|
||||
class CoreFixedVersionUpdateStep extends FixedVersionUpdateStep implements CoreUpdateStep {
|
||||
|
||||
Reference in New Issue
Block a user