mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merge with 2.0.0-m3
This commit is contained in:
14
pom.xml
14
pom.xml
@@ -409,8 +409,9 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.github.sdorra</groupId>
|
<groupId>com.github.sdorra</groupId>
|
||||||
<artifactId>buildfrontend-maven-plugin</artifactId>
|
<artifactId>buildfrontend-maven-plugin</artifactId>
|
||||||
<version>2.2.0</version>
|
<version>2.3.0</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-javadoc-plugin</artifactId>
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
@@ -432,6 +433,12 @@
|
|||||||
<artifactId>enunciate-maven-plugin</artifactId>
|
<artifactId>enunciate-maven-plugin</artifactId>
|
||||||
<version>${enunciate.version}</version>
|
<version>${enunciate.version}</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>sonia.scm.maven</groupId>
|
||||||
|
<artifactId>smp-maven-plugin</artifactId>
|
||||||
|
<version>1.0.0-alpha-4</version>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</pluginManagement>
|
</pluginManagement>
|
||||||
|
|
||||||
@@ -838,8 +845,8 @@
|
|||||||
<quartz.version>2.2.3</quartz.version>
|
<quartz.version>2.2.3</quartz.version>
|
||||||
|
|
||||||
<!-- frontend -->
|
<!-- frontend -->
|
||||||
<nodejs.version>8.11.4</nodejs.version>
|
<nodejs.version>10.16.0</nodejs.version>
|
||||||
<yarn.version>1.9.4</yarn.version>
|
<yarn.version>1.16.0</yarn.version>
|
||||||
|
|
||||||
<!-- build properties -->
|
<!-- build properties -->
|
||||||
<project.build.javaLevel>1.8</project.build.javaLevel>
|
<project.build.javaLevel>1.8</project.build.javaLevel>
|
||||||
@@ -855,7 +862,6 @@
|
|||||||
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
|
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
|
||||||
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
|
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
|
||||||
|
|
||||||
<node.version>8.11.4</node.version>
|
|
||||||
<sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable>
|
<sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,80 @@ package sonia.scm.migration;
|
|||||||
import sonia.scm.plugin.ExtensionPoint;
|
import sonia.scm.plugin.ExtensionPoint;
|
||||||
import sonia.scm.version.Version;
|
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>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>
|
||||||
|
*/
|
||||||
@ExtensionPoint
|
@ExtensionPoint
|
||||||
public interface UpdateStep {
|
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;
|
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();
|
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();
|
String getAffectedDataType();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,6 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
|
|||||||
|
|
||||||
public static final String DEFAULT_VERSION_INFORMATION = "unknown";
|
public static final String DEFAULT_VERSION_INFORMATION = "unknown";
|
||||||
|
|
||||||
public static final String DOT = ".";
|
|
||||||
static final String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the logger for AbstractSimpleRepositoryHandler
|
* the logger for AbstractSimpleRepositoryHandler
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import java.io.File;
|
|||||||
*/
|
*/
|
||||||
public interface RepositoryDirectoryHandler extends RepositoryHandler {
|
public interface RepositoryDirectoryHandler extends RepositoryHandler {
|
||||||
|
|
||||||
|
String REPOSITORIES_NATIVE_DIRECTORY = "data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current directory of the repository for the given id.
|
* Get the current directory of the repository for the given id.
|
||||||
* @return the current directory of the given repository
|
* @return the current directory of the given repository
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import static java.util.Collections.unmodifiableSet;
|
|||||||
* Custom role with specific permissions related to {@link Repository}.
|
* Custom role with specific permissions related to {@link Repository}.
|
||||||
* This object should be immutable, but could not be due to mapstruct.
|
* This object should be immutable, but could not be due to mapstruct.
|
||||||
*/
|
*/
|
||||||
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"read", "modify"})
|
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"write"})
|
||||||
@XmlRootElement(name = "roles")
|
@XmlRootElement(name = "roles")
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class RepositoryRole implements ModelObject, PermissionObject {
|
public class RepositoryRole implements ModelObject, PermissionObject {
|
||||||
@@ -121,7 +121,10 @@ public class RepositoryRole implements ModelObject, PermissionObject {
|
|||||||
* @return the hash code value for the {@link RepositoryRole}
|
* @return the hash code value for the {@link RepositoryRole}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode()
|
||||||
|
{
|
||||||
|
// Normally we do not have a log of repository permissions having the same size of verbs, but different content.
|
||||||
|
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
|
||||||
return Objects.hashCode(name, verbs == null? -1: verbs.size());
|
return Objects.hashCode(name, verbs == null? -1: verbs.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,24 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
|
|||||||
this.mail = mail;
|
this.mail = mail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs ...
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param displayName
|
||||||
|
* @param mail
|
||||||
|
*/
|
||||||
|
public User(String name, String displayName, String mail, String password, String type, boolean active)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.mail = mail;
|
||||||
|
this.password = password;
|
||||||
|
this.type = type;
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException;
|
|||||||
import sonia.scm.store.StoreConstants;
|
import sonia.scm.store.StoreConstants;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -28,9 +29,10 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
|||||||
*
|
*
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@Singleton
|
||||||
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
|
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
|
||||||
|
|
||||||
private static final String STORE_NAME = "repositories";
|
public static final String STORE_NAME = "repository-paths";
|
||||||
|
|
||||||
private final SCMContextProvider contextProvider;
|
private final SCMContextProvider contextProvider;
|
||||||
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
||||||
@@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
|||||||
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
|
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
|
PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
|
||||||
super(Path.class);
|
super(Path.class);
|
||||||
this.contextProvider = contextProvider;
|
this.contextProvider = contextProvider;
|
||||||
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
|
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
|
||||||
@@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
|||||||
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
|
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
|
||||||
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
|
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
this.read();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,10 +95,17 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void add(Repository repository) {
|
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();
|
Repository clone = repository.clone();
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
|
Path repositoryPath = (Path) location;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path metadataPath = resolveDataPath(repositoryPath);
|
Path metadataPath = resolveDataPath(repositoryPath);
|
||||||
@@ -111,10 +118,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
byId.put(repository.getId(), clone);
|
byId.put(repository.getId(), clone);
|
||||||
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
|
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean contains(Repository repository) {
|
public boolean contains(Repository repository) {
|
||||||
return byId.containsKey(repository.getId());
|
return byId.containsKey(repository.getId());
|
||||||
@@ -193,4 +198,11 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
public Long getLastModified() {
|
public Long getLastModified() {
|
||||||
return repositoryLocationResolver.getLastModified();
|
return repositoryLocationResolver.getLastModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
repositoryLocationResolver.refresh();
|
||||||
|
byNamespaceAndName.clear();
|
||||||
|
byId.clear();
|
||||||
|
init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getXmlFileContent() {
|
private String getXmlFileContent() {
|
||||||
Path storePath = basePath.resolve("config").resolve("repositories.xml");
|
Path storePath = basePath.resolve("config").resolve("repository-paths.xml");
|
||||||
|
|
||||||
assertThat(storePath).isRegularFile();
|
assertThat(storePath).isRegularFile();
|
||||||
return content(storePath);
|
return content(storePath);
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import org.junit.jupiter.api.Nested;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junitpioneer.jupiter.TempDirectory;
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.Captor;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
@@ -32,7 +30,9 @@ import static java.util.Arrays.asList;
|
|||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -47,9 +47,6 @@ class XmlRepositoryDAOTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private PathBasedRepositoryLocationResolver locationResolver;
|
private PathBasedRepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
|
|
||||||
|
|
||||||
private FileSystem fileSystem = new DefaultFileSystem();
|
private FileSystem fileSystem = new DefaultFileSystem();
|
||||||
|
|
||||||
private XmlRepositoryDAO dao;
|
private XmlRepositoryDAO dao;
|
||||||
@@ -268,43 +265,80 @@ class XmlRepositoryDAOTest {
|
|||||||
|
|
||||||
verify(locationResolver).updateModificationDate();
|
verify(locationResolver).updateModificationDate();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
private String getXmlFileContent(String id) {
|
||||||
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
|
Path storePath = metadataFile(id);
|
||||||
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
|
|
||||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
|
||||||
|
|
||||||
Path repositoryPath = basePath.resolve("existing");
|
assertThat(storePath).isRegularFile();
|
||||||
Files.createDirectories(repositoryPath);
|
return content(storePath);
|
||||||
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
|
}
|
||||||
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
|
|
||||||
|
|
||||||
forAllCaptor.getValue().accept("existing", repositoryPath);
|
private Path metadataFile(String id) {
|
||||||
|
return locationResolver.create(id).resolve("metadata.xml");
|
||||||
|
}
|
||||||
|
|
||||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
private String content(Path storePath) {
|
||||||
}
|
try {
|
||||||
|
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
|
||||||
private String getXmlFileContent(String id) {
|
} catch (IOException e) {
|
||||||
Path storePath = metadataFile(id);
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
assertThat(storePath).isRegularFile();
|
|
||||||
return content(storePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path metadataFile(String id) {
|
|
||||||
return locationResolver.create(id).resolve("metadata.xml");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String content(Path storePath) {
|
|
||||||
try {
|
|
||||||
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Repository createRepository(String id) {
|
@Nested
|
||||||
|
class WithExistingRepositories {
|
||||||
|
|
||||||
|
private Path repositoryPath;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException {
|
||||||
|
repositoryPath = basePath.resolve("existing");
|
||||||
|
|
||||||
|
Files.createDirectories(repositoryPath);
|
||||||
|
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
|
||||||
|
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadExistingRepositoriesFromPathDatabase() {
|
||||||
|
// given
|
||||||
|
mockExistingPath();
|
||||||
|
|
||||||
|
// when
|
||||||
|
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRefreshWithExistingRepositoriesFromPathDatabase() {
|
||||||
|
// given
|
||||||
|
doNothing().when(locationResolver).forAllPaths(any());
|
||||||
|
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||||
|
|
||||||
|
mockExistingPath();
|
||||||
|
|
||||||
|
// when
|
||||||
|
dao.refresh();
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(locationResolver).refresh();
|
||||||
|
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockExistingPath() {
|
||||||
|
doAnswer(
|
||||||
|
invocation -> {
|
||||||
|
((BiConsumer<String, Path>) invocation.getArgument(0)).accept("existing", repositoryPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
).when(locationResolver).forAllPaths(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Repository createRepository(String id) {
|
||||||
return new Repository(id, "xml", "space", id);
|
return new Repository(id, "xml", "space", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>sonia.scm.maven</groupId>
|
<groupId>sonia.scm.maven</groupId>
|
||||||
<artifactId>smp-maven-plugin</artifactId>
|
<artifactId>smp-maven-plugin</artifactId>
|
||||||
<version>1.0.0-alpha-3</version>
|
|
||||||
<extensions>true</extensions>
|
<extensions>true</extensions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"@scm-manager/ui-extensions": "^0.1.2"
|
"@scm-manager/ui-extensions": "^0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28"
|
"@scm-manager/ui-bundler": "^0.0.29"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.web.lfs;
|
|
||||||
|
|
||||||
import com.github.legman.Subscribe;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import sonia.scm.EagerSingleton;
|
|
||||||
import sonia.scm.HandlerEventType;
|
|
||||||
import sonia.scm.plugin.Extension;
|
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
|
||||||
import sonia.scm.repository.Repository;
|
|
||||||
import sonia.scm.repository.RepositoryEvent;
|
|
||||||
import sonia.scm.store.Blob;
|
|
||||||
import sonia.scm.store.BlobStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
* @since 1.54
|
|
||||||
*/
|
|
||||||
@Extension
|
|
||||||
@EagerSingleton
|
|
||||||
public class LfsStoreRemoveListener {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class);
|
|
||||||
|
|
||||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) {
|
|
||||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all object from the blob store, if the event is an delete event and the repository is a git repository.
|
|
||||||
*
|
|
||||||
* @param event repository event
|
|
||||||
*/
|
|
||||||
@Subscribe
|
|
||||||
public void handleRepositoryEvent(RepositoryEvent event) {
|
|
||||||
if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) {
|
|
||||||
removeLfsStore(event.getItem());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDeleteEvent(RepositoryEvent event) {
|
|
||||||
return HandlerEventType.DELETE == event.getEventType();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isGitRepositoryEvent(RepositoryEvent event) {
|
|
||||||
return event.getItem() != null
|
|
||||||
&& event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeLfsStore(Repository repository) {
|
|
||||||
LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName());
|
|
||||||
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
|
||||||
for ( Blob blob : blobStore.getAll() ) {
|
|
||||||
LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName());
|
|
||||||
blobStore.remove(blob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -117,6 +117,6 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
|
|
||||||
initRepository();
|
initRepository();
|
||||||
File path = repositoryHandler.getDirectory(repository.getId());
|
File path = repositoryHandler.getDirectory(repository.getId());
|
||||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.web.lfs;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import java.util.List;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
|
||||||
import sonia.scm.HandlerEventType;
|
|
||||||
import sonia.scm.repository.Repository;
|
|
||||||
import sonia.scm.repository.RepositoryEvent;
|
|
||||||
import sonia.scm.repository.RepositoryTestData;
|
|
||||||
import sonia.scm.store.Blob;
|
|
||||||
import sonia.scm.store.BlobStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for {@link LfsStoreRemoveListener}.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
|
||||||
public class LfsStoreRemoveListenerTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private LfsBlobStoreFactory lfsBlobStoreFactory;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private BlobStore blobStore;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private LfsStoreRemoveListener lfsStoreRemoveListener;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHandleRepositoryEventWithNonDeleteEvents() {
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
|
|
||||||
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
|
|
||||||
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
|
|
||||||
|
|
||||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHandleRepositoryEventWithNonGitRepositories() {
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
|
|
||||||
|
|
||||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHandleRepositoryEvent() {
|
|
||||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
|
|
||||||
|
|
||||||
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
|
|
||||||
Blob blobA = mockBlob("a");
|
|
||||||
Blob blobB = mockBlob("b");
|
|
||||||
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
|
|
||||||
when(blobStore.getAll()).thenReturn(blobs);
|
|
||||||
|
|
||||||
|
|
||||||
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
|
|
||||||
verify(blobStore).getAll();
|
|
||||||
verify(blobStore).remove(blobA);
|
|
||||||
verify(blobStore).remove(blobB);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(blobStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Blob mockBlob(String id) {
|
|
||||||
Blob blob = mock(Blob.class);
|
|
||||||
when(blob.getId()).thenReturn(id);
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RepositoryEvent event(HandlerEventType eventType) {
|
|
||||||
return event(eventType, "git");
|
|
||||||
}
|
|
||||||
|
|
||||||
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
|
|
||||||
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@scm-manager/scm-hg-plugin",
|
"name": "@scm-manager/scm-hg-plugin",
|
||||||
"main": "src/main/js/index.js",
|
"main": "src/main/js/index.js",
|
||||||
"license" : "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ui-bundler plugin"
|
"build": "ui-bundler plugin"
|
||||||
},
|
},
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
"@scm-manager/ui-extensions": "^0.1.2"
|
"@scm-manager/ui-extensions": "^0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28"
|
"@scm-manager/ui-bundler": "^0.0.29"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
|
|
||||||
initRepository();
|
initRepository();
|
||||||
File path = repositoryHandler.getDirectory(repository.getId());
|
File path = repositoryHandler.getDirectory(repository.getId());
|
||||||
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
|||||||
"@scm-manager/ui-extensions": "^0.1.2"
|
"@scm-manager/ui-extensions": "^0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28"
|
"@scm-manager/ui-bundler": "^0.0.29"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,6 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
|
|
||||||
initRepository();
|
initRepository();
|
||||||
File path = repositoryHandler.getDirectory(repository.getId());
|
File path = repositoryHandler.getDirectory(repository.getId());
|
||||||
assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
assertEquals(repoPath.toString()+File.separator+ RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
|
|||||||
File repoDirectory = new File(baseDirectory, repository.getId());
|
File repoDirectory = new File(baseDirectory, repository.getId());
|
||||||
repoPath = repoDirectory.toPath();
|
repoPath = repoDirectory.toPath();
|
||||||
// when(repoDao.getPath(repository.getId())).thenReturn(repoPath);
|
// when(repoDao.getPath(repository.getId())).thenReturn(repoPath);
|
||||||
return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
return new File(repoDirectory, RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected File baseDirectory;
|
protected File baseDirectory;
|
||||||
|
|||||||
@@ -153,7 +153,12 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
|||||||
*/
|
*/
|
||||||
private void extract(File folder) throws IOException
|
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;
|
ZipInputStream zip = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -164,7 +169,7 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
|
|||||||
|
|
||||||
while (entry != null)
|
while (entry != null)
|
||||||
{
|
{
|
||||||
File file = new File(folder, entry.getName());
|
File file = new File(targetFolder, entry.getName());
|
||||||
File parent = file.getParentFile();
|
File parent = file.getParentFile();
|
||||||
|
|
||||||
if (!parent.exists())
|
if (!parent.exists())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"eslint-fix": "eslint src --fix"
|
"eslint-fix": "eslint src --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28",
|
"@scm-manager/ui-bundler": "^0.0.29",
|
||||||
"create-index": "^2.3.0",
|
"create-index": "^2.3.0",
|
||||||
"enzyme": "^3.5.0",
|
"enzyme": "^3.5.0",
|
||||||
"enzyme-adapter-react-16": "^1.3.1",
|
"enzyme-adapter-react-16": "^1.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { withRouter } from "react-router-dom";
|
||||||
|
import { withContextPath } from "./urls";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds anchor links to markdown headings.
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/rexxars/react-markdown/issues/69">Headings are missing anchors / ids</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.Node,
|
||||||
|
level: number,
|
||||||
|
location: any
|
||||||
|
};
|
||||||
|
|
||||||
|
function flatten(text: string, child: any) {
|
||||||
|
return typeof child === "string"
|
||||||
|
? text + child
|
||||||
|
: React.Children.toArray(child.props.children).reduce(flatten, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns heading text into a anchor id
|
||||||
|
*
|
||||||
|
* @VisibleForTesting
|
||||||
|
*/
|
||||||
|
export function headingToAnchorId(heading: string) {
|
||||||
|
return heading.toLowerCase().replace(/\W/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownHeadingRenderer(props: Props) {
|
||||||
|
const children = React.Children.toArray(props.children);
|
||||||
|
const heading = children.reduce(flatten, "");
|
||||||
|
const anchorId = headingToAnchorId(heading);
|
||||||
|
const headingElement = React.createElement("h" + props.level, {}, props.children);
|
||||||
|
const href = withContextPath(props.location.pathname + "#" + anchorId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a id={`${anchorId}`} className="anchor" href={href}>
|
||||||
|
{headingElement}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(MarkdownHeadingRenderer);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { headingToAnchorId } from "./MarkdownHeadingRenderer";
|
||||||
|
|
||||||
|
describe("headingToAnchorId tests", () => {
|
||||||
|
|
||||||
|
it("should lower case the text", () => {
|
||||||
|
expect(headingToAnchorId("Hello")).toBe("hello");
|
||||||
|
expect(headingToAnchorId("HeLlO")).toBe("hello");
|
||||||
|
expect(headingToAnchorId("HELLO")).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should replace spaces with hyphen", () => {
|
||||||
|
expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff");
|
||||||
|
expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -3,17 +3,49 @@ import React from "react";
|
|||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
import Markdown from "react-markdown/with-html";
|
import Markdown from "react-markdown/with-html";
|
||||||
import {binder} from "@scm-manager/ui-extensions";
|
import {binder} from "@scm-manager/ui-extensions";
|
||||||
|
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
|
||||||
|
import { withRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: string,
|
content: string,
|
||||||
renderContext?: Object,
|
renderContext?: Object,
|
||||||
renderers?: Object,
|
renderers?: Object,
|
||||||
|
enableAnchorHeadings: boolean,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
location: any
|
||||||
};
|
};
|
||||||
|
|
||||||
class MarkdownView extends React.Component<Props> {
|
class MarkdownView extends React.Component<Props> {
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
enableAnchorHeadings: false
|
||||||
|
};
|
||||||
|
|
||||||
|
contentRef: ?HTMLDivElement;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// we have to use componentDidUpdate, because we have to wait until all
|
||||||
|
// children are rendered and componentDidMount is called before the
|
||||||
|
// markdown content was rendered.
|
||||||
|
const hash = this.props.location.hash;
|
||||||
|
if (this.contentRef && hash) {
|
||||||
|
// we query only child elements, to avoid strange scrolling with multiple
|
||||||
|
// markdown elements on one page.
|
||||||
|
const element = this.contentRef.querySelector(hash);
|
||||||
|
if (element && element.scrollIntoView) {
|
||||||
|
element.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {content, renderers, renderContext} = this.props;
|
const {content, renderers, renderContext, enableAnchorHeadings} = this.props;
|
||||||
|
|
||||||
const rendererFactory = binder.getExtension("markdown-renderer-factory");
|
const rendererFactory = binder.getExtension("markdown-renderer-factory");
|
||||||
let rendererList = renderers;
|
let rendererList = renderers;
|
||||||
@@ -26,20 +58,26 @@ class MarkdownView extends React.Component<Props> {
|
|||||||
rendererList = {};
|
rendererList = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableAnchorHeadings) {
|
||||||
|
rendererList.heading = MarkdownHeadingRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
if (!rendererList.code){
|
if (!rendererList.code){
|
||||||
rendererList.code = SyntaxHighlighter;
|
rendererList.code = SyntaxHighlighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Markdown
|
<div ref={el => (this.contentRef = el)}>
|
||||||
className="content"
|
<Markdown
|
||||||
skipHtml={true}
|
className="content"
|
||||||
escapeHtml={true}
|
skipHtml={true}
|
||||||
source={content}
|
escapeHtml={true}
|
||||||
renderers={rendererList}
|
source={content}
|
||||||
/>
|
renderers={rendererList}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MarkdownView;
|
export default withRouter(MarkdownView);
|
||||||
|
|||||||
@@ -17,13 +17,25 @@ const styles = {
|
|||||||
panel: {
|
panel: {
|
||||||
fontSize: "1rem"
|
fontSize: "1rem"
|
||||||
},
|
},
|
||||||
|
/* breaks into a second row
|
||||||
|
when buttons and title become too long */
|
||||||
|
level: {
|
||||||
|
flexWrap: "wrap"
|
||||||
|
},
|
||||||
titleHeader: {
|
titleHeader: {
|
||||||
|
display: "flex",
|
||||||
|
maxWidth: "100%",
|
||||||
cursor: "pointer"
|
cursor: "pointer"
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
marginLeft: ".25rem",
|
marginLeft: ".25rem",
|
||||||
fontSize: "1rem"
|
fontSize: "1rem"
|
||||||
},
|
},
|
||||||
|
/* align child to right */
|
||||||
|
buttonHeader: {
|
||||||
|
display: "flex",
|
||||||
|
marginLeft: "auto"
|
||||||
|
},
|
||||||
hunkDivider: {
|
hunkDivider: {
|
||||||
margin: ".5rem 0"
|
margin: ".5rem 0"
|
||||||
},
|
},
|
||||||
@@ -143,14 +155,41 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return file.newPath;
|
return file.newPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hoverFileTitle = (file: any) => {
|
||||||
|
if (
|
||||||
|
file.oldPath !== file.newPath &&
|
||||||
|
(file.type === "copy" || file.type === "rename")
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{file.oldPath} > {file.newPath}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (file.type === "delete") {
|
||||||
|
return file.oldPath;
|
||||||
|
}
|
||||||
|
return file.newPath;
|
||||||
|
};
|
||||||
|
|
||||||
renderChangeTag = (file: any) => {
|
renderChangeTag = (file: any) => {
|
||||||
const { t } = this.props;
|
const { t, classes } = this.props;
|
||||||
const key = "diff.changes." + file.type;
|
const key = "diff.changes." + file.type;
|
||||||
let value = t(key);
|
let value = t(key);
|
||||||
if (key === value) {
|
if (key === value) {
|
||||||
value = file.type;
|
value = file.type;
|
||||||
}
|
}
|
||||||
return <span className="tag is-info has-text-weight-normal">{value}</span>;
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"tag",
|
||||||
|
"is-info",
|
||||||
|
"has-text-weight-normal",
|
||||||
|
classes.changeType
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -187,20 +226,21 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div className={classNames("panel", classes.panel)}>
|
<div className={classNames("panel", classes.panel)}>
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<div className="level">
|
<div className={classNames("level", classes.level)}>
|
||||||
<div
|
<div
|
||||||
className={classNames("level-left", classes.titleHeader)}
|
className={classNames("level-left", classes.titleHeader)}
|
||||||
onClick={this.toggleCollapse}
|
onClick={this.toggleCollapse}
|
||||||
|
title={this.hoverFileTitle(file)}
|
||||||
>
|
>
|
||||||
<i className={icon} />
|
<i className={icon} />
|
||||||
<span className={classes.title}>
|
<span
|
||||||
|
className={classNames("is-ellipsis-overflow", classes.title)}
|
||||||
|
>
|
||||||
{this.renderFileTitle(file)}
|
{this.renderFileTitle(file)}
|
||||||
</span>
|
</span>
|
||||||
<span className={classes.changeType}>
|
{this.renderChangeTag(file)}
|
||||||
{this.renderChangeTag(file)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="level-right">
|
<div className={classNames("level-right", classes.buttonHeader)}>
|
||||||
<Button action={this.toggleSideBySide} className="reduced-mobile">
|
<Button action={this.toggleSideBySide} className="reduced-mobile">
|
||||||
<span className="icon is-small">
|
<span className="icon is-small">
|
||||||
<i
|
<i
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
"check": "flow check"
|
"check": "flow check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28"
|
"@scm-manager/ui-bundler": "^0.0.29"
|
||||||
},
|
},
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [
|
"transform": [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@
|
|||||||
"pre-commit": "jest && flow && eslint src"
|
"pre-commit": "jest && flow && eslint src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-bundler": "^0.0.28",
|
"@scm-manager/ui-bundler": "^0.0.29",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"copyfiles": "^2.0.0",
|
"copyfiles": "^2.0.0",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
|
|||||||
@@ -9,29 +9,35 @@
|
|||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
"navLink": "Berechtigungsrollen",
|
"navLink": "Berechtigungsrollen",
|
||||||
"title": "Berechtigungsrollen",
|
"title": "Berechtigungsrollen",
|
||||||
"noPermissionRoles": "Keine Berechtigungsrollen gefunden.",
|
"errorTitle": "Fehler",
|
||||||
"system": "System",
|
"errorSubtitle": "Unbekannter Berechtigungsrollen Fehler",
|
||||||
"createButton": "Berechtigungsrolle erstellen",
|
"createSubtitle": "Berechtigungsrolle erstellen",
|
||||||
|
"editSubtitle": "Berechtigungsrolle bearbeiten",
|
||||||
|
"overview": {
|
||||||
|
"title": "Übersicht aller verfügbaren Berechtigungsrollen",
|
||||||
|
"noPermissionRoles": "Keine Berechtigungsrollen gefunden.",
|
||||||
|
"createButton": "Berechtigungsrolle erstellen"
|
||||||
|
},
|
||||||
|
"editButton": "Bearbeiten",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"verbs": "Verben",
|
"verbs": "Berechtigungen",
|
||||||
"button": {
|
"system": "System",
|
||||||
"edit": "Bearbeiten"
|
|
||||||
},
|
|
||||||
"create": {
|
|
||||||
"name": "Name"
|
|
||||||
},
|
|
||||||
"edit": "Berechtigungsrolle bearbeiten",
|
|
||||||
"form": {
|
"form": {
|
||||||
"subtitle": "Berechtigungsrolle bearbeiten",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"permissions": "Berechtigungen",
|
"permissions": "Berechtigungen",
|
||||||
"submit": "Speichern"
|
"submit": "Speichern"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"deleteRole" : {
|
||||||
"name": "Name",
|
"button": "Löschen",
|
||||||
"system": "System"
|
"subtitle": "Berechtigungsrolle löschen",
|
||||||
|
"confirmAlert": {
|
||||||
|
"title": "Berechtigungsrolle löschen",
|
||||||
|
"message": "Soll die Berechtigungsrolle wirklich gelöscht werden? Alle Nutzer dieser Rolle verlieren Ihre Berechtigungen.",
|
||||||
|
"submit": "Ja",
|
||||||
|
"cancel": "Nein"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config-form": {
|
"config-form": {
|
||||||
"submit": "Speichern",
|
"submit": "Speichern",
|
||||||
|
|||||||
@@ -9,29 +9,35 @@
|
|||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
"navLink": "Permission Roles",
|
"navLink": "Permission Roles",
|
||||||
"title": "Permission Roles",
|
"title": "Permission Roles",
|
||||||
"noPermissionRoles": "No permission roles found.",
|
"errorTitle": "Error",
|
||||||
"system": "System",
|
"errorSubtitle": "Unknown Permission Role Error",
|
||||||
"createButton": "Create Permission Role",
|
"createSubtitle": "Create Permission Role",
|
||||||
|
"editSubtitle": "Edit Permission Role",
|
||||||
|
"overview": {
|
||||||
|
"title": "Overview of all permission roles",
|
||||||
|
"noPermissionRoles": "No permission roles found.",
|
||||||
|
"createButton": "Create Permission Role"
|
||||||
|
},
|
||||||
|
"editButton": "Edit",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"verbs": "Verbs",
|
"verbs": "Permissions",
|
||||||
"edit": "Edit Permission Role",
|
"system": "System",
|
||||||
"button": {
|
|
||||||
"edit": "Edit"
|
|
||||||
},
|
|
||||||
"create": {
|
|
||||||
"name": "Name"
|
|
||||||
},
|
|
||||||
"form": {
|
"form": {
|
||||||
"subtitle": "Edit Permission Role",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"permissions": "Permissions",
|
"permissions": "Permissions",
|
||||||
"submit": "Save"
|
"submit": "Save"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"deleteRole": {
|
||||||
"name": "Name",
|
"button": "Delete",
|
||||||
"system": "System"
|
"subtitle": "Delete Permission Role",
|
||||||
|
"confirmAlert": {
|
||||||
|
"title": "Delete Permission Role",
|
||||||
|
"message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.",
|
||||||
|
"submit": "Yes",
|
||||||
|
"cancel": "No"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config-form": {
|
"config-form": {
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class Config extends React.Component<Props> {
|
|||||||
path={`${url}/roles/create`}
|
path={`${url}/roles/create`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<CreateRepositoryRole
|
<CreateRepositoryRole
|
||||||
disabled={false}
|
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -104,6 +103,7 @@ class Config extends React.Component<Props> {
|
|||||||
to={`${url}/roles/`}
|
to={`${url}/roles/`}
|
||||||
label={t("repositoryRole.navLink")}
|
label={t("repositoryRole.navLink")}
|
||||||
activeWhenMatch={this.matchesRoles}
|
activeWhenMatch={this.matchesRoles}
|
||||||
|
activeOnlyWhenExact={false}
|
||||||
/>
|
/>
|
||||||
<ExtensionPoint
|
<ExtensionPoint
|
||||||
name="config.navigation"
|
name="config.navigation"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role: RepositoryRole,
|
role: RepositoryRole,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
|
classes: any,
|
||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||||
import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
|
import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
|
||||||
import { Button, Subtitle } from "@scm-manager/ui-components";
|
import { Button } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role: RepositoryRole,
|
role: RepositoryRole,
|
||||||
@@ -20,7 +20,7 @@ class PermissionRoleDetails extends React.Component<Props> {
|
|||||||
if (!!this.props.role._links.update) {
|
if (!!this.props.role._links.update) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
label={t("repositoryRole.button.edit")}
|
label={t("repositoryRole.editButton")}
|
||||||
link={`${url}/edit`}
|
link={`${url}/edit`}
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
@@ -33,18 +33,16 @@ class PermissionRoleDetails extends React.Component<Props> {
|
|||||||
const { role } = this.props;
|
const { role } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<PermissionRoleDetailsTable role={role} />
|
<PermissionRoleDetailsTable role={role} />
|
||||||
<hr />
|
<hr />
|
||||||
{this.renderEditButton()}
|
{this.renderEditButton()}
|
||||||
<div className="content">
|
<ExtensionPoint
|
||||||
<ExtensionPoint
|
name="repositoryRole.role-details.information"
|
||||||
name="repositoryRole.role-details.information"
|
renderAll={true}
|
||||||
renderAll={true}
|
props={{ role }}
|
||||||
props={{ role }}
|
/>
|
||||||
/>
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import AvailableVerbs from "./AvailableVerbs";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role: RepositoryRole,
|
role: RepositoryRole,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
@@ -16,18 +17,18 @@ class PermissionRoleDetailsTable extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<table className="table content">
|
<table className="table content">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("repositoryRole.name")}</th>
|
<th>{t("repositoryRole.name")}</th>
|
||||||
<td>{role.name}</td>
|
<td>{role.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("repositoryRole.type")}</th>
|
<th>{t("repositoryRole.type")}</th>
|
||||||
<td>{role.type}</td>
|
<td>{role.type}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("repositoryRole.verbs")}</th>
|
<th>{t("repositoryRole.verbs")}</th>
|
||||||
<AvailableVerbs role={role} />
|
<AvailableVerbs role={role} />
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Props = {
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
roles: RepositoryRole[],
|
roles: RepositoryRole[],
|
||||||
|
|
||||||
|
// context props
|
||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,16 +18,16 @@ class PermissionRoleTable extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<table className="card-table table is-hoverable is-fullwidth">
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("repositoryRole.form.name")}</th>
|
<th>{t("repositoryRole.name")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{roles.map((role, index) => {
|
{roles.map((role, index) => {
|
||||||
return (
|
return (
|
||||||
<PermissionRoleRow key={index} baseUrl={baseUrl} role={role} />
|
<PermissionRoleRow key={index} baseUrl={baseUrl} role={role} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { translate } from "react-i18next";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
system?: boolean,
|
system?: boolean,
|
||||||
|
|
||||||
|
// context props
|
||||||
classes: any,
|
classes: any,
|
||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
@@ -24,7 +26,7 @@ class SystemRoleTag extends React.Component<Props> {
|
|||||||
if (system) {
|
if (system) {
|
||||||
return (
|
return (
|
||||||
<span className={classNames("tag is-dark", classes.tag)}>
|
<span className={classNames("tag is-dark", classes.tag)}>
|
||||||
{t("role.system")}
|
{t("repositoryRole.system")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import RepositoryRoleForm from "./RepositoryRoleForm";
|
import RepositoryRoleForm from "./RepositoryRoleForm";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { ErrorNotification, Title } from "@scm-manager/ui-components";
|
import {ErrorNotification, Subtitle, Title} from "@scm-manager/ui-components";
|
||||||
import {
|
import {
|
||||||
createRole,
|
createRole,
|
||||||
getCreateRoleFailure,
|
getCreateRoleFailure,
|
||||||
@@ -15,11 +15,12 @@ import {
|
|||||||
getRepositoryRolesLink,
|
getRepositoryRolesLink,
|
||||||
getRepositoryVerbsLink
|
getRepositoryVerbsLink
|
||||||
} from "../../../modules/indexResource";
|
} from "../../../modules/indexResource";
|
||||||
|
import type {History} from "history";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled: boolean,
|
|
||||||
repositoryRolesLink: string,
|
repositoryRolesLink: string,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
|
history: History,
|
||||||
|
|
||||||
//dispatch function
|
//dispatch function
|
||||||
addRole: (link: string, role: RepositoryRole, callback?: () => void) => void,
|
addRole: (link: string, role: RepositoryRole, callback?: () => void) => void,
|
||||||
@@ -50,8 +51,8 @@ class CreateRepositoryRole extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title title={t("repositoryRole.title")} />
|
<Title title={t("repositoryRole.title")} />
|
||||||
|
<Subtitle subtitle={t("repositoryRole.createSubtitle")} />
|
||||||
<RepositoryRoleForm
|
<RepositoryRoleForm
|
||||||
disabled={this.props.disabled}
|
|
||||||
submitForm={role => this.createRepositoryRole(role)}
|
submitForm={role => this.createRepositoryRole(role)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
113
scm-ui/src/config/roles/containers/DeleteRepositoryRole.js
Normal file
113
scm-ui/src/config/roles/containers/DeleteRepositoryRole.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||||
|
import {
|
||||||
|
Subtitle,
|
||||||
|
DeleteButton,
|
||||||
|
confirmAlert,
|
||||||
|
ErrorNotification
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { withRouter } from "react-router-dom";
|
||||||
|
import type { History } from "history";
|
||||||
|
import {
|
||||||
|
deleteRole,
|
||||||
|
getDeleteRoleFailure,
|
||||||
|
isDeleteRolePending
|
||||||
|
} from "../modules/roles";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
role: RepositoryRole,
|
||||||
|
confirmDialog?: boolean,
|
||||||
|
deleteRole: (role: RepositoryRole, callback?: () => void) => void,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
history: History,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeleteRepositoryRole extends React.Component<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
confirmDialog: true
|
||||||
|
};
|
||||||
|
|
||||||
|
roleDeleted = () => {
|
||||||
|
this.props.history.push("/config/roles/");
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRole = () => {
|
||||||
|
this.props.deleteRole(this.props.role, this.roleDeleted);
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmDelete = () => {
|
||||||
|
const { t } = this.props;
|
||||||
|
confirmAlert({
|
||||||
|
title: t("deleteRole.confirmAlert.title"),
|
||||||
|
message: t("deleteRole.confirmAlert.message"),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: t("deleteRole.confirmAlert.submit"),
|
||||||
|
onClick: () => this.deleteRole()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("deleteRole.confirmAlert.cancel"),
|
||||||
|
onClick: () => null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
isDeletable = () => {
|
||||||
|
return this.props.role._links.delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, error, confirmDialog, t } = this.props;
|
||||||
|
const action = confirmDialog ? this.confirmDelete : this.deleteRole;
|
||||||
|
|
||||||
|
if (!this.isDeletable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Subtitle subtitle={t("deleteRole.subtitle")} />
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<ErrorNotification error={error} />
|
||||||
|
<DeleteButton
|
||||||
|
label={t("deleteRole.button")}
|
||||||
|
action={action}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const loading = isDeleteRolePending(state, ownProps.role.name);
|
||||||
|
const error = getDeleteRoleFailure(state, ownProps.role.name);
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
deleteRole: (role: RepositoryRole, callback?: () => void) => {
|
||||||
|
dispatch(deleteRole(role, callback));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(withRouter(translate("config")(DeleteRepositoryRole)));
|
||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
isModifyRolePending,
|
isModifyRolePending,
|
||||||
modifyRole
|
modifyRole
|
||||||
} from "../modules/roles";
|
} from "../modules/roles";
|
||||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
import { ErrorNotification, Subtitle } from "@scm-manager/ui-components";
|
||||||
import type { RepositoryRole } from "@scm-manager/ui-types";
|
import type { RepositoryRole } from "@scm-manager/ui-types";
|
||||||
|
import type { History } from "history";
|
||||||
|
import DeleteRepositoryRole from "./DeleteRepositoryRole";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
@@ -17,26 +19,25 @@ type Props = {
|
|||||||
repositoryRolesLink: string,
|
repositoryRolesLink: string,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
|
|
||||||
|
// context objects
|
||||||
|
t: string => string,
|
||||||
|
history: History,
|
||||||
|
|
||||||
//dispatch function
|
//dispatch function
|
||||||
updateRole: (
|
updateRole: (role: RepositoryRole, callback?: () => void) => void
|
||||||
link: string,
|
|
||||||
role: RepositoryRole,
|
|
||||||
callback?: () => void
|
|
||||||
) => void
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditRepositoryRole extends React.Component<Props> {
|
class EditRepositoryRole extends React.Component<Props> {
|
||||||
repositoryRoleUpdated = (role: RepositoryRole) => {
|
repositoryRoleUpdated = () => {
|
||||||
const { history } = this.props;
|
this.props.history.push("/config/roles/");
|
||||||
history.push("/config/roles/");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateRepositoryRole = (role: RepositoryRole) => {
|
updateRepositoryRole = (role: RepositoryRole) => {
|
||||||
this.props.updateRole(role, () => this.repositoryRoleUpdated(role));
|
this.props.updateRole(role, this.repositoryRoleUpdated);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { error } = this.props;
|
const { error, t } = this.props;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
@@ -44,18 +45,20 @@ class EditRepositoryRole extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Subtitle subtitle={t("repositoryRole.editSubtitle")} />
|
||||||
<RepositoryRoleForm
|
<RepositoryRoleForm
|
||||||
nameDisabled={true}
|
|
||||||
role={this.props.role}
|
role={this.props.role}
|
||||||
submitForm={role => this.updateRepositoryRole(role)}
|
submitForm={role => this.updateRepositoryRole(role)}
|
||||||
/>
|
/>
|
||||||
|
<hr/>
|
||||||
|
<DeleteRepositoryRole role={this.props.role}/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const loading = isModifyRolePending(state);
|
const loading = isModifyRolePending(state, ownProps.role.name);
|
||||||
const error = getModifyRoleFailure(state, ownProps.role.name);
|
const error = getModifyRoleFailure(state, ownProps.role.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
type Props = {
|
type Props = {
|
||||||
role?: RepositoryRole,
|
role?: RepositoryRole,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
nameDisabled: boolean,
|
|
||||||
availableVerbs: string[],
|
availableVerbs: string[],
|
||||||
verbsLink: string,
|
verbsLink: string,
|
||||||
submitForm: RepositoryRole => void,
|
submitForm: RepositoryRole => void,
|
||||||
@@ -103,7 +102,7 @@ class RepositoryRoleForm extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, availableVerbs, nameDisabled, t } = this.props;
|
const { loading, availableVerbs, t } = this.props;
|
||||||
const { role } = this.state;
|
const { role } = this.state;
|
||||||
|
|
||||||
const verbSelectBoxes = !availableVerbs
|
const verbSelectBoxes = !availableVerbs
|
||||||
@@ -119,28 +118,25 @@ class RepositoryRoleForm extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.submit}>
|
||||||
<div className="columns">
|
<InputField
|
||||||
<div className="column">
|
name="name"
|
||||||
<InputField
|
label={t("repositoryRole.form.name")}
|
||||||
name="name"
|
onChange={this.handleNameChange}
|
||||||
label={t("repositoryRole.create.name")}
|
value={role.name ? role.name : ""}
|
||||||
onChange={this.handleNameChange}
|
disabled={!!this.props.role}
|
||||||
value={role.name ? role.name : ""}
|
/>
|
||||||
disabled={nameDisabled}
|
<div className="field">
|
||||||
/>
|
<label className="label">
|
||||||
</div>
|
{t("repositoryRole.form.permissions")}
|
||||||
|
</label>
|
||||||
|
{verbSelectBoxes}
|
||||||
</div>
|
</div>
|
||||||
<>{verbSelectBoxes}</>
|
|
||||||
<hr />
|
<hr />
|
||||||
<div className="columns">
|
<SubmitButton
|
||||||
<div className="column">
|
loading={loading}
|
||||||
<SubmitButton
|
label={t("repositoryRole.form.submit")}
|
||||||
loading={loading}
|
disabled={!this.isValid()}
|
||||||
label={t("repositoryRole.form.submit")}
|
/>
|
||||||
disabled={!this.isValid()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { History } from "history";
|
|||||||
import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types";
|
import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types";
|
||||||
import {
|
import {
|
||||||
Title,
|
Title,
|
||||||
|
Subtitle,
|
||||||
Loading,
|
Loading,
|
||||||
Notification,
|
Notification,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
@@ -22,7 +23,8 @@ import {
|
|||||||
getFetchRolesFailure
|
getFetchRolesFailure
|
||||||
} from "../modules/roles";
|
} from "../modules/roles";
|
||||||
import PermissionRoleTable from "../components/PermissionRoleTable";
|
import PermissionRoleTable from "../components/PermissionRoleTable";
|
||||||
import { getRolesLink } from "../../../modules/indexResource";
|
import { getRepositoryRolesLink } from "../../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
roles: RepositoryRole[],
|
roles: RepositoryRole[],
|
||||||
@@ -36,6 +38,7 @@ type Props = {
|
|||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
history: History,
|
history: History,
|
||||||
|
location: any,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchRolesByPage: (link: string, page: number) => void
|
fetchRolesByPage: (link: string, page: number) => void
|
||||||
@@ -61,8 +64,7 @@ class RepositoryRoles extends React.Component<Props> {
|
|||||||
if (page !== statePage || prevProps.location.search !== location.search) {
|
if (page !== statePage || prevProps.location.search !== location.search) {
|
||||||
fetchRolesByPage(
|
fetchRolesByPage(
|
||||||
rolesLink,
|
rolesLink,
|
||||||
page,
|
page
|
||||||
urls.getQueryStringFromLocation(location)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,11 +78,12 @@ class RepositoryRoles extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Title title={t("repositoryRole.title")} />
|
<Title title={t("repositoryRole.title")} />
|
||||||
|
<Subtitle subtitle={t("repositoryRole.overview.title")} />
|
||||||
{this.renderPermissionsTable()}
|
{this.renderPermissionsTable()}
|
||||||
{this.renderCreateButton()}
|
{this.renderCreateButton()}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ class RepositoryRoles extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Notification type="info">
|
<Notification type="info">
|
||||||
{t("repositoryRole.noPermissionRoles")}
|
{t("repositoryRole.overview.noPermissionRoles")}
|
||||||
</Notification>
|
</Notification>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,7 @@ class RepositoryRoles extends React.Component<Props> {
|
|||||||
if (canAddRoles) {
|
if (canAddRoles) {
|
||||||
return (
|
return (
|
||||||
<CreateButton
|
<CreateButton
|
||||||
label={t("repositoryRole.createButton")}
|
label={t("repositoryRole.overview.createButton")}
|
||||||
link={`${baseUrl}/create`}
|
link={`${baseUrl}/create`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -123,7 +126,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const page = urls.getPageFromMatch(match);
|
const page = urls.getPageFromMatch(match);
|
||||||
const canAddRoles = isPermittedToCreateRoles(state);
|
const canAddRoles = isPermittedToCreateRoles(state);
|
||||||
const list = selectListAsCollection(state);
|
const list = selectListAsCollection(state);
|
||||||
const rolesLink = getRolesLink(state);
|
const rolesLink = getRepositoryRolesLink(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roles,
|
roles,
|
||||||
|
|||||||
@@ -81,26 +81,22 @@ class SingleRepositoryRole extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title title={t("repositoryRole.title")} />
|
<Title title={t("repositoryRole.title")} />
|
||||||
<div className="columns">
|
<Route
|
||||||
<div className="column is-three-quarters">
|
path={`${url}/info`}
|
||||||
<Route
|
component={() => <PermissionRoleDetail role={role} url={url} />}
|
||||||
path={`${url}/info`}
|
/>
|
||||||
component={() => <PermissionRoleDetail role={role} url={url} />}
|
<Route
|
||||||
/>
|
path={`${url}/edit`}
|
||||||
<Route
|
exact
|
||||||
path={`${url}/edit`}
|
component={() => (
|
||||||
exact
|
<EditRepositoryRole role={role} history={this.props.history} />
|
||||||
component={() => (
|
)}
|
||||||
<EditRepositoryRole role={role} history={this.props.history} />
|
/>
|
||||||
)}
|
<ExtensionPoint
|
||||||
/>
|
name="roles.route"
|
||||||
<ExtensionPoint
|
props={extensionProps}
|
||||||
name="roles.route"
|
renderAll={true}
|
||||||
props={extensionProps}
|
/>
|
||||||
renderAll={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class DeleteGroup extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
groupDeleted = () => {
|
groupDeleted = () => {
|
||||||
this.props.history.push("/groups");
|
this.props.history.push("/groups/");
|
||||||
};
|
};
|
||||||
|
|
||||||
confirmDelete = () => {
|
confirmDelete = () => {
|
||||||
|
|||||||
@@ -159,10 +159,6 @@ export function getSvnConfigLink(state: Object) {
|
|||||||
return getLink(state, "svnConfig");
|
return getLink(state, "svnConfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRolesLink(state: Object) {
|
|
||||||
return getLink(state, "repositoryRoles");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserAutoCompleteLink(state: Object): string {
|
export function getUserAutoCompleteLink(state: Object): string {
|
||||||
const link = getLinkCollection(state, "autocomplete").find(
|
const link = getLinkCollection(state, "autocomplete").find(
|
||||||
i => i.name === "users"
|
i => i.name === "users"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DeleteRepo extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
deleted = () => {
|
deleted = () => {
|
||||||
this.props.history.push("/repos");
|
this.props.history.push("/repos/");
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRepo = () => {
|
deleteRepo = () => {
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class Permissions extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Subtitle subtitle={t("permission.title")} />
|
<Subtitle subtitle={t("permission.title")} />
|
||||||
<table className="has-background-light table is-hoverable is-fullwidth">
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DeleteUser extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
userDeleted = () => {
|
userDeleted = () => {
|
||||||
this.props.history.push("/users");
|
this.props.history.push("/users/");
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteUser = () => {
|
deleteUser = () => {
|
||||||
|
|||||||
@@ -275,6 +275,14 @@ ul.is-separated {
|
|||||||
.panel-block {
|
.panel-block {
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
& .comment-wrapper:first-child div:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .diff-widget-content div {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-footer {
|
.panel-footer {
|
||||||
|
|||||||
1022
scm-ui/yarn.lock
1022
scm-ui/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -461,7 +461,6 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>sonia.scm.maven</groupId>
|
<groupId>sonia.scm.maven</groupId>
|
||||||
<artifactId>smp-maven-plugin</artifactId>
|
<artifactId>smp-maven-plugin</artifactId>
|
||||||
<version>1.0.0-alpha-2</version>
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<artifactItems>
|
<artifactItems>
|
||||||
<artifactItem>
|
<artifactItem>
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import sonia.scm.plugin.ExtensionProcessor;
|
|||||||
import sonia.scm.plugin.PluginWrapper;
|
import sonia.scm.plugin.PluginWrapper;
|
||||||
import sonia.scm.repository.RepositoryManager;
|
import sonia.scm.repository.RepositoryManager;
|
||||||
import sonia.scm.schedule.Scheduler;
|
import sonia.scm.schedule.Scheduler;
|
||||||
import sonia.scm.upgrade.UpgradeManager;
|
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
|
|
||||||
@@ -102,17 +101,8 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void beforeInjectorCreation() {
|
private void beforeInjectorCreation() {
|
||||||
upgradeIfNecessary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upgradeIfNecessary() {
|
|
||||||
if (!hasStartupErrors()) {
|
|
||||||
UpgradeManager upgradeHandler = new UpgradeManager();
|
|
||||||
|
|
||||||
upgradeHandler.doUpgrade();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasStartupErrors() {
|
private boolean hasStartupErrors() {
|
||||||
return SCMContext.getContext().getStartupError() != null;
|
return SCMContext.getContext().getStartupError() != null;
|
||||||
}
|
}
|
||||||
@@ -132,9 +122,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
List<Module> moduleList = Lists.newArrayList();
|
List<Module> moduleList = Lists.newArrayList();
|
||||||
|
|
||||||
moduleList.add(new ResteasyModule());
|
moduleList.add(new ResteasyModule());
|
||||||
moduleList.add(new ScmInitializerModule());
|
|
||||||
moduleList.add(new ScmEventBusModule());
|
|
||||||
moduleList.add(new EagerSingletonModule());
|
|
||||||
moduleList.add(ShiroWebModule.guiceFilterModule());
|
moduleList.add(ShiroWebModule.guiceFilterModule());
|
||||||
moduleList.add(new WebElementModule(pluginLoader));
|
moduleList.add(new WebElementModule(pluginLoader));
|
||||||
moduleList.add(new ScmServletModule(context, pluginLoader, overrides));
|
moduleList.add(new ScmServletModule(context, pluginLoader, overrides));
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ import sonia.scm.group.GroupDisplayManager;
|
|||||||
import sonia.scm.group.GroupManager;
|
import sonia.scm.group.GroupManager;
|
||||||
import sonia.scm.group.GroupManagerProvider;
|
import sonia.scm.group.GroupManagerProvider;
|
||||||
import sonia.scm.group.xml.XmlGroupDAO;
|
import sonia.scm.group.xml.XmlGroupDAO;
|
||||||
import sonia.scm.io.DefaultFileSystem;
|
|
||||||
import sonia.scm.io.FileSystem;
|
|
||||||
import sonia.scm.net.SSLContextProvider;
|
import sonia.scm.net.SSLContextProvider;
|
||||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||||
import sonia.scm.net.ahc.ContentTransformer;
|
import sonia.scm.net.ahc.ContentTransformer;
|
||||||
@@ -63,7 +61,6 @@ import sonia.scm.net.ahc.JsonContentTransformer;
|
|||||||
import sonia.scm.net.ahc.XmlContentTransformer;
|
import sonia.scm.net.ahc.XmlContentTransformer;
|
||||||
import sonia.scm.plugin.DefaultPluginLoader;
|
import sonia.scm.plugin.DefaultPluginLoader;
|
||||||
import sonia.scm.plugin.DefaultPluginManager;
|
import sonia.scm.plugin.DefaultPluginManager;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
|
||||||
import sonia.scm.plugin.PluginManager;
|
import sonia.scm.plugin.PluginManager;
|
||||||
import sonia.scm.repository.DefaultRepositoryManager;
|
import sonia.scm.repository.DefaultRepositoryManager;
|
||||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||||
@@ -87,23 +84,11 @@ import sonia.scm.schedule.QuartzScheduler;
|
|||||||
import sonia.scm.schedule.Scheduler;
|
import sonia.scm.schedule.Scheduler;
|
||||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||||
import sonia.scm.security.AuthorizationChangedEventProducer;
|
import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||||
import sonia.scm.security.CipherHandler;
|
|
||||||
import sonia.scm.security.CipherUtil;
|
|
||||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||||
import sonia.scm.security.DefaultKeyGenerator;
|
|
||||||
import sonia.scm.security.DefaultSecuritySystem;
|
import sonia.scm.security.DefaultSecuritySystem;
|
||||||
import sonia.scm.security.KeyGenerator;
|
|
||||||
import sonia.scm.security.LoginAttemptHandler;
|
import sonia.scm.security.LoginAttemptHandler;
|
||||||
import sonia.scm.security.SecuritySystem;
|
import sonia.scm.security.SecuritySystem;
|
||||||
import sonia.scm.store.BlobStoreFactory;
|
|
||||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
|
||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
|
||||||
import sonia.scm.store.DataStoreFactory;
|
|
||||||
import sonia.scm.store.FileBlobStoreFactory;
|
|
||||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
|
||||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
|
||||||
import sonia.scm.store.JAXBDataStoreFactory;
|
|
||||||
import sonia.scm.template.MustacheTemplateEngine;
|
import sonia.scm.template.MustacheTemplateEngine;
|
||||||
import sonia.scm.template.TemplateEngine;
|
import sonia.scm.template.TemplateEngine;
|
||||||
import sonia.scm.template.TemplateEngineFactory;
|
import sonia.scm.template.TemplateEngineFactory;
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
|||||||
|
|
||||||
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
|
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
|
||||||
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
|
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
|
||||||
if (RepositoryRolePermissions.read().isPermitted()) {
|
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
||||||
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class PermissionListDto extends HalRepresentation {
|
public class PermissionListDto extends HalRepresentation {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
private String[] permissions;
|
private String[] permissions;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class RepositoryRoleCollectionToDtoMapper extends BasicCollectionToDtoMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> createCreateLink() {
|
Optional<String> createCreateLink() {
|
||||||
return RepositoryRolePermissions.modify().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
|
return RepositoryRolePermissions.write().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
String createSelfLink() {
|
String createSelfLink() {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper
|
|||||||
@ObjectFactory
|
@ObjectFactory
|
||||||
RepositoryRoleDto createDto(RepositoryRole repositoryRole) {
|
RepositoryRoleDto createDto(RepositoryRole repositoryRole) {
|
||||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName()));
|
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName()));
|
||||||
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) {
|
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.write().isPermitted()) {
|
||||||
linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName())));
|
linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName())));
|
||||||
linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName())));
|
linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName())));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import sonia.scm.security.PermissionDescriptor;
|
|||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
@@ -69,7 +70,7 @@ public class UserPermissionResource {
|
|||||||
@ResponseCode(code = 500, condition = "internal server error")
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
})
|
})
|
||||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||||
public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
|
public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) {
|
||||||
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
|
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
|
||||||
.map(PermissionDescriptor::new)
|
.map(PermissionDescriptor::new)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
@@ -39,14 +39,18 @@ import com.google.inject.Module;
|
|||||||
import com.google.inject.assistedinject.FactoryModuleBuilder;
|
import com.google.inject.assistedinject.FactoryModuleBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.EagerSingletonModule;
|
||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
import sonia.scm.ScmContextListener;
|
import sonia.scm.ScmContextListener;
|
||||||
|
import sonia.scm.ScmEventBusModule;
|
||||||
|
import sonia.scm.ScmInitializerModule;
|
||||||
import sonia.scm.Stage;
|
import sonia.scm.Stage;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.plugin.DefaultPluginLoader;
|
import sonia.scm.plugin.DefaultPluginLoader;
|
||||||
import sonia.scm.plugin.Plugin;
|
import sonia.scm.plugin.Plugin;
|
||||||
import sonia.scm.plugin.PluginException;
|
import sonia.scm.plugin.PluginException;
|
||||||
import sonia.scm.plugin.PluginLoadException;
|
import sonia.scm.plugin.PluginLoadException;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
import sonia.scm.plugin.PluginWrapper;
|
import sonia.scm.plugin.PluginWrapper;
|
||||||
import sonia.scm.plugin.PluginsInternal;
|
import sonia.scm.plugin.PluginsInternal;
|
||||||
import sonia.scm.plugin.SmpArchive;
|
import sonia.scm.plugin.SmpArchive;
|
||||||
@@ -134,6 +138,19 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
|
|
||||||
File pluginDirectory = getPluginDirectory();
|
File pluginDirectory = getPluginDirectory();
|
||||||
|
|
||||||
|
createContextListener(pluginDirectory);
|
||||||
|
|
||||||
|
contextListener.contextInitialized(sce);
|
||||||
|
|
||||||
|
// register for restart events
|
||||||
|
if (!registered && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
|
||||||
|
logger.info("register for restart events");
|
||||||
|
ScmEventBus.getInstance().register(this);
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createContextListener(File pluginDirectory) {
|
||||||
try {
|
try {
|
||||||
if (!isCorePluginExtractionDisabled()) {
|
if (!isCorePluginExtractionDisabled()) {
|
||||||
extractCorePlugins(context, pluginDirectory);
|
extractCorePlugins(context, pluginDirectory);
|
||||||
@@ -145,12 +162,9 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
|
|
||||||
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
|
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
|
||||||
|
|
||||||
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
||||||
|
|
||||||
Module scmContextListenerModule = new ScmContextListenerModule();
|
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
|
||||||
BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader);
|
|
||||||
|
|
||||||
Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule);
|
|
||||||
|
|
||||||
processUpdates(pluginLoader, bootstrapInjector);
|
processUpdates(pluginLoader, bootstrapInjector);
|
||||||
|
|
||||||
@@ -158,19 +172,25 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new PluginLoadException("could not load plugins", ex);
|
throw new PluginLoadException("could not load plugins", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
contextListener.contextInitialized(sce);
|
|
||||||
|
|
||||||
// register for restart events
|
|
||||||
if (!registered
|
|
||||||
&& (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
|
|
||||||
logger.info("register for restart events");
|
|
||||||
ScmEventBus.getInstance().register(this);
|
|
||||||
registered = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processUpdates(DefaultPluginLoader pluginLoader, Injector bootstrapInjector) {
|
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
|
||||||
|
Module scmContextListenerModule = new ScmContextListenerModule();
|
||||||
|
BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader);
|
||||||
|
ScmInitializerModule scmInitializerModule = new ScmInitializerModule();
|
||||||
|
EagerSingletonModule eagerSingletonModule = new EagerSingletonModule();
|
||||||
|
ScmEventBusModule scmEventBusModule = new ScmEventBusModule();
|
||||||
|
|
||||||
|
return Guice.createInjector(
|
||||||
|
bootstrapModule,
|
||||||
|
scmContextListenerModule,
|
||||||
|
scmEventBusModule,
|
||||||
|
scmInitializerModule,
|
||||||
|
eagerSingletonModule
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) {
|
||||||
Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader));
|
Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader));
|
||||||
|
|
||||||
UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class);
|
UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class);
|
||||||
@@ -390,7 +410,6 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
private static class ScmContextListenerModule extends AbstractModule {
|
private static class ScmContextListenerModule extends AbstractModule {
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
|
|
||||||
install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class));
|
install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import sonia.scm.SCMContext;
|
|||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.io.DefaultFileSystem;
|
import sonia.scm.io.DefaultFileSystem;
|
||||||
import sonia.scm.io.FileSystem;
|
import sonia.scm.io.FileSystem;
|
||||||
import sonia.scm.plugin.DefaultPluginLoader;
|
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||||
@@ -33,7 +32,7 @@ public class BootstrapModule extends AbstractModule {
|
|||||||
private final ClassOverrides overrides;
|
private final ClassOverrides overrides;
|
||||||
private final PluginLoader pluginLoader;
|
private final PluginLoader pluginLoader;
|
||||||
|
|
||||||
BootstrapModule(DefaultPluginLoader pluginLoader) {
|
BootstrapModule(PluginLoader pluginLoader) {
|
||||||
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
||||||
this.pluginLoader = pluginLoader;
|
this.pluginLoader = pluginLoader;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
|
|
||||||
return managerDaoAdapter.create(
|
return managerDaoAdapter.create(
|
||||||
repositoryRole,
|
repositoryRole,
|
||||||
RepositoryRolePermissions::modify,
|
RepositoryRolePermissions::write,
|
||||||
newRepositoryRole -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepositoryRole),
|
newRepositoryRole -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepositoryRole),
|
||||||
newRepositoryRole -> fireEvent(HandlerEventType.CREATE, newRepositoryRole)
|
newRepositoryRole -> fireEvent(HandlerEventType.CREATE, newRepositoryRole)
|
||||||
);
|
);
|
||||||
@@ -100,7 +100,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||||
managerDaoAdapter.delete(
|
managerDaoAdapter.delete(
|
||||||
repositoryRole,
|
repositoryRole,
|
||||||
RepositoryRolePermissions::modify,
|
RepositoryRolePermissions::write,
|
||||||
toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete),
|
toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete),
|
||||||
toDelete -> fireEvent(HandlerEventType.DELETE, toDelete)
|
toDelete -> fireEvent(HandlerEventType.DELETE, toDelete)
|
||||||
);
|
);
|
||||||
@@ -116,7 +116,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||||
managerDaoAdapter.modify(
|
managerDaoAdapter.modify(
|
||||||
repositoryRole,
|
repositoryRole,
|
||||||
x -> RepositoryRolePermissions.modify(),
|
x -> RepositoryRolePermissions.write(),
|
||||||
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, repositoryRole, notModified),
|
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, repositoryRole, notModified),
|
||||||
notModified -> fireEvent(HandlerEventType.MODIFY, repositoryRole, notModified));
|
notModified -> fireEvent(HandlerEventType.MODIFY, repositoryRole, notModified));
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
public void refresh(RepositoryRole repositoryRole) {
|
public void refresh(RepositoryRole repositoryRole) {
|
||||||
logger.info("refresh repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
logger.info("refresh repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
|
||||||
|
|
||||||
RepositoryRolePermissions.read().check();
|
|
||||||
RepositoryRole fresh = repositoryRoleDAO.get(repositoryRole.getName());
|
RepositoryRole fresh = repositoryRoleDAO.get(repositoryRole.getName());
|
||||||
|
|
||||||
if (fresh == null) {
|
if (fresh == null) {
|
||||||
@@ -135,8 +134,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepositoryRole get(String id) {
|
public RepositoryRole get(String id) {
|
||||||
RepositoryRolePermissions.read().check();
|
|
||||||
|
|
||||||
return findSystemRole(id).orElse(findCustomRole(id));
|
return findSystemRole(id).orElse(findCustomRole(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,9 +165,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
|
|||||||
public List<RepositoryRole> getAll() {
|
public List<RepositoryRole> getAll() {
|
||||||
List<RepositoryRole> repositoryRoles = new ArrayList<>();
|
List<RepositoryRole> repositoryRoles = new ArrayList<>();
|
||||||
|
|
||||||
if (!RepositoryRolePermissions.read().isPermitted()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) {
|
for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) {
|
||||||
repositoryRoles.add(repositoryRole.clone());
|
repositoryRoles.add(repositoryRole.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package sonia.scm.update.group;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.group.Group;
|
||||||
|
import sonia.scm.group.xml.XmlGroupDAO;
|
||||||
|
import sonia.scm.migration.UpdateException;
|
||||||
|
import sonia.scm.migration.UpdateStep;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.StoreConstants;
|
||||||
|
import sonia.scm.update.properties.V1Properties;
|
||||||
|
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.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static sonia.scm.version.Version.parse;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class XmlGroupV1UpdateStep implements UpdateStep {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class);
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final XmlGroupDAO groupDAO;
|
||||||
|
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public XmlGroupV1UpdateStep(
|
||||||
|
SCMContextProvider contextProvider,
|
||||||
|
XmlGroupDAO groupDAO,
|
||||||
|
ConfigurationEntryStoreFactory configurationEntryStoreFactory
|
||||||
|
) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
this.groupDAO = groupDAO;
|
||||||
|
this.propertyStore = configurationEntryStoreFactory
|
||||||
|
.withType(V1Properties.class)
|
||||||
|
.withName("group-properties-v1")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doUpdate() throws JAXBException {
|
||||||
|
Optional<Path> v1GroupsFile = determineV1File();
|
||||||
|
if (!v1GroupsFile.isPresent()) {
|
||||||
|
LOG.info("no v1 file for groups found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get());
|
||||||
|
if (v1Database.groupList != null && v1Database.groupList.groups != null) {
|
||||||
|
v1Database.groupList.groups.forEach(this::update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return parse("2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.group.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(V1Group v1Group) {
|
||||||
|
LOG.debug("updating group {}", v1Group.name);
|
||||||
|
Group group = new Group(
|
||||||
|
v1Group.type,
|
||||||
|
v1Group.name,
|
||||||
|
v1Group.members);
|
||||||
|
group.setDescription(v1Group.description);
|
||||||
|
group.setCreationDate(v1Group.creationDate);
|
||||||
|
group.setLastModified(v1Group.lastModified);
|
||||||
|
groupDAO.add(group);
|
||||||
|
|
||||||
|
propertyStore.put(v1Group.name, v1Group.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException {
|
||||||
|
JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class);
|
||||||
|
return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Path> determineV1File() {
|
||||||
|
Path existingGroupsFile = resolveConfigFile("groups");
|
||||||
|
Path groupsV1File = resolveConfigFile("groupsV1");
|
||||||
|
if (existingGroupsFile.toFile().exists()) {
|
||||||
|
try {
|
||||||
|
Files.move(existingGroupsFile, groupsV1File);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not move old groups file to " + groupsV1File.toAbsolutePath());
|
||||||
|
}
|
||||||
|
LOG.info("moved old groups file to {}", groupsV1File.toAbsolutePath());
|
||||||
|
return of(groupsV1File);
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveConfigFile(String name) {
|
||||||
|
return contextProvider
|
||||||
|
.resolve(
|
||||||
|
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "group")
|
||||||
|
private static class V1Group {
|
||||||
|
private V1Properties properties;
|
||||||
|
private long creationDate;
|
||||||
|
private String description;
|
||||||
|
private Long lastModified;
|
||||||
|
private String name;
|
||||||
|
private String type;
|
||||||
|
@XmlElement(name = "members")
|
||||||
|
private List<String> members;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "V1Group{" +
|
||||||
|
"properties=" + properties +
|
||||||
|
", creationDate=" + creationDate + '\'' +
|
||||||
|
", description=" + description + '\'' +
|
||||||
|
", lastModified=" + lastModified + '\'' +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", type='" + type + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GroupList {
|
||||||
|
@XmlElement(name = "group")
|
||||||
|
private List<V1Group> groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlRootElement(name = "group-db")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
private static class V1GroupDatabase {
|
||||||
|
private long creationTime;
|
||||||
|
private Long lastModified;
|
||||||
|
@XmlElement(name = "groups")
|
||||||
|
private XmlGroupV1UpdateStep.GroupList groupList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package sonia.scm.update.properties;
|
||||||
|
|
||||||
|
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.util.List;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "properties")
|
||||||
|
public class V1Properties {
|
||||||
|
@XmlElement(name = "item")
|
||||||
|
private List<V1Property> properties;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package sonia.scm.update.properties;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class V1Property {
|
||||||
|
private String key;
|
||||||
|
private String value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.migration.UpdateException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
abstract class BaseMigrationStrategy implements MigrationStrategy.Instance {
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
BaseMigrationStrategy(SCMContextProvider contextProvider) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path getSourceDataPath(String name, String type) {
|
||||||
|
return Arrays.stream(name.split("/"))
|
||||||
|
.reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path getTypeDependentPath(String type) {
|
||||||
|
return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Path> listSourceDirectory(Path sourceDirectory) {
|
||||||
|
try {
|
||||||
|
return Files.list(sourceDirectory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not read original directory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void createDataDirectory(Path target) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(target);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not create data directory " + target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveFile(Path sourceFile, Path targetFile) {
|
||||||
|
try {
|
||||||
|
Files.move(sourceFile, targetFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyFile(Path sourceFile, Path targetFile) {
|
||||||
|
try {
|
||||||
|
Files.copy(sourceFile, targetFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not copy original file from " + sourceFile + " to " + targetFile, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
class CopyMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
private final RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||||
|
super(contextProvider);
|
||||||
|
this.locationResolver = locationResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path migrate(String id, String name, String type) {
|
||||||
|
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||||
|
Path targetDataPath = repositoryBasePath
|
||||||
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
|
Path sourceDataPath = getSourceDataPath(name, type);
|
||||||
|
copyData(sourceDataPath, targetDataPath);
|
||||||
|
return repositoryBasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyData(Path sourceDirectory, Path targetDirectory) {
|
||||||
|
createDataDirectory(targetDirectory);
|
||||||
|
listSourceDirectory(sourceDirectory).forEach(
|
||||||
|
sourceFile -> {
|
||||||
|
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||||
|
if (Files.isDirectory(sourceFile)) {
|
||||||
|
copyData(sourceFile, targetFile);
|
||||||
|
} else {
|
||||||
|
copyFile(sourceFile, targetFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
class InlineMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
|
||||||
|
super(contextProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path migrate(String id, String name, String type) {
|
||||||
|
Path repositoryBasePath = getSourceDataPath(name, type);
|
||||||
|
Path targetDataPath = repositoryBasePath
|
||||||
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
|
moveData(repositoryBasePath, targetDataPath);
|
||||||
|
return repositoryBasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||||
|
createDataDirectory(targetDirectory);
|
||||||
|
listSourceDirectory(sourceDirectory)
|
||||||
|
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
|
||||||
|
.forEach(
|
||||||
|
sourceFile -> {
|
||||||
|
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||||
|
if (Files.isDirectory(sourceFile)) {
|
||||||
|
moveData(sourceFile, targetFile);
|
||||||
|
} else {
|
||||||
|
moveFile(sourceFile, targetFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package sonia.scm.repository.update;
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
enum MigrationStrategy {
|
||||||
|
|
||||||
|
COPY(CopyMigrationStrategy.class),
|
||||||
|
MOVE(MoveMigrationStrategy.class),
|
||||||
|
INLINE(InlineMigrationStrategy.class);
|
||||||
|
|
||||||
|
private Class<? extends Instance> implementationClass;
|
||||||
|
|
||||||
|
MigrationStrategy(Class<? extends Instance> implementationClass) {
|
||||||
|
this.implementationClass = implementationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance from(Injector injector) {
|
||||||
|
return injector.getInstance(implementationClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Instance {
|
||||||
|
Path migrate(String id, String name, String type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.store.ConfigurationStore;
|
||||||
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class MigrationStrategyDao {
|
||||||
|
|
||||||
|
private final RepositoryMigrationPlan plan;
|
||||||
|
private final ConfigurationStore<RepositoryMigrationPlan> store;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
|
||||||
|
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
|
||||||
|
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MigrationStrategy> get(String id) {
|
||||||
|
return plan.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String repositoryId, MigrationStrategy strategy) {
|
||||||
|
plan.set(repositoryId, strategy);
|
||||||
|
store.set(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
|
class MoveMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class);
|
||||||
|
|
||||||
|
private final RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||||
|
super(contextProvider);
|
||||||
|
this.locationResolver = locationResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path migrate(String id, String name, String type) {
|
||||||
|
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
||||||
|
Path targetDataPath = repositoryBasePath
|
||||||
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
|
Path sourceDataPath = getSourceDataPath(name, type);
|
||||||
|
moveData(sourceDataPath, targetDataPath);
|
||||||
|
deleteOldDataDir(getTypeDependentPath(type), name);
|
||||||
|
return repositoryBasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteOldDataDir(Path rootPath, String name) {
|
||||||
|
delete(rootPath, asList(name.split("/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delete(Path rootPath, List<String> directories) {
|
||||||
|
if (directories.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path directory = rootPath.resolve(directories.get(0));
|
||||||
|
delete(directory, directories.subList(1, directories.size()));
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(directory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("could not delete source repository directory {}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||||
|
createDataDirectory(targetDirectory);
|
||||||
|
listSourceDirectory(sourceDirectory).forEach(
|
||||||
|
sourceFile -> {
|
||||||
|
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||||
|
if (Files.isDirectory(sourceFile)) {
|
||||||
|
moveData(sourceFile, targetFile);
|
||||||
|
} else {
|
||||||
|
moveFile(sourceFile, targetFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
Files.delete(sourceDirectory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("could not delete source repository directory {}", sourceDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "repository-migration")
|
||||||
|
class RepositoryMigrationPlan {
|
||||||
|
|
||||||
|
private List<RepositoryEntry> entries;
|
||||||
|
|
||||||
|
RepositoryMigrationPlan() {
|
||||||
|
this(new RepositoryEntry[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RepositoryMigrationPlan(RepositoryEntry... entries) {
|
||||||
|
this.entries = new ArrayList<>(asList(entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MigrationStrategy> get(String repositoryId) {
|
||||||
|
return findEntry(repositoryId)
|
||||||
|
.map(RepositoryEntry::getDataMigrationStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String repositoryId, MigrationStrategy strategy) {
|
||||||
|
Optional<RepositoryEntry> entry = findEntry(repositoryId);
|
||||||
|
if (entry.isPresent()) {
|
||||||
|
entry.get().setStrategy(strategy);
|
||||||
|
} else {
|
||||||
|
entries.add(new RepositoryEntry(repositoryId, strategy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<RepositoryEntry> findEntry(String repositoryId) {
|
||||||
|
return entries.stream()
|
||||||
|
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlRootElement(name = "entries")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
static class RepositoryEntry {
|
||||||
|
|
||||||
|
private String repositoryId;
|
||||||
|
private MigrationStrategy dataMigrationStrategy;
|
||||||
|
|
||||||
|
RepositoryEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
|
||||||
|
this.repositoryId = repositoryId;
|
||||||
|
this.dataMigrationStrategy = dataMigrationStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MigrationStrategy getDataMigrationStrategy() {
|
||||||
|
return dataMigrationStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStrategy(MigrationStrategy strategy) {
|
||||||
|
this.dataMigrationStrategy = strategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package sonia.scm.repository.update;
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
public class RepositoryUpdates {
|
public class RepositoryUpdates {
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
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.repository.xml.XmlRepositoryDAO;
|
||||||
|
import sonia.scm.store.StoreConstants;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static sonia.scm.version.Version.parse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an existing <code>repositories.xml</code> file to <code>repository-paths.xml</code>.
|
||||||
|
* Note that this has to run <em>after</em> an old v1 repository database has been migrated to v2
|
||||||
|
* (see {@link XmlRepositoryV1UpdateStep}).
|
||||||
|
*/
|
||||||
|
@Extension
|
||||||
|
public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final XmlRepositoryDAO repositoryDAO;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
this.repositoryDAO = repositoryDAO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
repositoryDAO.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return parse("2.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.repository.xml";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.migration.UpdateException;
|
||||||
|
import sonia.scm.migration.UpdateStep;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.StoreConstants;
|
||||||
|
import sonia.scm.update.properties.V1Properties;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.xml.bind.JAXBContext;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static sonia.scm.version.Version.parse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates SCM-Manager v1 repository data structure to SCM-Manager v2 data structure.
|
||||||
|
* That is:
|
||||||
|
* <ul>
|
||||||
|
* <li>The old <code>repositories.xml</code> file is read</li>
|
||||||
|
* <li>For each repository in this database,
|
||||||
|
* <ul>
|
||||||
|
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
|
||||||
|
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
|
||||||
|
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
|
||||||
|
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
|
||||||
|
* and
|
||||||
|
* <li>the new <code>metadata.xml</code> file is created.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Extension
|
||||||
|
public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
||||||
|
|
||||||
|
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final XmlRepositoryDAO repositoryDao;
|
||||||
|
private final MigrationStrategyDao migrationStrategyDao;
|
||||||
|
private final Injector injector;
|
||||||
|
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public XmlRepositoryV1UpdateStep(
|
||||||
|
SCMContextProvider contextProvider,
|
||||||
|
XmlRepositoryDAO repositoryDao,
|
||||||
|
MigrationStrategyDao migrationStrategyDao,
|
||||||
|
Injector injector,
|
||||||
|
ConfigurationEntryStoreFactory configurationEntryStoreFactory
|
||||||
|
) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
this.repositoryDao = repositoryDao;
|
||||||
|
this.migrationStrategyDao = migrationStrategyDao;
|
||||||
|
this.injector = injector;
|
||||||
|
this.propertyStore = configurationEntryStoreFactory
|
||||||
|
.withType(V1Properties.class)
|
||||||
|
.withName("repository-properties-v1")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return parse("2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.repository.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doUpdate() throws JAXBException {
|
||||||
|
if (!resolveV1File().exists()) {
|
||||||
|
LOG.info("no v1 repositories database file found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
|
||||||
|
readV1Database(jaxbContext).ifPresent(
|
||||||
|
v1Database -> {
|
||||||
|
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
|
||||||
|
v1Database.repositoryList.repositories.forEach(this::update);
|
||||||
|
backupOldRepositoriesFile();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backupOldRepositoriesFile() {
|
||||||
|
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||||
|
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||||
|
Path backupFile = configDir.resolve("repositories.xml.v1.backup");
|
||||||
|
LOG.info("moving old repositories database files to backup file {}", backupFile);
|
||||||
|
try {
|
||||||
|
Files.move(oldRepositoriesFile, backupFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not backup old repository database file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(V1Repository v1Repository) {
|
||||||
|
Path destination = handleDataDirectory(v1Repository);
|
||||||
|
Repository repository = new Repository(
|
||||||
|
v1Repository.id,
|
||||||
|
v1Repository.type,
|
||||||
|
getNamespace(v1Repository),
|
||||||
|
getName(v1Repository),
|
||||||
|
v1Repository.contact,
|
||||||
|
v1Repository.description,
|
||||||
|
createPermissions(v1Repository));
|
||||||
|
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
|
||||||
|
repositoryDao.add(repository, destination);
|
||||||
|
propertyStore.put(v1Repository.id, v1Repository.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path handleDataDirectory(V1Repository v1Repository) {
|
||||||
|
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
|
||||||
|
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
|
||||||
|
return migrationStrategyDao.get(v1Repository.id)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
|
||||||
|
if (v1Repository.permissions == null) {
|
||||||
|
return new RepositoryPermission[0];
|
||||||
|
}
|
||||||
|
return v1Repository.permissions
|
||||||
|
.stream()
|
||||||
|
.map(this::createPermission)
|
||||||
|
.toArray(RepositoryPermission[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryPermission createPermission(V1Permission v1Permission) {
|
||||||
|
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
|
||||||
|
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNamespace(V1Repository v1Repository) {
|
||||||
|
String[] nameParts = getNameParts(v1Repository.name);
|
||||||
|
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getName(V1Repository v1Repository) {
|
||||||
|
String[] nameParts = getNameParts(v1Repository.name);
|
||||||
|
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String concatPathElements(String[] nameParts) {
|
||||||
|
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getNameParts(String v1Name) {
|
||||||
|
return v1Name.split("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
|
||||||
|
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File());
|
||||||
|
if (unmarshal instanceof V1RepositoryDatabase) {
|
||||||
|
return of((V1RepositoryDatabase) unmarshal);
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File resolveV1File() {
|
||||||
|
return contextProvider
|
||||||
|
.resolve(
|
||||||
|
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION)
|
||||||
|
).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "permissions")
|
||||||
|
private static class V1Permission {
|
||||||
|
private boolean groupPermission;
|
||||||
|
private String name;
|
||||||
|
private String type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "repositories")
|
||||||
|
private static class V1Repository {
|
||||||
|
private String contact;
|
||||||
|
private long creationDate;
|
||||||
|
private Long lastModified;
|
||||||
|
private String description;
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private boolean isPublic;
|
||||||
|
private boolean archived;
|
||||||
|
private String type;
|
||||||
|
private List<V1Permission> permissions;
|
||||||
|
private V1Properties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "V1Repository{" +
|
||||||
|
", contact='" + contact + '\'' +
|
||||||
|
", creationDate=" + creationDate +
|
||||||
|
", lastModified=" + lastModified +
|
||||||
|
", description='" + description + '\'' +
|
||||||
|
", id='" + id + '\'' +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", isPublic=" + isPublic +
|
||||||
|
", archived=" + archived +
|
||||||
|
", type='" + type + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RepositoryList {
|
||||||
|
@XmlElement(name = "repository")
|
||||||
|
private List<V1Repository> repositories;
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlRootElement(name = "repository-db")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
private static class V1RepositoryDatabase {
|
||||||
|
private long creationTime;
|
||||||
|
private Long lastModified;
|
||||||
|
@XmlElement(name = "repositories")
|
||||||
|
private RepositoryList repositoryList;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package sonia.scm.update.security;
|
||||||
|
|
||||||
|
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.security.AssignedPermission;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.StoreConstants;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.xml.bind.JAXBContext;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
import static sonia.scm.version.Version.parse;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class XmlSecurityV1UpdateStep implements UpdateStep {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class);
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public XmlSecurityV1UpdateStep(SCMContextProvider contextProvider, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doUpdate() throws JAXBException {
|
||||||
|
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
|
||||||
|
|
||||||
|
forAllAdmins(user -> createSecurityEntry(user, false, securityStore),
|
||||||
|
group -> createSecurityEntry(group, true, securityStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException {
|
||||||
|
Path configDirectory = determineConfigDirectory();
|
||||||
|
Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION);
|
||||||
|
if (existingConfigFile.toFile().exists()) {
|
||||||
|
forAllAdmins(existingConfigFile, userConsumer, groupConsumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forAllAdmins(
|
||||||
|
Path existingConfigFile, Consumer<String> userConsumer, Consumer<String> groupConsumer
|
||||||
|
) throws JAXBException {
|
||||||
|
JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Configuration.class);
|
||||||
|
V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile());
|
||||||
|
|
||||||
|
ofNullable(v1Configuration.adminUsers).ifPresent(users -> forAll(users, userConsumer));
|
||||||
|
ofNullable(v1Configuration.adminGroups).ifPresent(groups -> forAll(groups, groupConsumer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void forAll(String entries, Consumer<String> consumer) {
|
||||||
|
Arrays.stream(entries.split(",")).forEach(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return parse("2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.security.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSecurityEntry(String name, boolean group, ConfigurationEntryStore<AssignedPermission> securityStore) {
|
||||||
|
LOG.debug("setting admin permissions for {} {}", group? "group": "user", name);
|
||||||
|
securityStore.put(new AssignedPermission(name, group, "*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
|
||||||
|
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path determineConfigDirectory() {
|
||||||
|
return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "scm-config")
|
||||||
|
private static class V1Configuration {
|
||||||
|
@XmlElement(name = "admin-users")
|
||||||
|
private String adminUsers;
|
||||||
|
@XmlElement(name = "admin-groups")
|
||||||
|
private String adminGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package sonia.scm.update.user;
|
||||||
|
|
||||||
|
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.security.AssignedPermission;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.StoreConstants;
|
||||||
|
import sonia.scm.update.properties.V1Properties;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
import sonia.scm.user.xml.XmlUserDAO;
|
||||||
|
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.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static sonia.scm.version.Version.parse;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class XmlUserV1UpdateStep implements UpdateStep {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class);
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final XmlUserDAO userDAO;
|
||||||
|
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
|
||||||
|
private final ConfigurationEntryStore<V1Properties> propertyStore;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public XmlUserV1UpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
this.userDAO = userDAO;
|
||||||
|
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
|
||||||
|
this.propertyStore = configurationEntryStoreFactory
|
||||||
|
.withType(V1Properties.class)
|
||||||
|
.withName("user-properties-v1")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doUpdate() throws JAXBException {
|
||||||
|
Optional<Path> v1UsersFile = determineV1File();
|
||||||
|
if (!v1UsersFile.isPresent()) {
|
||||||
|
LOG.info("no v1 file for users found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get());
|
||||||
|
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
|
||||||
|
v1Database.userList.users.forEach(user -> update(user, securityStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return parse("2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.user.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(V1User v1User, ConfigurationEntryStore<AssignedPermission> securityStore) {
|
||||||
|
LOG.debug("updating user {}", v1User.name);
|
||||||
|
User user = new User(
|
||||||
|
v1User.name,
|
||||||
|
v1User.displayName,
|
||||||
|
v1User.mail,
|
||||||
|
v1User.password,
|
||||||
|
v1User.type,
|
||||||
|
v1User.active);
|
||||||
|
user.setCreationDate(v1User.creationDate);
|
||||||
|
user.setLastModified(v1User.lastModified);
|
||||||
|
userDAO.add(user);
|
||||||
|
|
||||||
|
if (v1User.admin) {
|
||||||
|
LOG.debug("setting admin permissions for user {}", v1User.name);
|
||||||
|
securityStore.put(new AssignedPermission(v1User.name, "*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyStore.put(v1User.name, v1User.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private XmlUserV1UpdateStep.V1UserDatabase readV1Database(Path v1UsersFile) throws JAXBException {
|
||||||
|
JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class);
|
||||||
|
return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(v1UsersFile.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
|
||||||
|
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Path> determineV1File() {
|
||||||
|
Path existingUsersFile = resolveConfigFile("users");
|
||||||
|
Path usersV1File = resolveConfigFile("usersV1");
|
||||||
|
if (existingUsersFile.toFile().exists()) {
|
||||||
|
try {
|
||||||
|
Files.move(existingUsersFile, usersV1File);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath());
|
||||||
|
}
|
||||||
|
LOG.info("moved old users file to {}", usersV1File.toAbsolutePath());
|
||||||
|
return of(usersV1File);
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveConfigFile(String name) {
|
||||||
|
return contextProvider
|
||||||
|
.resolve(
|
||||||
|
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "user")
|
||||||
|
private static class V1User {
|
||||||
|
private V1Properties properties;
|
||||||
|
private boolean admin;
|
||||||
|
private long creationDate;
|
||||||
|
private String displayName;
|
||||||
|
private Long lastModified;
|
||||||
|
private String mail;
|
||||||
|
private String name;
|
||||||
|
private String password;
|
||||||
|
private String type;
|
||||||
|
private boolean active;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "V1User{" +
|
||||||
|
"properties=" + properties +
|
||||||
|
", admin='" + admin + '\'' +
|
||||||
|
", creationDate=" + creationDate + '\'' +
|
||||||
|
", displayName=" + displayName + '\'' +
|
||||||
|
", lastModified=" + lastModified + '\'' +
|
||||||
|
", mail='" + mail + '\'' +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", type='" + type + '\'' +
|
||||||
|
", active='" + active + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UserList {
|
||||||
|
@XmlElement(name = "user")
|
||||||
|
private List<V1User> users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlRootElement(name = "user-db")
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
private static class V1UserDatabase {
|
||||||
|
private long creationTime;
|
||||||
|
private Long lastModified;
|
||||||
|
@XmlElement(name = "users")
|
||||||
|
private XmlUserV1UpdateStep.UserList userList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.upgrade;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public final class ClientDateFormatConverter
|
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String SINGLECHAR_REGEX = "(^|[^%s])[%s]($|[^%s])";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for DateFormatConverter
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(ClientDateFormatConverter.class);
|
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs ...
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private ClientDateFormatConverter() {}
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Documentations:
|
|
||||||
* - Extjs: http://trac.geoext.org/browser/ext/3.4.0/docs/source/Date.html
|
|
||||||
* - Moments: http://momentjs.com/docs/#/displaying/format
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String extjsToMoments(String value)
|
|
||||||
{
|
|
||||||
logger.trace(
|
|
||||||
"try to convert extjs date format \"{}\" to moments date format", value);
|
|
||||||
|
|
||||||
String result = replaceDateFormatChars(value, "d", "DD");
|
|
||||||
|
|
||||||
result = replaceDateFormatChars(result, "D", "ddd");
|
|
||||||
result = replaceDateFormatChars(result, "j", "D");
|
|
||||||
result = replaceDateFormatChars(result, "l", "dddd");
|
|
||||||
|
|
||||||
// no replacement found for 1-7, only 0-6 found
|
|
||||||
result = replaceDateFormatChars(result, "N", "d");
|
|
||||||
result = replaceDateFormatChars(result, "w", "d");
|
|
||||||
result = replaceDateFormatChars(result, "z", "DDDD");
|
|
||||||
result = replaceDateFormatChars(result, "W", "ww");
|
|
||||||
result = replaceDateFormatChars(result, "M", "MMM");
|
|
||||||
result = replaceDateFormatChars(result, "F", "MMMM");
|
|
||||||
result = replaceDateFormatChars(result, "m", "MM");
|
|
||||||
result = replaceDateFormatChars(result, "n", "M");
|
|
||||||
result = replaceDateFormatChars(result, "Y", "YYYY");
|
|
||||||
result = replaceDateFormatChars(result, "o", "YYYY");
|
|
||||||
result = replaceDateFormatChars(result, "y", "YY");
|
|
||||||
result = replaceDateFormatChars(result, "H", "HH");
|
|
||||||
result = replaceDateFormatChars(result, "h", "hh");
|
|
||||||
result = replaceDateFormatChars(result, "g", "h");
|
|
||||||
result = replaceDateFormatChars(result, "G", "H");
|
|
||||||
result = replaceDateFormatChars(result, "i", "mm");
|
|
||||||
result = replaceDateFormatChars(result, "s", "ss");
|
|
||||||
result = replaceDateFormatChars(result, "O", "ZZ");
|
|
||||||
result = replaceDateFormatChars(result, "P", "Z");
|
|
||||||
result = replaceDateFormatChars(result, "T", "z");
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"converted extjs date format \"{}\" to moments date format \"{}\"",
|
|
||||||
value, result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
* @param c
|
|
||||||
* @param replacement
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private static String replaceDateFormatChars(String value, String c,
|
|
||||||
String replacement)
|
|
||||||
{
|
|
||||||
Pattern p = Pattern.compile(String.format(SINGLECHAR_REGEX, c, c, c));
|
|
||||||
StringBuffer buffer = new StringBuffer();
|
|
||||||
Matcher m = p.matcher(value);
|
|
||||||
|
|
||||||
while (m.find())
|
|
||||||
{
|
|
||||||
m.appendReplacement(buffer, "$1" + replacement + "$2");
|
|
||||||
}
|
|
||||||
|
|
||||||
m.appendTail(buffer);
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.upgrade;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.common.io.Files;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
|
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.version.Version;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class ClientDateFormatUpgradeHandler extends XmlUpgradeHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for ClientDateFormatUpgradeHandler
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(ClientDateFormatUpgradeHandler.class);
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param homeDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
* @param oldVersion
|
|
||||||
* @param newVersion
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void doUpgrade(File homeDirectory, File configDirectory,
|
|
||||||
Version oldVersion, Version newVersion)
|
|
||||||
{
|
|
||||||
if (oldVersion.isOlder("1.23"))
|
|
||||||
{
|
|
||||||
if (logger.isInfoEnabled())
|
|
||||||
{
|
|
||||||
logger.info("data format is older than 1.23, upgrade to version {}",
|
|
||||||
SCMContext.getContext().getVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClientDateFormat(homeDirectory, configDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param document
|
|
||||||
*/
|
|
||||||
private void fixClientDateFormat(Document document)
|
|
||||||
{
|
|
||||||
NodeList nodes = document.getElementsByTagName("dateFormat");
|
|
||||||
|
|
||||||
if (nodes != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < nodes.getLength(); i++)
|
|
||||||
{
|
|
||||||
Node node = nodes.item(i);
|
|
||||||
String value = node.getTextContent();
|
|
||||||
|
|
||||||
value = ClientDateFormatConverter.extjsToMoments(value);
|
|
||||||
node.setTextContent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param baseDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private void updateClientDateFormat(File baseDirectory, File configDirectory)
|
|
||||||
{
|
|
||||||
File configFile = new File(configDirectory, "config.xml");
|
|
||||||
|
|
||||||
if (configFile.exists())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
// backup config.xml
|
|
||||||
File backupDirectory = createBackupDirectory(baseDirectory,
|
|
||||||
"upgrade to version {0}");
|
|
||||||
|
|
||||||
Files.copy(configFile, new File(backupDirectory, "config.xml"));
|
|
||||||
|
|
||||||
// change dateformat
|
|
||||||
|
|
||||||
DocumentBuilder builder =
|
|
||||||
DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
|
||||||
Document document = builder.parse(configFile);
|
|
||||||
|
|
||||||
fixClientDateFormat(document);
|
|
||||||
writeDocument(document, configFile);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.error("could not parse document", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.upgrade;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
|
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.version.Version;
|
|
||||||
import sonia.scm.util.IOUtil;
|
|
||||||
import sonia.scm.util.Util;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
import javax.xml.transform.TransformerConfigurationException;
|
|
||||||
import javax.xml.transform.TransformerException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class TimestampUpgradeHandler extends XmlUpgradeHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for TimestampUpgradeHandler
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(TimestampUpgradeHandler.class);
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param homeDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
* @param oldVersion
|
|
||||||
* @param newVersion
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void doUpgrade(File homeDirectory, File configDirectory,
|
|
||||||
Version oldVersion, Version newVersion)
|
|
||||||
{
|
|
||||||
if (oldVersion.isOlder("1.2"))
|
|
||||||
{
|
|
||||||
if (logger.isInfoEnabled())
|
|
||||||
{
|
|
||||||
logger.info("data format is older than 1.2, upgrade to version {}",
|
|
||||||
SCMContext.getContext().getVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
fixDate(homeDirectory, configDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String convertDate(String value)
|
|
||||||
{
|
|
||||||
if (!Strings.isNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Date date = Util.parseDate(value);
|
|
||||||
|
|
||||||
if (date != null)
|
|
||||||
{
|
|
||||||
value = Long.toString(date.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ParseException ex)
|
|
||||||
{
|
|
||||||
logger.warn("could not parse date", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param baseDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
*/
|
|
||||||
private void fixDate(File baseDirectory, File configDirectory)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DocumentBuilder builder =
|
|
||||||
DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
|
||||||
File backupDirectory = createBackupDirectory(baseDirectory,
|
|
||||||
"upgrade to version {0}");
|
|
||||||
|
|
||||||
fixDate(builder, configDirectory, backupDirectory, "users.xml");
|
|
||||||
fixDate(builder, configDirectory, backupDirectory, "groups.xml");
|
|
||||||
fixDate(builder, configDirectory, backupDirectory, "repositories.xml");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.error("could not parse document", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param builder
|
|
||||||
* @param configDirectory
|
|
||||||
* @param backupDirectory
|
|
||||||
* @param filename
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
* @throws SAXException
|
|
||||||
* @throws TransformerConfigurationException
|
|
||||||
* @throws TransformerException
|
|
||||||
*/
|
|
||||||
private void fixDate(DocumentBuilder builder, File configDirectory,
|
|
||||||
File backupDirectory, String filename)
|
|
||||||
throws SAXException, IOException, TransformerConfigurationException,
|
|
||||||
TransformerException
|
|
||||||
{
|
|
||||||
File configFile = new File(configDirectory, filename);
|
|
||||||
File backupFile = new File(backupDirectory, filename);
|
|
||||||
|
|
||||||
IOUtil.copy(configFile, backupFile);
|
|
||||||
|
|
||||||
if (configFile.exists())
|
|
||||||
{
|
|
||||||
if (logger.isInfoEnabled())
|
|
||||||
{
|
|
||||||
logger.info("fix date elements of {}", configFile.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
Document document = builder.parse(configFile);
|
|
||||||
|
|
||||||
fixDate(document, "lastModified");
|
|
||||||
fixDate(document, "creationDate");
|
|
||||||
fixDate(document, "creationTime");
|
|
||||||
writeDocument(document, configFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param document
|
|
||||||
* @param element
|
|
||||||
*/
|
|
||||||
private void fixDate(Document document, String element)
|
|
||||||
{
|
|
||||||
NodeList nodes = document.getElementsByTagName(element);
|
|
||||||
|
|
||||||
if (nodes != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < nodes.getLength(); i++)
|
|
||||||
{
|
|
||||||
Node node = nodes.item(i);
|
|
||||||
String value = node.getTextContent();
|
|
||||||
|
|
||||||
value = convertDate(value);
|
|
||||||
node.setTextContent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.upgrade;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.common.base.Charsets;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.io.Files;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.version.Version;
|
|
||||||
import sonia.scm.util.IOUtil;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class UpgradeManager
|
|
||||||
{
|
|
||||||
|
|
||||||
/** the logger for ScmUpgradeHandler */
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(UpgradeManager.class);
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public void doUpgrade()
|
|
||||||
{
|
|
||||||
File baseDirectory = SCMContext.getContext().getBaseDirectory();
|
|
||||||
File configDirectory = new File(baseDirectory, "config");
|
|
||||||
File versionFile = new File(configDirectory, "version.txt");
|
|
||||||
|
|
||||||
if (configDirectory.exists())
|
|
||||||
{
|
|
||||||
boolean writeVersionFile = false;
|
|
||||||
|
|
||||||
String newVersion = SCMContext.getContext().getVersion();
|
|
||||||
|
|
||||||
if (versionFile.exists())
|
|
||||||
{
|
|
||||||
|
|
||||||
String oldVersion = getVersionString(versionFile);
|
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty(oldVersion) &&!oldVersion.equals(newVersion))
|
|
||||||
{
|
|
||||||
if (!newVersion.equals(oldVersion))
|
|
||||||
{
|
|
||||||
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
|
|
||||||
configDirectory, oldVersion, newVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
|
|
||||||
configDirectory, "1.1", newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writeVersionFile)
|
|
||||||
{
|
|
||||||
writeVersionFile(versionFile);
|
|
||||||
logger.info("upgrade to version {} was successful", newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
// fresh installation
|
|
||||||
IOUtil.mkdirs(configDirectory);
|
|
||||||
writeVersionFile(versionFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private List<UpgradeHandler> collectUpgradeHandlers()
|
|
||||||
{
|
|
||||||
|
|
||||||
List<UpgradeHandler> upgradeHandlers = Lists.newArrayList();
|
|
||||||
|
|
||||||
upgradeHandlers.add(new TimestampUpgradeHandler());
|
|
||||||
upgradeHandlers.add(new ClientDateFormatUpgradeHandler());
|
|
||||||
|
|
||||||
// TODO find upgrade handlers on classpath
|
|
||||||
return upgradeHandlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param baseDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
* @param versionString
|
|
||||||
* @param oldVersionString
|
|
||||||
* @param newVersionString
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private boolean doUpgradesForOldVersion(File baseDirectory,
|
|
||||||
File configDirectory, String oldVersionString, String newVersionString)
|
|
||||||
{
|
|
||||||
logger.info("start upgrade from version \"{}\" to \"{}\"",
|
|
||||||
oldVersionString, newVersionString);
|
|
||||||
|
|
||||||
boolean writeVersionFile = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Version oldVersion = Version.parse(oldVersionString);
|
|
||||||
Version newVersion = Version.parse(newVersionString);
|
|
||||||
|
|
||||||
doUpgradesForOldVersion(baseDirectory, configDirectory, oldVersion,
|
|
||||||
newVersion);
|
|
||||||
writeVersionFile = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.error("error upgrade failed", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return writeVersionFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param baseDirectory
|
|
||||||
* @param configDirectory
|
|
||||||
* @param version
|
|
||||||
* @param oldVersion
|
|
||||||
* @param newVersion
|
|
||||||
*/
|
|
||||||
private void doUpgradesForOldVersion(File baseDirectory,
|
|
||||||
File configDirectory, Version oldVersion, Version newVersion)
|
|
||||||
{
|
|
||||||
List<UpgradeHandler> upgradeHandlers = collectUpgradeHandlers();
|
|
||||||
|
|
||||||
for (UpgradeHandler upgradeHandler : upgradeHandlers)
|
|
||||||
{
|
|
||||||
logger.trace("call upgrade handler {}", upgradeHandler.getClass());
|
|
||||||
upgradeHandler.doUpgrade(baseDirectory, configDirectory, oldVersion,
|
|
||||||
newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param versionFile
|
|
||||||
*/
|
|
||||||
private void writeVersionFile(File versionFile)
|
|
||||||
{
|
|
||||||
OutputStream output = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
output = new FileOutputStream(versionFile);
|
|
||||||
output.write(SCMContext.getContext().getVersion().getBytes());
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.error("could not write version file", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IOUtil.close(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param versionFile
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String getVersionString(File versionFile)
|
|
||||||
{
|
|
||||||
String version = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
version = Files.toString(versionFile, Charsets.UTF_8).trim();
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.error("could not read version file", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer.
|
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from this
|
|
||||||
* software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.upgrade;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
|
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.util.IOUtil;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import javax.xml.transform.OutputKeys;
|
|
||||||
import javax.xml.transform.Transformer;
|
|
||||||
import javax.xml.transform.TransformerConfigurationException;
|
|
||||||
import javax.xml.transform.TransformerException;
|
|
||||||
import javax.xml.transform.TransformerFactory;
|
|
||||||
import javax.xml.transform.dom.DOMSource;
|
|
||||||
import javax.xml.transform.stream.StreamResult;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public abstract class XmlUpgradeHandler implements UpgradeHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for XmlUpgradeHandler
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(XmlUpgradeHandler.class);
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param baseDirectory
|
|
||||||
* @param note
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected File createBackupDirectory(File baseDirectory, String note)
|
|
||||||
{
|
|
||||||
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
|
||||||
File backupDirectory = new File(baseDirectory,
|
|
||||||
"backups".concat(File.separator).concat(date));
|
|
||||||
|
|
||||||
IOUtil.mkdirs(backupDirectory);
|
|
||||||
|
|
||||||
FileWriter writer = null;
|
|
||||||
|
|
||||||
note = MessageFormat.format(note, SCMContext.getContext().getVersion());
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
writer = new FileWriter(new File(backupDirectory, "note.txt"));
|
|
||||||
writer.write(note);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.error("could not write note.txt for backup", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IOUtil.close(writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return backupDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param document
|
|
||||||
* @param configFile
|
|
||||||
*
|
|
||||||
* @throws TransformerConfigurationException
|
|
||||||
* @throws TransformerException
|
|
||||||
*/
|
|
||||||
protected void writeDocument(Document document, File configFile)
|
|
||||||
throws TransformerConfigurationException, TransformerException
|
|
||||||
{
|
|
||||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
|
||||||
|
|
||||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
|
||||||
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
|
|
||||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
|
||||||
transformer.transform(new DOMSource(document),
|
|
||||||
new StreamResult(configFile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<value>configuration:read,write:*</value>
|
<value>configuration:read,write:*</value>
|
||||||
</permission>
|
</permission>
|
||||||
<permission>
|
<permission>
|
||||||
<value>repositoryRole:read,write</value>
|
<value>repositoryRole:write</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
|
||||||
</permissions>
|
</permissions>
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ class DefaultRepositoryRoleManagerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void authorizeUser() {
|
void authorizeUser() {
|
||||||
when(subject.isPermitted("repositoryRole:read")).thenReturn(true);
|
when(subject.isPermitted("repositoryRole:write")).thenReturn(true);
|
||||||
when(subject.isPermitted("repositoryRole:modify")).thenReturn(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -184,8 +183,15 @@ class DefaultRepositoryRoleManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowException_forGet() {
|
void shouldReturnNull_forNotExistingRole() {
|
||||||
assertThrows(UnauthorizedException.class, () -> manager.get("any"));
|
RepositoryRole role = manager.get("noSuchRole");
|
||||||
|
assertThat(role).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnRole_forExistingRole() {
|
||||||
|
RepositoryRole role = manager.get(CUSTOM_ROLE_NAME);
|
||||||
|
assertThat(role).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -201,18 +207,25 @@ class DefaultRepositoryRoleManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyList() {
|
void shouldReturnAllRoles() {
|
||||||
assertThat(manager.getAll()).isEmpty();
|
List<RepositoryRole> allRoles = manager.getAll();
|
||||||
|
assertThat(allRoles).containsExactly(CUSTOM_ROLE, SYSTEM_ROLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyFilteredList() {
|
void shouldReturnFilteredList() {
|
||||||
assertThat(manager.getAll(x -> true, null)).isEmpty();
|
Collection<RepositoryRole> allRoles = manager.getAll(role -> CUSTOM_ROLE_NAME.equals(role.getName()), null);
|
||||||
|
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyPaginatedList() {
|
void shouldReturnPaginatedRoles() {
|
||||||
assertThat(manager.getAll(1, 1)).isEmpty();
|
Collection<RepositoryRole> allRoles =
|
||||||
|
manager.getAll(
|
||||||
|
Comparator.comparing(RepositoryRole::getType),
|
||||||
|
1, 1
|
||||||
|
);
|
||||||
|
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sonia.scm.update;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.security.AssignedPermission;
|
||||||
|
import sonia.scm.security.DefaultKeyGenerator;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
|
||||||
|
public class UpdateStepTestUtil {
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
private final Path tempDir;
|
||||||
|
private final ConfigurationEntryStoreFactory storeFactory;
|
||||||
|
|
||||||
|
public UpdateStepTestUtil(Path tempDir) {
|
||||||
|
this.tempDir = tempDir;
|
||||||
|
contextProvider = Mockito.mock(SCMContextProvider.class);
|
||||||
|
storeFactory = new JAXBConfigurationEntryStoreFactory(contextProvider, null, new DefaultKeyGenerator());
|
||||||
|
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SCMContextProvider getContextProvider() {
|
||||||
|
return contextProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigurationEntryStoreFactory getStoreFactory() {
|
||||||
|
return storeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyConfigFile(String fileName) throws IOException {
|
||||||
|
Path configDir = tempDir.resolve("config");
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
copyTestDatabaseFile(configDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyConfigFile(String fileName, String targetFileName) throws IOException {
|
||||||
|
Path configDir = tempDir.resolve("config");
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
copyTestDatabaseFile(configDir, fileName, targetFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigurationEntryStore<AssignedPermission> getStoreForConfigFile(String name) {
|
||||||
|
return storeFactory
|
||||||
|
.withType(AssignedPermission.class)
|
||||||
|
.withName(name)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getFile(String name) {
|
||||||
|
return tempDir.resolve("config").resolve(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
|
||||||
|
Path targetFileName = Paths.get(fileName).getFileName();
|
||||||
|
copyTestDatabaseFile(configDir, fileName, targetFileName.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyTestDatabaseFile(Path configDir, String fileName, String targetFileName) throws IOException {
|
||||||
|
URL url = Resources.getResource(fileName);
|
||||||
|
Files.copy(url.openStream(), configDir.resolve(targetFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package sonia.scm.update.group;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.group.Group;
|
||||||
|
import sonia.scm.group.xml.XmlGroupDAO;
|
||||||
|
import sonia.scm.update.UpdateStepTestUtil;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.linesOf;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class XmlGroupV1UpdateStepTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
XmlGroupDAO groupDAO;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
ArgumentCaptor<Group> groupCaptor;
|
||||||
|
|
||||||
|
XmlGroupV1UpdateStep updateStep;
|
||||||
|
|
||||||
|
private UpdateStepTestUtil testUtil;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
testUtil = new UpdateStepTestUtil(tempDir);
|
||||||
|
updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, testUtil.getStoreFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabase {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void captureStoredRepositories() {
|
||||||
|
doNothing().when(groupDAO).add(groupCaptor.capture());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createGroupV1XML() throws IOException {
|
||||||
|
testUtil.copyConfigFile("sonia/scm/update/group/groups.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
verify(groupDAO, times(2)).add(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapAttributesFromGroupsV1Xml() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
Optional<Group> group = groupCaptor.getAllValues().stream().filter(u -> u.getName().equals("normals")).findFirst();
|
||||||
|
assertThat(group)
|
||||||
|
.get()
|
||||||
|
.hasFieldOrPropertyWithValue("name", "normals")
|
||||||
|
.hasFieldOrPropertyWithValue("description", "Normal people")
|
||||||
|
.hasFieldOrPropertyWithValue("type", "xml")
|
||||||
|
.hasFieldOrPropertyWithValue("members", asList("trillian", "dent"))
|
||||||
|
.hasFieldOrPropertyWithValue("lastModified", 1559550955883L)
|
||||||
|
.hasFieldOrPropertyWithValue("creationDate", 1559548942457L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractProperties() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
Path propertiesFile = testUtil.getFile("group-properties-v1.xml");
|
||||||
|
assertThat(propertiesFile)
|
||||||
|
.exists();
|
||||||
|
assertThat(linesOf(propertiesFile.toFile()))
|
||||||
|
.extracting(String::trim)
|
||||||
|
.containsSequence(
|
||||||
|
"<key>normals</key>",
|
||||||
|
"<value>",
|
||||||
|
"<item>",
|
||||||
|
"<key>mostly</key>",
|
||||||
|
"<value>humans</value>",
|
||||||
|
"</item>",
|
||||||
|
"</value>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabaseWithEmptyList {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createGroupV1XML() throws IOException {
|
||||||
|
testUtil.copyConfigFile("sonia/scm/update/group/groups_empty_groups.xml", "groups.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
verify(groupDAO, times(0)).add(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabaseWithoutList {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createGroupV1XML() throws IOException {
|
||||||
|
testUtil.copyConfigFile("sonia/scm/update/group/groups_no_groups.xml", "groups.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
verify(groupDAO, times(0)).add(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailForMissingConfigDir() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CopyMigrationStrategyTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SCMContextProvider contextProvider;
|
||||||
|
@Mock
|
||||||
|
RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||||
|
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||||
|
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||||
|
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||||
|
assertThat(target.resolve("data")).exists();
|
||||||
|
Path originalDataDir = tempDir
|
||||||
|
.resolve("repositories")
|
||||||
|
.resolve("git")
|
||||||
|
.resolve("some")
|
||||||
|
.resolve("more")
|
||||||
|
.resolve("directories")
|
||||||
|
.resolve("than")
|
||||||
|
.resolve("one");
|
||||||
|
assertDirectoriesEqual(target.resolve("data"), originalDataDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertDirectoriesEqual(Path targetDataDir, Path originalDataDir) {
|
||||||
|
Stream<Path> list = null;
|
||||||
|
try {
|
||||||
|
list = Files.list(originalDataDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
fail("could not read original directory", e);
|
||||||
|
}
|
||||||
|
list.forEach(
|
||||||
|
original -> {
|
||||||
|
Path expectedTarget = targetDataDir.resolve(original.getFileName());
|
||||||
|
assertThat(expectedTarget).exists();
|
||||||
|
if (Files.isDirectory(original)) {
|
||||||
|
assertDirectoriesEqual(expectedTarget, original);
|
||||||
|
} else {
|
||||||
|
assertThat(expectedTarget).hasSameContentAs(original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package sonia.scm.repository.update;
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
import com.google.common.io.Resources;
|
import com.google.common.io.Resources;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
@@ -42,7 +42,7 @@ class MigrateVerbsToPermissionRolesTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
|
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
URL metadataUrl = Resources.getResource("sonia/scm/repository/update/metadataWithoutRoles.xml");
|
URL metadataUrl = Resources.getResource("sonia/scm/update/repository/metadataWithoutRoles.xml");
|
||||||
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
|
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
((BiConsumer<String, Path>) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir);
|
((BiConsumer<String, Path>) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
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 sonia.scm.update.repository.MigrationStrategy;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategyDao;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class MigrationStrategyDaoTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
private ConfigurationStoreFactory storeFactory;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initStore(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
|
||||||
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
|
Optional<MigrationStrategy> strategy = dao.get("any");
|
||||||
|
|
||||||
|
Assertions.assertThat(strategy).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNewValue() throws JAXBException {
|
||||||
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
|
dao.set("id", INLINE);
|
||||||
|
|
||||||
|
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||||
|
|
||||||
|
Assertions.assertThat(strategy).contains(INLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabase {
|
||||||
|
@BeforeEach
|
||||||
|
void initExistingDatabase() throws JAXBException {
|
||||||
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
|
dao.set("id", INLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindExistingValue() throws JAXBException {
|
||||||
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
|
Optional<MigrationStrategy> strategy = dao.get("id");
|
||||||
|
|
||||||
|
Assertions.assertThat(strategy).contains(INLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategy.Instance;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class MigrationStrategyMock {
|
||||||
|
|
||||||
|
static Injector init() {
|
||||||
|
Map<Class, Instance> mocks = new HashMap<>();
|
||||||
|
Injector mock = mock(Injector.class);
|
||||||
|
when(
|
||||||
|
mock.getInstance(any(Class.class)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
|
||||||
|
);
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class MoveMigrationStrategyTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SCMContextProvider contextProvider;
|
||||||
|
@Mock
|
||||||
|
RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||||
|
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||||
|
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||||
|
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||||
|
assertThat(target.resolve("data")).exists();
|
||||||
|
Path originalDataDir = tempDir
|
||||||
|
.resolve("repositories")
|
||||||
|
.resolve("git")
|
||||||
|
.resolve("some");
|
||||||
|
assertThat(originalDataDir).doesNotExist();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.repository.spi.ZippedRepositoryTestBase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
class V1RepositoryFileSystem {
|
||||||
|
/**
|
||||||
|
* Creates the following v1 repositories in the temp dir:
|
||||||
|
* <pre>
|
||||||
|
* <repository>
|
||||||
|
* <properties/>
|
||||||
|
* <contact>arthur@dent.uk</contact>
|
||||||
|
* <creationDate>1558423492071</creationDate>
|
||||||
|
* <description>A repository with two folders.</description>
|
||||||
|
* <id>3b91caa5-59c3-448f-920b-769aaa56b761</id>
|
||||||
|
* <name>one/directory</name>
|
||||||
|
* <public>false</public>
|
||||||
|
* <archived>false</archived>
|
||||||
|
* <type>git</type>
|
||||||
|
* </repository>
|
||||||
|
* <repository>
|
||||||
|
* <properties/>
|
||||||
|
* <contact>arthur@dent.uk</contact>
|
||||||
|
* <creationDate>1558423543716</creationDate>
|
||||||
|
* <description>A repository in deeply nested folders.</description>
|
||||||
|
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
|
||||||
|
* <name>some/more/directories/than/one</name>
|
||||||
|
* <public>false</public>
|
||||||
|
* <archived>false</archived>
|
||||||
|
* <type>git</type>
|
||||||
|
* </repository>
|
||||||
|
* <repository>
|
||||||
|
* <properties/>
|
||||||
|
* <contact>arthur@dent.uk</contact>
|
||||||
|
* <creationDate>1558423440258</creationDate>
|
||||||
|
* <description>A simple repository without directories.</description>
|
||||||
|
* <id>454972da-faf9-4437-b682-dc4a4e0aa8eb</id>
|
||||||
|
* <lastModified>1558425918578</lastModified>
|
||||||
|
* <name>simple</name>
|
||||||
|
* <permissions>
|
||||||
|
* <groupPermission>true</groupPermission>
|
||||||
|
* <name>mice</name>
|
||||||
|
* <type>WRITE</type>
|
||||||
|
* </permissions>
|
||||||
|
* <permissions>
|
||||||
|
* <groupPermission>false</groupPermission>
|
||||||
|
* <name>dent</name>
|
||||||
|
* <type>OWNER</type>
|
||||||
|
* </permissions>
|
||||||
|
* <permissions>
|
||||||
|
* <groupPermission>false</groupPermission>
|
||||||
|
* <name>trillian</name>
|
||||||
|
* <type>READ</type>
|
||||||
|
* </permissions>
|
||||||
|
* <public>false</public>
|
||||||
|
* <archived>false</archived>
|
||||||
|
* <type>git</type>
|
||||||
|
* <url>http://localhost:8081/scm/git/simple</url>
|
||||||
|
* </repository>
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
static void createV1Home(Path tempDir) throws IOException {
|
||||||
|
ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/update/repository/scm-home.v1.zip");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
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 sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
|
|
||||||
|
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.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class XmlRepositoryFileNameUpdateStepTest {
|
||||||
|
|
||||||
|
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
|
||||||
|
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
|
||||||
|
URL url = Resources.getResource("sonia/scm/update/repository/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();
|
||||||
|
verify(repositoryDAO).refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.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 sonia.scm.update.UpdateStepTestUtil;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.update.repository.MigrationStrategy.COPY;
|
||||||
|
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
|
||||||
|
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class XmlRepositoryV1UpdateStepTest {
|
||||||
|
|
||||||
|
Injector injectorMock = MigrationStrategyMock.init();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
XmlRepositoryDAO repositoryDAO;
|
||||||
|
@Mock
|
||||||
|
MigrationStrategyDao migrationStrategyDao;
|
||||||
|
|
||||||
|
ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore());
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
ArgumentCaptor<Repository> storeCaptor;
|
||||||
|
@Captor
|
||||||
|
ArgumentCaptor<Path> locationCaptor;
|
||||||
|
|
||||||
|
UpdateStepTestUtil testUtil;
|
||||||
|
|
||||||
|
XmlRepositoryV1UpdateStep updateStep;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createUpdateStepFromMocks(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
testUtil = new UpdateStepTestUtil(tempDir);
|
||||||
|
updateStep = new XmlRepositoryV1UpdateStep(
|
||||||
|
testUtil.getContextProvider(),
|
||||||
|
repositoryDAO,
|
||||||
|
migrationStrategyDao,
|
||||||
|
injectorMock,
|
||||||
|
configurationEntryStoreFactory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabase {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void captureStoredRepositories() {
|
||||||
|
lenient().doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createMigrationPlan() {
|
||||||
|
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
|
||||||
|
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
|
||||||
|
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewRepositories() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
verify(repositoryDAO, times(3)).add(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapAttributes() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
Optional<Repository> repository = findByNamespace("git");
|
||||||
|
|
||||||
|
assertThat(repository)
|
||||||
|
.get()
|
||||||
|
.hasFieldOrPropertyWithValue("type", "git")
|
||||||
|
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
||||||
|
.hasFieldOrPropertyWithValue("description", "A simple repository without directories.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
Optional<Repository> repository = findByNamespace("git");
|
||||||
|
|
||||||
|
assertThat(repository)
|
||||||
|
.get()
|
||||||
|
.hasFieldOrPropertyWithValue("namespace", "git")
|
||||||
|
.hasFieldOrPropertyWithValue("name", "simple");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
Optional<Repository> repository = findByNamespace("one");
|
||||||
|
|
||||||
|
assertThat(repository)
|
||||||
|
.get()
|
||||||
|
.hasFieldOrPropertyWithValue("namespace", "one")
|
||||||
|
.hasFieldOrPropertyWithValue("name", "directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
Optional<Repository> repository = findByNamespace("some");
|
||||||
|
|
||||||
|
assertThat(repository)
|
||||||
|
.get()
|
||||||
|
.hasFieldOrPropertyWithValue("namespace", "some")
|
||||||
|
.hasFieldOrPropertyWithValue("name", "more_directories_than_one");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapPermissions() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
Optional<Repository> repository = findByNamespace("git");
|
||||||
|
|
||||||
|
assertThat(repository.get().getPermissions())
|
||||||
|
.hasSize(3)
|
||||||
|
.contains(
|
||||||
|
new RepositoryPermission("mice", "WRITE", true),
|
||||||
|
new RepositoryPermission("dent", "OWNER", false),
|
||||||
|
new RepositoryPermission("trillian", "READ", false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractPropertiesFromRepositories() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
ConfigurationEntryStore<Object> store = configurationEntryStoreFactory.withType(null).withName("").build();
|
||||||
|
assertThat(store.getAll())
|
||||||
|
.hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||||
|
Path targetDir = tempDir.resolve("someDir");
|
||||||
|
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class);
|
||||||
|
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir);
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(locationCaptor.getAllValues()).contains(targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailForMissingMigrationStrategy() {
|
||||||
|
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
|
||||||
|
assertThrows(IllegalStateException.class, () -> updateStep.doUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBackupOldRepositoryDatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(tempDir.resolve("config").resolve("repositories.xml")).doesNotExist();
|
||||||
|
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailIfNoOldDatabaseExists() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailIfFormerV2DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||||
|
createFormerV2RepositoriesFile(tempDir);
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotBackupFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
||||||
|
createFormerV2RepositoriesFile(tempDir);
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(tempDir.resolve("config").resolve("repositories.xml")).exists();
|
||||||
|
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
||||||
|
Path configDir = tempDir.resolve("config");
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Repository> findByNamespace(String namespace) {
|
||||||
|
return storeCaptor.getAllValues().stream().filter(r -> r.getNamespace().equals(namespace)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package sonia.scm.update.security;
|
||||||
|
|
||||||
|
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.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.security.AssignedPermission;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationEntryStore;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||||
|
import sonia.scm.update.security.XmlSecurityV1UpdateStep;
|
||||||
|
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class XmlSecurityV1UpdateStepTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
XmlSecurityV1UpdateStep updateStep;
|
||||||
|
ConfigurationEntryStore<AssignedPermission> assignedPermissionStore;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
|
assignedPermissionStore = new InMemoryConfigurationEntryStore<>();
|
||||||
|
ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore);
|
||||||
|
updateStep = new XmlSecurityV1UpdateStep(contextProvider, inMemoryConfigurationEntryStoreFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithExistingDatabase {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createConfigV1XML(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path configDir = tempDir.resolve("config");
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
copyTestDatabaseFile(configDir, "config.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
List<String> assignedPermission =
|
||||||
|
assignedPermissionStore.getAll().values()
|
||||||
|
.stream()
|
||||||
|
.filter(a -> a.getPermission().getValue().equals("*"))
|
||||||
|
.filter(a -> !a.isGroupPermission())
|
||||||
|
.map(AssignedPermission::getName)
|
||||||
|
.collect(toList());
|
||||||
|
assertThat(assignedPermission).contains("arthur", "dent", "ldap-admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreatePermissionForGroupsConfiguredAsAdmin() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
List<String> assignedPermission =
|
||||||
|
assignedPermissionStore.getAll().values()
|
||||||
|
.stream()
|
||||||
|
.filter(a -> a.getPermission().getValue().equals("*"))
|
||||||
|
.filter(AssignedPermission::isGroupPermission)
|
||||||
|
.map(AssignedPermission::getName)
|
||||||
|
.collect(toList());
|
||||||
|
assertThat(assignedPermission).contains("admins", "vogons");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
|
||||||
|
URL url = Resources.getResource("sonia/scm/update/security/" + fileName);
|
||||||
|
Files.copy(url.openStream(), configDir.resolve(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailForMissingConfigDir() throws JAXBException {
|
||||||
|
updateStep.doUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user