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:
@@ -33,14 +33,16 @@ package sonia.scm.boot;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import sonia.scm.Stage;
|
|
||||||
import sonia.scm.event.Event;
|
import sonia.scm.event.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event can be used to force a restart of the webapp context. The restart
|
* This event can be used to force a restart of the webapp context. The restart
|
||||||
* event is useful during plugin development, because we don't have to restart
|
* event is useful during plugin development, because we don't have to restart
|
||||||
* the whole server, to see our changes. The restart event can only be used in
|
* the whole server, to see our changes. The restart event could also be used
|
||||||
* stage {@link Stage#DEVELOPMENT}.
|
* to install or upgrade plugins.
|
||||||
|
*
|
||||||
|
* But the restart event should be used carefully, because the whole context
|
||||||
|
* will be restarted and that process could take some time.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
|
|||||||
@@ -13,8 +13,27 @@ public abstract class RepositoryLocationResolver {
|
|||||||
return create(type);
|
return create(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface RepositoryLocationResolverInstance<T> {
|
public interface RepositoryLocationResolverInstance<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the existing location for the repository.
|
||||||
|
* @param repositoryId The id of the repository.
|
||||||
|
* @throws IllegalStateException when there is no known location for the given repository.
|
||||||
|
*/
|
||||||
T getLocation(String repositoryId);
|
T getLocation(String repositoryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new location for the new repository.
|
||||||
|
* @param repositoryId The id of the new repository.
|
||||||
|
* @throws IllegalStateException when there already is a location for the given repository registered.
|
||||||
|
*/
|
||||||
|
T createLocation(String repositoryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the location of a new repository.
|
||||||
|
* @param repositoryId The id of the new repository.
|
||||||
|
* @throws IllegalStateException when there already is a location for the given repository registered.
|
||||||
|
*/
|
||||||
|
void setLocation(String repositoryId, T location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler {
|
|||||||
* @return decrypted value
|
* @return decrypted value
|
||||||
*/
|
*/
|
||||||
public String decode(char[] plainKey, String value) {
|
public String decode(char[] plainKey, String value) {
|
||||||
String result = null;
|
Base64.Decoder decoder = Base64.getUrlDecoder();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] encodedInput = Base64.getUrlDecoder().decode(value);
|
return decode(plainKey, value, decoder);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return decode(plainKey, value, Base64.getDecoder());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decode(char[] plainKey, String value, Base64.Decoder decoder) {
|
||||||
|
try {
|
||||||
|
byte[] encodedInput = decoder.decode(value);
|
||||||
byte[] salt = new byte[SALT_LENGTH];
|
byte[] salt = new byte[SALT_LENGTH];
|
||||||
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
|
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
|
||||||
|
|
||||||
@@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler {
|
|||||||
|
|
||||||
byte[] decoded = cipher.doFinal(encoded);
|
byte[] decoded = cipher.doFinal(encoded);
|
||||||
|
|
||||||
result = new String(decoded, ENCODING);
|
return new String(decoded, ENCODING);
|
||||||
} catch (IOException | GeneralSecurityException ex) {
|
} catch (IOException | GeneralSecurityException ex) {
|
||||||
throw new CipherException("could not decode string", ex);
|
throw new CipherException("could not decode string", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable
|
|||||||
@Override
|
@Override
|
||||||
public int hashCode()
|
public int hashCode()
|
||||||
{
|
{
|
||||||
return value.hashCode();
|
return value == null? -1: value.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ public class ValidationUtilTest
|
|||||||
public void testIsRepositoryNameValid() {
|
public void testIsRepositoryNameValid() {
|
||||||
String[] validPaths = {
|
String[] validPaths = {
|
||||||
"scm",
|
"scm",
|
||||||
|
"scm-",
|
||||||
|
"scm_",
|
||||||
|
"s_cm",
|
||||||
|
"s-cm",
|
||||||
"s",
|
"s",
|
||||||
"sc",
|
"sc",
|
||||||
".hiddenrepo",
|
".hiddenrepo",
|
||||||
@@ -206,7 +210,8 @@ public class ValidationUtilTest
|
|||||||
"a/..b",
|
"a/..b",
|
||||||
"scm/main",
|
"scm/main",
|
||||||
"scm/plugins/git-plugin",
|
"scm/plugins/git-plugin",
|
||||||
"scm/plugins/git-plugin"
|
"_scm",
|
||||||
|
"-scm"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String path : validPaths) {
|
for (String path : validPaths) {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package sonia.scm.repository.xml;
|
package sonia.scm.repository.xml;
|
||||||
|
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.io.FileSystem;
|
||||||
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
||||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.store.StoreConstants;
|
import sonia.scm.store.StoreConstants;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import javax.inject.Singleton;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -28,12 +28,14 @@ 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> {
|
||||||
|
|
||||||
public static final String STORE_NAME = "repository-paths";
|
public static final String STORE_NAME = "repository-paths";
|
||||||
|
|
||||||
private final SCMContextProvider contextProvider;
|
private final SCMContextProvider contextProvider;
|
||||||
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
|
||||||
|
private final FileSystem fileSystem;
|
||||||
|
|
||||||
private final PathDatabase pathDatabase;
|
private final PathDatabase pathDatabase;
|
||||||
private final Map<String, Path> pathById;
|
private final Map<String, Path> pathById;
|
||||||
@@ -44,14 +46,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
|||||||
private Long lastModified;
|
private Long lastModified;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
|
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) {
|
||||||
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
|
this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
|
PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, Clock clock) {
|
||||||
super(Path.class);
|
super(Path.class);
|
||||||
this.contextProvider = contextProvider;
|
this.contextProvider = contextProvider;
|
||||||
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
|
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
|
||||||
|
this.fileSystem = fileSystem;
|
||||||
this.pathById = new ConcurrentHashMap<>();
|
this.pathById = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
@@ -64,23 +67,43 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
||||||
return repositoryId -> {
|
return new RepositoryLocationResolverInstance<T>() {
|
||||||
|
@Override
|
||||||
|
public T getLocation(String repositoryId) {
|
||||||
if (pathById.containsKey(repositoryId)) {
|
if (pathById.containsKey(repositoryId)) {
|
||||||
return (T) contextProvider.resolve(pathById.get(repositoryId));
|
return (T) contextProvider.resolve(pathById.get(repositoryId));
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("location for repository " + repositoryId + " does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T createLocation(String repositoryId) {
|
||||||
|
if (pathById.containsKey(repositoryId)) {
|
||||||
|
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||||
} else {
|
} else {
|
||||||
return (T) create(repositoryId);
|
return (T) create(repositoryId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLocation(String repositoryId, T location) {
|
||||||
|
if (pathById.containsKey(repositoryId)) {
|
||||||
|
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||||
|
} else {
|
||||||
|
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Path create(String repositoryId) {
|
Path create(String repositoryId) {
|
||||||
Path path = initialRepositoryLocationResolver.getPath(repositoryId);
|
Path path = initialRepositoryLocationResolver.getPath(repositoryId);
|
||||||
pathById.put(repositoryId, path);
|
setLocation(repositoryId, path);
|
||||||
writePathDatabase();
|
|
||||||
Path resolvedPath = contextProvider.resolve(path);
|
Path resolvedPath = contextProvider.resolve(path);
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(resolvedPath);
|
fileSystem.create(resolvedPath.toFile());
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e);
|
throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e);
|
||||||
}
|
}
|
||||||
return resolvedPath;
|
return resolvedPath;
|
||||||
@@ -138,4 +161,13 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setLocation(String repositoryId, Path repositoryBasePath) {
|
||||||
|
pathById.put(repositoryId, repositoryBasePath);
|
||||||
|
writePathDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
this.read();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package sonia.scm.repository.xml;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public class SingleRepositoryUpdateProcessor {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private PathBasedRepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
|
public void doUpdate(BiConsumer<String, Path> forEachRepository) {
|
||||||
|
locationResolver.forAllPaths(forEachRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
import org.mockito.quality.Strictness;
|
import org.mockito.quality.Strictness;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.io.DefaultFileSystem;
|
||||||
|
import sonia.scm.io.FileSystem;
|
||||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,6 +43,8 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
|
|
||||||
|
private final FileSystem fileSystem = new DefaultFileSystem();
|
||||||
|
|
||||||
private Path basePath;
|
private Path basePath;
|
||||||
|
|
||||||
private PathBasedRepositoryLocationResolver resolver;
|
private PathBasedRepositoryLocationResolver resolver;
|
||||||
@@ -57,7 +61,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateInitialDirectory() {
|
void shouldCreateInitialDirectory() {
|
||||||
Path path = resolver.forClass(Path.class).getLocation("newId");
|
Path path = resolver.forClass(Path.class).createLocation("newId");
|
||||||
|
|
||||||
assertThat(path).isEqualTo(basePath.resolve("newId"));
|
assertThat(path).isEqualTo(basePath.resolve("newId"));
|
||||||
assertThat(path).isDirectory();
|
assertThat(path).isDirectory();
|
||||||
@@ -65,7 +69,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPersistInitialDirectory() {
|
void shouldPersistInitialDirectory() {
|
||||||
resolver.forClass(Path.class).getLocation("newId");
|
resolver.forClass(Path.class).createLocation("newId");
|
||||||
|
|
||||||
String content = getXmlFileContent();
|
String content = getXmlFileContent();
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
long now = CREATION_TIME + 100;
|
long now = CREATION_TIME + 100;
|
||||||
when(clock.millis()).thenReturn(now);
|
when(clock.millis()).thenReturn(now);
|
||||||
|
|
||||||
resolver.forClass(Path.class).getLocation("newId");
|
resolver.forClass(Path.class).createLocation("newId");
|
||||||
|
|
||||||
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
|
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
|
||||||
|
|
||||||
@@ -91,7 +95,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
long now = CREATION_TIME + 100;
|
long now = CREATION_TIME + 100;
|
||||||
when(clock.millis()).thenReturn(now);
|
when(clock.millis()).thenReturn(now);
|
||||||
|
|
||||||
resolver.forClass(Path.class).getLocation("newId");
|
resolver.forClass(Path.class).createLocation("newId");
|
||||||
|
|
||||||
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
|
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
|
||||||
assertThat(resolver.getLastModified()).isEqualTo(now);
|
assertThat(resolver.getLastModified()).isEqualTo(now);
|
||||||
@@ -108,8 +112,8 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void createExistingDatabase() {
|
void createExistingDatabase() {
|
||||||
resolver.forClass(Path.class).getLocation("existingId_1");
|
resolver.forClass(Path.class).createLocation("existingId_1");
|
||||||
resolver.forClass(Path.class).getLocation("existingId_2");
|
resolver.forClass(Path.class).createLocation("existingId_2");
|
||||||
resolverWithExistingData = createResolver();
|
resolverWithExistingData = createResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +163,7 @@ class PathBasedRepositoryLocationResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PathBasedRepositoryLocationResolver createResolver() {
|
private PathBasedRepositoryLocationResolver createResolver() {
|
||||||
return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock);
|
return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String content(Path storePath) {
|
private String content(Path 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;
|
||||||
@@ -19,6 +17,7 @@ import sonia.scm.io.DefaultFileSystem;
|
|||||||
import sonia.scm.io.FileSystem;
|
import sonia.scm.io.FileSystem;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.repository.RepositoryPermission;
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -32,7 +31,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,16 +48,29 @@ 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;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void createDAO(@TempDirectory.TempDir Path basePath) {
|
void createDAO(@TempDirectory.TempDir Path basePath) {
|
||||||
when(locationResolver.create(Path.class)).thenReturn(locationResolver::create);
|
when(locationResolver.create(Path.class)).thenReturn(
|
||||||
|
new RepositoryLocationResolver.RepositoryLocationResolverInstance<Path>() {
|
||||||
|
@Override
|
||||||
|
public Path getLocation(String repositoryId) {
|
||||||
|
return locationResolver.create(repositoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path createLocation(String repositoryId) {
|
||||||
|
return locationResolver.create(repositoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLocation(String repositoryId, Path location) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
|
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
|
||||||
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
|
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
|
||||||
}
|
}
|
||||||
@@ -268,22 +282,6 @@ class XmlRepositoryDAOTest {
|
|||||||
|
|
||||||
verify(locationResolver).updateModificationDate();
|
verify(locationResolver).updateModificationDate();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
|
|
||||||
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
|
|
||||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
|
||||||
|
|
||||||
Path repositoryPath = basePath.resolve("existing");
|
|
||||||
Files.createDirectories(repositoryPath);
|
|
||||||
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
|
|
||||||
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
|
|
||||||
|
|
||||||
forAllCaptor.getValue().accept("existing", repositoryPath);
|
|
||||||
|
|
||||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getXmlFileContent(String id) {
|
private String getXmlFileContent(String id) {
|
||||||
Path storePath = metadataFile(id);
|
Path storePath = metadataFile(id);
|
||||||
@@ -303,8 +301,61 @@ class XmlRepositoryDAOTest {
|
|||||||
throw new RuntimeException(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package sonia.scm;
|
package sonia.scm;
|
||||||
|
|
||||||
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -16,6 +15,21 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
||||||
return repositoryId -> (T) tempDirectory.toPath();
|
return new RepositoryLocationResolverInstance<T>() {
|
||||||
|
@Override
|
||||||
|
public T getLocation(String repositoryId) {
|
||||||
|
return (T) tempDirectory.toPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T createLocation(String repositoryId) {
|
||||||
|
return (T) tempDirectory.toPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLocation(String repositoryId, T location) {
|
||||||
|
throw new UnsupportedOperationException("not implemented for tests");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ package sonia.scm.repository;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
import sonia.scm.AbstractTestBase;
|
import sonia.scm.AbstractTestBase;
|
||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
@@ -82,11 +83,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
|
|||||||
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||||
when(locationResolver.create(any())).thenReturn(instanceMock);
|
when(locationResolver.create(any())).thenReturn(instanceMock);
|
||||||
when(locationResolver.supportsLocationType(any())).thenReturn(true);
|
when(locationResolver.supportsLocationType(any())).thenReturn(true);
|
||||||
|
Answer<Object> pathAnswer = ic -> {
|
||||||
when(instanceMock.getLocation(anyString())).then(ic -> {
|
|
||||||
String id = ic.getArgument(0);
|
String id = ic.getArgument(0);
|
||||||
return baseDirectory.toPath().resolve(id);
|
return baseDirectory.toPath().resolve(id);
|
||||||
});
|
};
|
||||||
|
when(instanceMock.getLocation(anyString())).then(pathAnswer);
|
||||||
|
when(instanceMock.createLocation(anyString())).then(pathAnswer);
|
||||||
|
|
||||||
handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory);
|
handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory);
|
||||||
}
|
}
|
||||||
|
|||||||
25
scm-ui-components/packages/ui-components/src/Icon.js
Normal file
25
scm-ui-components/packages/ui-components/src/Icon.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Icon extends React.Component<Props> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { title, name } = this.props;
|
||||||
|
if(title) {
|
||||||
|
return (
|
||||||
|
<i title={title} className={classNames("is-icon", "fas", "fa-fw", "fa-" + name)}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<i className={classNames("is-icon", "fas", "fa-" + name)}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ class Radio extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<label className="radio" disabled={this.props.disabled}>
|
<label className="radio" disabled={this.props.disabled}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export { validation, urls, repositories };
|
|||||||
export { default as DateFromNow } from "./DateFromNow.js";
|
export { default as DateFromNow } from "./DateFromNow.js";
|
||||||
export { default as ErrorNotification } from "./ErrorNotification.js";
|
export { default as ErrorNotification } from "./ErrorNotification.js";
|
||||||
export { default as ErrorPage } from "./ErrorPage.js";
|
export { default as ErrorPage } from "./ErrorPage.js";
|
||||||
|
export { default as Icon } from "./Icon.js";
|
||||||
export { default as Image } from "./Image.js";
|
export { default as Image } from "./Image.js";
|
||||||
export { default as Loading } from "./Loading.js";
|
export { default as Loading } from "./Loading.js";
|
||||||
export { default as Logo } from "./Logo.js";
|
export { default as Logo } from "./Logo.js";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
import {Link, Route} from "react-router-dom";
|
import {Link, Route} from "react-router-dom";
|
||||||
|
|
||||||
// TODO mostly copy of PrimaryNavigationLink
|
// TODO mostly copy of PrimaryNavigationLink
|
||||||
@@ -28,7 +29,7 @@ class NavLink extends React.Component<Props> {
|
|||||||
|
|
||||||
let showIcon = null;
|
let showIcon = null;
|
||||||
if (icon) {
|
if (icon) {
|
||||||
showIcon = (<><i className={icon} />{" "}</>);
|
showIcon = (<><i className={classNames(icon, "fa-fw")} />{" "}</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link, Route } from "react-router-dom";
|
import { Link, Route } from "react-router-dom";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
to: string,
|
to: string,
|
||||||
@@ -37,7 +38,7 @@ class SubNavigation extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
|
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
|
||||||
<i className={defaultIcon} /> {label}
|
<i className={classNames(defaultIcon, "fa-fw")} /> {label}
|
||||||
</Link>
|
</Link>
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"lastModified": "Zuletzt bearbeitet",
|
"lastModified": "Zuletzt bearbeitet",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"external": "Extern",
|
"external": "Extern",
|
||||||
|
"internal": "Intern",
|
||||||
"members": "Mitglieder"
|
"members": "Mitglieder"
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"mail": "E-Mail",
|
"mail": "E-Mail",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
|
"inactive": "Inaktiv",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"creationDate": "Erstellt",
|
"creationDate": "Erstellt",
|
||||||
"lastModified": "Zuletzt bearbeitet"
|
"lastModified": "Zuletzt bearbeitet"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"lastModified": "Last Modified",
|
"lastModified": "Last Modified",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"external": "External",
|
"external": "External",
|
||||||
|
"internal": "Internal",
|
||||||
"members": "Members"
|
"members": "Members"
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"mail": "E-Mail",
|
"mail": "E-Mail",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"creationDate": "Creation Date",
|
"creationDate": "Creation Date",
|
||||||
"lastModified": "Last Modified"
|
"lastModified": "Last Modified"
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { Group } from "@scm-manager/ui-types";
|
import type { Group } from "@scm-manager/ui-types";
|
||||||
import { Checkbox } from "@scm-manager/ui-components";
|
import { Icon } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: Group
|
group: Group,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class GroupRow extends React.Component<Props> {
|
class GroupRow extends React.Component<Props> {
|
||||||
renderLink(to: string, label: string) {
|
renderLink(to: string, label: string) {
|
||||||
return <Link to={to}>{label}</Link>;
|
return <Link to={to}>{label}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { group } = this.props;
|
const { group, t } = this.props;
|
||||||
const to = `/group/${group.name}`;
|
const to = `/group/${group.name}`;
|
||||||
|
const iconType = group.external ? (
|
||||||
|
<Icon title={t("group.external")} name="sign-out-alt fa-rotate-270" />
|
||||||
|
) : (
|
||||||
|
<Icon title={t("group.internal")} name="sign-in-alt fa-rotate-90" />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{this.renderLink(to, group.name)}</td>
|
<td>{iconType} {this.renderLink(to, group.name)}</td>
|
||||||
<td className="is-hidden-mobile">{group.description}</td>
|
<td className="is-hidden-mobile">{group.description}</td>
|
||||||
<td>
|
|
||||||
<Checkbox checked={group.external} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default translate("groups")(GroupRow);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class GroupTable extends React.Component<Props> {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{t("group.name")}</th>
|
<th>{t("group.name")}</th>
|
||||||
<th className="is-hidden-mobile">{t("group.description")}</th>
|
<th className="is-hidden-mobile">{t("group.description")}</th>
|
||||||
<th>{t("group.external")}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "../modules/permissions";
|
} from "../modules/permissions";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import { Button } from "@scm-manager/ui-components";
|
import { Button, Icon } from "@scm-manager/ui-components";
|
||||||
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
|
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
|
||||||
import RoleSelector from "../components/RoleSelector";
|
import RoleSelector from "../components/RoleSelector";
|
||||||
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
||||||
@@ -49,9 +49,6 @@ type State = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
iconColor: {
|
|
||||||
color: "#9a9a9a"
|
|
||||||
},
|
|
||||||
centerMiddle: {
|
centerMiddle: {
|
||||||
display: "table-cell",
|
display: "table-cell",
|
||||||
verticalAlign: "middle !important"
|
verticalAlign: "middle !important"
|
||||||
@@ -148,15 +145,9 @@ class SinglePermission extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const iconType =
|
const iconType =
|
||||||
permission && permission.groupPermission ? (
|
permission && permission.groupPermission ? (
|
||||||
<i
|
<Icon title={t("permission.group")} name="user-friends" />
|
||||||
title={t("permission.group")}
|
|
||||||
className={classNames("fas fa-user-friends", classes.iconColor)}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<i
|
<Icon title={t("permission.user")} name="user" />
|
||||||
title={t("permission.user")}
|
|
||||||
className={classNames("fas fa-user", classes.iconColor)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,7 +162,7 @@ class SinglePermission extends React.Component<Props, State> {
|
|||||||
action={this.handleDetailedPermissionsPressed}
|
action={this.handleDetailedPermissionsPressed}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className={classes.centerMiddle}>
|
<td className={classNames("is-darker", classes.centerMiddle)}>
|
||||||
<DeletePermissionButton
|
<DeletePermissionButton
|
||||||
permission={permission}
|
permission={permission}
|
||||||
namespace={namespace}
|
namespace={namespace}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
this.props.user.displayName === user.displayName &&
|
this.props.user.displayName === user.displayName &&
|
||||||
this.props.user.mail === user.mail &&
|
this.props.user.mail === user.mail &&
|
||||||
this.props.user.admin === user.admin &&
|
|
||||||
this.props.user.active === user.active
|
this.props.user.active === user.active
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,31 +1,43 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { User } from "@scm-manager/ui-types";
|
import type { User } from "@scm-manager/ui-types";
|
||||||
|
import { Icon } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User
|
user: User,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class UserRow extends React.Component<Props> {
|
class UserRow extends React.Component<Props> {
|
||||||
renderLink(to: string, label: string) {
|
renderLink(to: string, label: string) {
|
||||||
return <Link to={to}>{label}</Link>;
|
return <Link to={to}>{label}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user } = this.props;
|
const { user, t } = this.props;
|
||||||
const to = `/user/${user.name}`;
|
const to = `/user/${user.name}`;
|
||||||
|
const iconType = user.active ? (
|
||||||
|
<Icon title={t("user.active")} name="user" />
|
||||||
|
) : (
|
||||||
|
<Icon title={t("user.inactive")} name="user-slash" />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
|
||||||
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
|
<td>{iconType} {this.renderLink(to, user.name)}</td>
|
||||||
<td>{this.renderLink(to, user.displayName)}</td>
|
<td className="is-hidden-mobile">
|
||||||
|
{this.renderLink(to, user.displayName)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href={`mailto:${user.mail}`}>{user.mail}</a>
|
<a href={`mailto:${user.mail}`}>{user.mail}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="is-hidden-mobile">
|
|
||||||
<input type="checkbox" id="active" checked={user.active} readOnly />
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default translate("users")(UserRow);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class UserTable extends React.Component<Props> {
|
|||||||
<th className="is-hidden-mobile">{t("user.name")}</th>
|
<th className="is-hidden-mobile">{t("user.name")}</th>
|
||||||
<th>{t("user.displayName")}</th>
|
<th>{t("user.displayName")}</th>
|
||||||
<th>{t("user.mail")}</th>
|
<th>{t("user.mail")}</th>
|
||||||
<th className="is-hidden-mobile">{t("user.active")}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -219,16 +219,23 @@ ul.is-separated {
|
|||||||
// card tables
|
// card tables
|
||||||
.card-table {
|
.card-table {
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0px 5px;
|
border-spacing: 0 5px;
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
a {
|
a {
|
||||||
color: #363636;
|
color: #363636;
|
||||||
}
|
}
|
||||||
|
&.border-is-green td:first-child {
|
||||||
|
border-left-color: $green;
|
||||||
|
}
|
||||||
|
&.border-is-yellow td:first-child {
|
||||||
|
border-left-color: $yellow;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
td {
|
td {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
&:nth-child(4) {
|
|
||||||
|
&.is-darker {
|
||||||
background-color: #e1e1e1;
|
background-color: #e1e1e1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,13 +245,14 @@ ul.is-separated {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
border-bottom: 1px solid whitesmoke;
|
|
||||||
background-color: #fafafa;
|
|
||||||
padding: 1em 1.25em;
|
padding: 1em 1.25em;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-bottom: 1px solid whitesmoke;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-left: 3px solid $mint;
|
border-left: 3px solid $grey;
|
||||||
}
|
}
|
||||||
&:nth-child(4) {
|
&.is-darker {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +326,10 @@ form .field:not(.is-grouped) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-icon {
|
||||||
|
color: $grey-light;
|
||||||
|
}
|
||||||
|
|
||||||
// label with help-icon compensation
|
// label with help-icon compensation
|
||||||
.label-icon-spacing {
|
.label-icon-spacing {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ import sonia.scm.util.IOUtil;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.ServletContextEvent;
|
import javax.servlet.ServletContextEvent;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -109,13 +111,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<? extends Module> getModules(ServletContext context) {
|
protected List<? extends Module> getModules(ServletContext context) {
|
||||||
if (hasStartupErrors()) {
|
|
||||||
return getErrorModules();
|
|
||||||
}
|
|
||||||
return getDefaultModules(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<? extends Module> getDefaultModules(ServletContext context) {
|
|
||||||
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins);
|
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, parent, plugins);
|
||||||
|
|
||||||
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
||||||
@@ -150,10 +145,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<? extends Module> getErrorModules() {
|
|
||||||
return Collections.singletonList(new ScmErrorModule());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void withInjector(Injector injector) {
|
protected void withInjector(Injector injector) {
|
||||||
this.injector = injector;
|
this.injector = injector;
|
||||||
@@ -183,6 +174,18 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
}
|
}
|
||||||
|
|
||||||
super.contextDestroyed(servletContextEvent);
|
super.contextDestroyed(servletContextEvent);
|
||||||
|
|
||||||
|
for (PluginWrapper plugin : getPlugins()) {
|
||||||
|
ClassLoader pcl = plugin.getClassLoader();
|
||||||
|
|
||||||
|
if (pcl instanceof Closeable) {
|
||||||
|
try {
|
||||||
|
((Closeable) pcl).close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.warn("could not close plugin classloader", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeCloseables() {
|
private void closeCloseables() {
|
||||||
@@ -205,6 +208,4 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
|
|||||||
private void destroyServletContextListeners(ServletContextEvent event) {
|
private void destroyServletContextListeners(ServletContextEvent event) {
|
||||||
injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event);
|
injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +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;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.inject.multibindings.Multibinder;
|
|
||||||
import com.google.inject.servlet.ServletModule;
|
|
||||||
|
|
||||||
import sonia.scm.template.ErrorServlet;
|
|
||||||
import sonia.scm.template.MustacheTemplateEngine;
|
|
||||||
import sonia.scm.template.TemplateEngine;
|
|
||||||
import sonia.scm.template.TemplateEngineFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class ScmErrorModule extends ServletModule
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void configureServlets()
|
|
||||||
{
|
|
||||||
SCMContextProvider context = SCMContext.getContext();
|
|
||||||
|
|
||||||
bind(SCMContextProvider.class).toInstance(context);
|
|
||||||
|
|
||||||
Multibinder<TemplateEngine> engineBinder =
|
|
||||||
Multibinder.newSetBinder(binder(), TemplateEngine.class);
|
|
||||||
|
|
||||||
engineBinder.addBinding().to(MustacheTemplateEngine.class);
|
|
||||||
bind(TemplateEngine.class).annotatedWith(Default.class).to(
|
|
||||||
MustacheTemplateEngine.class);
|
|
||||||
bind(TemplateEngineFactory.class);
|
|
||||||
|
|
||||||
serve(ScmServletModule.PATTERN_ALL).with(ErrorServlet.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,6 @@ import com.github.legman.Subscribe;
|
|||||||
import com.google.inject.servlet.GuiceFilter;
|
import com.google.inject.servlet.GuiceFilter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContext;
|
|
||||||
import sonia.scm.Stage;
|
|
||||||
import sonia.scm.event.RecreateEventBusEvent;
|
import sonia.scm.event.RecreateEventBusEvent;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
|
||||||
@@ -104,12 +102,9 @@ public class BootstrapContextFilter extends GuiceFilter
|
|||||||
|
|
||||||
initGuice();
|
initGuice();
|
||||||
|
|
||||||
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)
|
|
||||||
{
|
|
||||||
logger.info("register for restart events");
|
logger.info("register for restart events");
|
||||||
ScmEventBus.getInstance().register(this);
|
ScmEventBus.getInstance().register(this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void initGuice() throws ServletException {
|
public void initGuice() throws ServletException {
|
||||||
super.init(filterConfig);
|
super.init(filterConfig);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import sonia.scm.SCMContext;
|
|||||||
import sonia.scm.ScmContextListener;
|
import sonia.scm.ScmContextListener;
|
||||||
import sonia.scm.ScmEventBusModule;
|
import sonia.scm.ScmEventBusModule;
|
||||||
import sonia.scm.ScmInitializerModule;
|
import sonia.scm.ScmInitializerModule;
|
||||||
|
import sonia.scm.migration.UpdateException;
|
||||||
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;
|
||||||
@@ -52,6 +53,7 @@ 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;
|
||||||
|
import sonia.scm.update.MigrationWizardContextListener;
|
||||||
import sonia.scm.update.UpdateEngine;
|
import sonia.scm.update.UpdateEngine;
|
||||||
import sonia.scm.util.ClassLoaders;
|
import sonia.scm.util.ClassLoaders;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
@@ -59,13 +61,13 @@ import sonia.scm.util.IOUtil;
|
|||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.ServletContextEvent;
|
import javax.servlet.ServletContextEvent;
|
||||||
import javax.servlet.ServletContextListener;
|
import javax.servlet.ServletContextListener;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.xml.bind.DataBindingException;
|
import javax.xml.bind.DataBindingException;
|
||||||
import javax.xml.bind.JAXB;
|
import javax.xml.bind.JAXB;
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@@ -108,18 +110,6 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
public void contextDestroyed(ServletContextEvent sce) {
|
public void contextDestroyed(ServletContextEvent sce) {
|
||||||
contextListener.contextDestroyed(sce);
|
contextListener.contextDestroyed(sce);
|
||||||
|
|
||||||
for (PluginWrapper plugin : contextListener.getPlugins()) {
|
|
||||||
ClassLoader pcl = plugin.getClassLoader();
|
|
||||||
|
|
||||||
if (pcl instanceof Closeable) {
|
|
||||||
try {
|
|
||||||
((Closeable) pcl).close();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
logger.warn("could not close plugin classloader", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context = null;
|
context = null;
|
||||||
contextListener = null;
|
contextListener = null;
|
||||||
}
|
}
|
||||||
@@ -134,35 +124,79 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
public void contextInitialized(ServletContextEvent sce) {
|
public void contextInitialized(ServletContextEvent sce) {
|
||||||
context = sce.getServletContext();
|
context = sce.getServletContext();
|
||||||
|
|
||||||
File pluginDirectory = getPluginDirectory();
|
createContextListener();
|
||||||
|
|
||||||
createContextListener(pluginDirectory);
|
|
||||||
|
|
||||||
contextListener.contextInitialized(sce);
|
contextListener.contextInitialized(sce);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createContextListener(File pluginDirectory) {
|
private void createContextListener() {
|
||||||
|
Throwable startupError = SCMContext.getContext().getStartupError();
|
||||||
|
if (startupError != null) {
|
||||||
|
contextListener = SingleView.error(startupError);
|
||||||
|
} else if (Versions.isTooOld()) {
|
||||||
|
contextListener = SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT);
|
||||||
|
} else {
|
||||||
|
createMigrationOrNormalContextListener();
|
||||||
|
Versions.writeNew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMigrationOrNormalContextListener() {
|
||||||
|
ClassLoader cl;
|
||||||
|
Set<PluginWrapper> plugins;
|
||||||
|
PluginLoader pluginLoader;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
File pluginDirectory = getPluginDirectory();
|
||||||
|
|
||||||
|
renameOldPluginsFolder(pluginDirectory);
|
||||||
|
|
||||||
if (!isCorePluginExtractionDisabled()) {
|
if (!isCorePluginExtractionDisabled()) {
|
||||||
extractCorePlugins(context, pluginDirectory);
|
extractCorePlugins(context, pluginDirectory);
|
||||||
} else {
|
} else {
|
||||||
logger.info("core plugin extraction is disabled");
|
logger.info("core plugin extraction is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
|
cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class);
|
||||||
|
|
||||||
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
|
plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
|
||||||
|
|
||||||
PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
pluginLoader = new DefaultPluginLoader(context, cl, plugins);
|
||||||
|
|
||||||
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
|
|
||||||
|
|
||||||
processUpdates(pluginLoader, bootstrapInjector);
|
|
||||||
|
|
||||||
contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins);
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new PluginLoadException("could not load plugins", ex);
|
throw new PluginLoadException("could not load plugins", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
|
||||||
|
|
||||||
|
startEitherMigrationOrNormalServlet(cl, plugins, pluginLoader, bootstrapInjector);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startEitherMigrationOrNormalServlet(ClassLoader cl, Set<PluginWrapper> plugins, PluginLoader pluginLoader, Injector bootstrapInjector) {
|
||||||
|
MigrationWizardContextListener wizardContextListener = prepareWizardIfNeeded(bootstrapInjector);
|
||||||
|
|
||||||
|
if (wizardContextListener.wizardNecessary()) {
|
||||||
|
contextListener = wizardContextListener;
|
||||||
|
} else {
|
||||||
|
processUpdates(pluginLoader, bootstrapInjector);
|
||||||
|
contextListener = bootstrapInjector.getInstance(ScmContextListener.Factory.class).create(cl, plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renameOldPluginsFolder(File pluginDirectory) {
|
||||||
|
if (new File(pluginDirectory, "classpath.xml").exists()) {
|
||||||
|
File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1");
|
||||||
|
boolean renamed = pluginDirectory.renameTo(backupDirectory);
|
||||||
|
if (renamed) {
|
||||||
|
logger.warn("moved old plugins directory to {}", backupDirectory);
|
||||||
|
} else {
|
||||||
|
throw new UpdateException("could not rename existing v1 plugin directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MigrationWizardContextListener prepareWizardIfNeeded(Injector bootstrapInjector) {
|
||||||
|
return new MigrationWizardContextListener(bootstrapInjector);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
|
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
|
||||||
@@ -393,7 +427,7 @@ public class BootstrapContextListener implements ServletContextListener {
|
|||||||
private ServletContext context;
|
private ServletContext context;
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private ScmContextListener contextListener;
|
private ServletContextListener contextListener;
|
||||||
|
|
||||||
private static class ScmContextListenerModule extends AbstractModule {
|
private static class ScmContextListenerModule extends AbstractModule {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
119
scm-webapp/src/main/java/sonia/scm/boot/SingleView.java
Normal file
119
scm-webapp/src/main/java/sonia/scm/boot/SingleView.java
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import com.google.inject.servlet.GuiceServletContextListener;
|
||||||
|
import com.google.inject.servlet.ServletModule;
|
||||||
|
import sonia.scm.Default;
|
||||||
|
import sonia.scm.SCMContext;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.template.MustacheTemplateEngine;
|
||||||
|
import sonia.scm.template.TemplateEngine;
|
||||||
|
import sonia.scm.template.TemplateEngineFactory;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.ServletContextListener;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
final class SingleView {
|
||||||
|
|
||||||
|
private SingleView() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static ServletContextListener error(Throwable throwable) {
|
||||||
|
String error = Throwables.getStackTraceAsString(throwable);
|
||||||
|
|
||||||
|
ViewController controller = new SimpleViewController("/templates/error.mustache", request -> {
|
||||||
|
Object model = ImmutableMap.of(
|
||||||
|
"contextPath", request.getContextPath(),
|
||||||
|
"error", error
|
||||||
|
);
|
||||||
|
return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model);
|
||||||
|
});
|
||||||
|
return new SingleViewContextListener(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ServletContextListener view(String template, int sc) {
|
||||||
|
ViewController controller = new SimpleViewController(template, request -> {
|
||||||
|
Object model = ImmutableMap.of(
|
||||||
|
"contextPath", request.getContextPath()
|
||||||
|
);
|
||||||
|
return new View(sc, model);
|
||||||
|
});
|
||||||
|
return new SingleViewContextListener(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SingleViewContextListener extends GuiceServletContextListener {
|
||||||
|
|
||||||
|
private final ViewController controller;
|
||||||
|
|
||||||
|
private SingleViewContextListener(ViewController controller) {
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Injector getInjector() {
|
||||||
|
return Guice.createInjector(new SingleViewModule(controller));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SingleViewModule extends ServletModule {
|
||||||
|
|
||||||
|
private final ViewController viewController;
|
||||||
|
|
||||||
|
private SingleViewModule(ViewController viewController) {
|
||||||
|
this.viewController = viewController;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configureServlets() {
|
||||||
|
SCMContextProvider context = SCMContext.getContext();
|
||||||
|
|
||||||
|
bind(SCMContextProvider.class).toInstance(context);
|
||||||
|
bind(ViewController.class).toInstance(viewController);
|
||||||
|
|
||||||
|
Multibinder<TemplateEngine> engineBinder =
|
||||||
|
Multibinder.newSetBinder(binder(), TemplateEngine.class);
|
||||||
|
|
||||||
|
engineBinder.addBinding().to(MustacheTemplateEngine.class);
|
||||||
|
bind(TemplateEngine.class).annotatedWith(Default.class).to(
|
||||||
|
MustacheTemplateEngine.class);
|
||||||
|
bind(TemplateEngineFactory.class);
|
||||||
|
|
||||||
|
bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext());
|
||||||
|
|
||||||
|
serve("/images/*", "/styles/*", "/favicon.ico").with(StaticResourceServlet.class);
|
||||||
|
serve("/*").with(SingleViewServlet.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SimpleViewController implements ViewController {
|
||||||
|
|
||||||
|
private final String template;
|
||||||
|
private final SimpleViewFactory viewFactory;
|
||||||
|
|
||||||
|
private SimpleViewController(String template, SimpleViewFactory viewFactory) {
|
||||||
|
this.template = template;
|
||||||
|
this.viewFactory = viewFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTemplate() {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View createView(HttpServletRequest request) {
|
||||||
|
return viewFactory.create(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface SimpleViewFactory {
|
||||||
|
View create(HttpServletRequest request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.template.Template;
|
||||||
|
import sonia.scm.template.TemplateEngine;
|
||||||
|
import sonia.scm.template.TemplateEngineFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class SingleViewServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SingleViewServlet.class);
|
||||||
|
|
||||||
|
private final Template template;
|
||||||
|
private final ViewController controller;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SingleViewServlet(TemplateEngineFactory templateEngineFactory, ViewController controller) {
|
||||||
|
template = createTemplate(templateEngineFactory, controller.getTemplate());
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Template createTemplate(TemplateEngineFactory templateEngineFactory, String template) {
|
||||||
|
TemplateEngine engine = templateEngineFactory.getEngineByExtension(template);
|
||||||
|
try {
|
||||||
|
return engine.getTemplate(template);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("failed to parse template: " + template, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
|
process(req, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
|
process(req, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void process(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
View view = controller.createView(request);
|
||||||
|
|
||||||
|
response.setStatus(view.getStatusCode());
|
||||||
|
response.setContentType("text/html");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
try (PrintWriter writer = response.getWriter()) {
|
||||||
|
template.execute(writer, view.getModel());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("failed to write view", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import com.github.sdorra.webresources.CacheControl;
|
||||||
|
import com.github.sdorra.webresources.WebResourceSender;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class StaticResourceServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private final WebResourceSender sender = WebResourceSender.create()
|
||||||
|
.withGZIP()
|
||||||
|
.withGZIPMinLength(512)
|
||||||
|
.withBufferSize(16384)
|
||||||
|
.withCacheControl(CacheControl.create().noCache());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
URL resource = createResourceUrlFromRequest(request);
|
||||||
|
if (resource != null) {
|
||||||
|
sender.resource(resource).get(request, response);
|
||||||
|
} else {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException {
|
||||||
|
String uri = HttpUtil.getStrippedURI(request);
|
||||||
|
return request.getServletContext().getResource(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
scm-webapp/src/main/java/sonia/scm/boot/Versions.java
Normal file
77
scm-webapp/src/main/java/sonia/scm/boot/Versions.java
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.SCMContext;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.util.IOUtil;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
class Versions {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Versions.class);
|
||||||
|
|
||||||
|
private static final Version MIN_VERSION = Version.parse("1.60");
|
||||||
|
|
||||||
|
private final SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Versions(SCMContextProvider contextProvider) {
|
||||||
|
this.contextProvider = contextProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean isPreviousVersionTooOld() {
|
||||||
|
return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void writeNewVersion() {
|
||||||
|
Path config = contextProvider.resolve(Paths.get("config"));
|
||||||
|
IOUtil.mkdirs(config.toFile());
|
||||||
|
|
||||||
|
String version = contextProvider.getVersion();
|
||||||
|
LOG.debug("write new version {} to file", version);
|
||||||
|
Path versionFile = config.resolve("version.txt");
|
||||||
|
try {
|
||||||
|
Files.write(versionFile, version.getBytes());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("failed to write version file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Version> readVersion() {
|
||||||
|
Path versionFile = contextProvider.resolve(Paths.get("config", "version.txt"));
|
||||||
|
if (versionFile.toFile().exists()) {
|
||||||
|
return Optional.of(readVersionFromFile(versionFile));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Version readVersionFromFile(Path versionFile) {
|
||||||
|
try {
|
||||||
|
String versionString = new String(Files.readAllBytes(versionFile), StandardCharsets.UTF_8).trim();
|
||||||
|
LOG.debug("read previous version {} from file", versionString);
|
||||||
|
return Version.parse(versionString);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("failed to read version file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isTooOld() {
|
||||||
|
return new Versions(SCMContext.getContext()).isPreviousVersionTooOld();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeNew() {
|
||||||
|
new Versions(SCMContext.getContext()).writeNewVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
scm-webapp/src/main/java/sonia/scm/boot/View.java
Normal file
20
scm-webapp/src/main/java/sonia/scm/boot/View.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
class View {
|
||||||
|
|
||||||
|
private final int statusCode;
|
||||||
|
private final Object model;
|
||||||
|
|
||||||
|
View(int statusCode, Object model) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getModel() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
scm-webapp/src/main/java/sonia/scm/boot/ViewController.java
Normal file
11
scm-webapp/src/main/java/sonia/scm/boot/ViewController.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
public interface ViewController {
|
||||||
|
|
||||||
|
String getTemplate();
|
||||||
|
|
||||||
|
View createView(HttpServletRequest request);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ import java.util.Set;
|
|||||||
import static java.util.Collections.unmodifiableCollection;
|
import static java.util.Collections.unmodifiableCollection;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
class SystemRepositoryPermissionProvider {
|
public class SystemRepositoryPermissionProvider {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
|
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
|
||||||
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
||||||
|
|||||||
@@ -1,191 +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.template;
|
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import sonia.scm.SCMContextProvider;
|
|
||||||
import sonia.scm.util.IOUtil;
|
|
||||||
import sonia.scm.util.Util;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class ErrorServlet extends HttpServlet
|
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String TEMPALTE = "/error.mustache";
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final long serialVersionUID = -3289076078469757874L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for ErrorServlet
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(ErrorServlet.class);
|
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs ...
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param templateEngineFactory
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
public ErrorServlet(SCMContextProvider context,
|
|
||||||
TemplateEngineFactory templateEngineFactory)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
this.templateEngineFactory = templateEngineFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
* @throws ServletException
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response)
|
|
||||||
throws ServletException, IOException
|
|
||||||
{
|
|
||||||
processRequest(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
* @throws ServletException
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void doPost(HttpServletRequest request,
|
|
||||||
HttpServletResponse response)
|
|
||||||
throws ServletException, IOException
|
|
||||||
{
|
|
||||||
processRequest(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
* @throws ServletException
|
|
||||||
*/
|
|
||||||
private void processRequest(HttpServletRequest request,
|
|
||||||
HttpServletResponse response)
|
|
||||||
throws ServletException, IOException
|
|
||||||
{
|
|
||||||
PrintWriter writer = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
writer = response.getWriter();
|
|
||||||
|
|
||||||
Map<String, Object> env = new HashMap<String, Object>();
|
|
||||||
String error = Util.EMPTY_STRING;
|
|
||||||
|
|
||||||
if (context.getStartupError() != null)
|
|
||||||
{
|
|
||||||
error = Throwables.getStackTraceAsString(context.getStartupError());
|
|
||||||
}
|
|
||||||
|
|
||||||
env.put("error", error);
|
|
||||||
|
|
||||||
TemplateEngine engine = templateEngineFactory.getDefaultEngine();
|
|
||||||
Template template = engine.getTemplate(TEMPALTE);
|
|
||||||
|
|
||||||
if (template != null)
|
|
||||||
{
|
|
||||||
template.execute(writer, env);
|
|
||||||
}
|
|
||||||
else if (logger.isWarnEnabled())
|
|
||||||
{
|
|
||||||
logger.warn("could not find template {}", TEMPALTE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IOUtil.close(writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final SCMContextProvider context;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final TemplateEngineFactory templateEngineFactory;
|
|
||||||
}
|
|
||||||
@@ -37,27 +37,22 @@ package sonia.scm.template;
|
|||||||
|
|
||||||
import com.github.mustachejava.Mustache;
|
import com.github.mustachejava.Mustache;
|
||||||
import com.github.mustachejava.MustacheException;
|
import com.github.mustachejava.MustacheException;
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import sonia.scm.Default;
|
import sonia.scm.Default;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
import javax.servlet.ServletContext;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -67,6 +62,14 @@ import javax.servlet.ServletContext;
|
|||||||
public class MustacheTemplateEngine implements TemplateEngine
|
public class MustacheTemplateEngine implements TemplateEngine
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to implement optional injection for the PluginLoader.
|
||||||
|
* @see <a href="https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-can-i-inject-optional-parameters-into-a-constructor">Optional Injection</a>
|
||||||
|
*/
|
||||||
|
static class PluginLoaderHolder {
|
||||||
|
@Inject(optional = true) PluginLoader pluginLoader;
|
||||||
|
}
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
public static final TemplateType TYPE = new TemplateType("mustache",
|
public static final TemplateType TYPE = new TemplateType("mustache",
|
||||||
"Mustache", "mustache");
|
"Mustache", "mustache");
|
||||||
@@ -87,13 +90,12 @@ public class MustacheTemplateEngine implements TemplateEngine
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @param context
|
* @param context
|
||||||
* @param pluginLoader
|
* @param pluginLoaderHolder
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public MustacheTemplateEngine(@Default ServletContext context,
|
public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder)
|
||||||
PluginLoader pluginLoader)
|
|
||||||
{
|
{
|
||||||
factory = new ServletMustacheFactory(context, pluginLoader);
|
factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader));
|
||||||
|
|
||||||
ThreadFactory threadFactory =
|
ThreadFactory threadFactory =
|
||||||
new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build();
|
new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build();
|
||||||
@@ -101,6 +103,13 @@ public class MustacheTemplateEngine implements TemplateEngine
|
|||||||
factory.setExecutorService(Executors.newCachedThreadPool(threadFactory));
|
factory.setExecutorService(Executors.newCachedThreadPool(threadFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClassLoader createClassLoader(PluginLoader pluginLoader) {
|
||||||
|
if (pluginLoader == null) {
|
||||||
|
return Thread.currentThread().getContextClassLoader();
|
||||||
|
}
|
||||||
|
return pluginLoader.getUberClassLoader();
|
||||||
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,12 +121,9 @@ public class MustacheTemplateEngine implements TemplateEngine
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*
|
*
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Template getTemplate(String templateIdentifier, Reader reader)
|
public Template getTemplate(String templateIdentifier, Reader reader) {
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
if (logger.isTraceEnabled())
|
if (logger.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("try to create mustache template from reader with id {}",
|
logger.trace("try to create mustache template from reader with id {}",
|
||||||
|
|||||||
@@ -36,22 +36,17 @@ package sonia.scm.template;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.github.mustachejava.DefaultMustacheFactory;
|
import com.github.mustachejava.DefaultMustacheFactory;
|
||||||
|
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import javax.servlet.ServletContext;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -73,13 +68,12 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @param servletContext
|
* @param servletContext
|
||||||
* @param pluginLoader
|
* @param classLoader
|
||||||
*/
|
*/
|
||||||
public ServletMustacheFactory(ServletContext servletContext,
|
public ServletMustacheFactory(ServletContext servletContext, ClassLoader classLoader)
|
||||||
PluginLoader pluginLoader)
|
|
||||||
{
|
{
|
||||||
this.servletContext = servletContext;
|
this.servletContext = servletContext;
|
||||||
this.pluginLoader = pluginLoader;
|
this.classLoader = classLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
@@ -116,7 +110,7 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
|
|||||||
resourceName = resourceName.substring(1);
|
resourceName = resourceName.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
is = pluginLoader.getUberClassLoader().getResourceAsStream(resourceName);
|
is = classLoader.getResourceAsStream(resourceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is != null)
|
if (is != null)
|
||||||
@@ -144,9 +138,8 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
|
|||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final PluginLoader pluginLoader;
|
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private ServletContext servletContext;
|
private ServletContext servletContext;
|
||||||
|
|
||||||
|
private ClassLoader classLoader;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package sonia.scm.update;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import com.google.inject.servlet.GuiceServletContextListener;
|
||||||
|
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
|
||||||
|
|
||||||
|
public class MigrationWizardContextListener extends GuiceServletContextListener {
|
||||||
|
|
||||||
|
private final Injector bootstrapInjector;
|
||||||
|
|
||||||
|
public MigrationWizardContextListener(Injector bootstrapInjector) {
|
||||||
|
this.bootstrapInjector = bootstrapInjector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wizardNecessary() {
|
||||||
|
return !bootstrapInjector.getInstance(XmlRepositoryV1UpdateStep.class).getRepositoriesWithoutMigrationStrategies().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Injector getInjector() {
|
||||||
|
return bootstrapInjector.createChildInjector(new MigrationWizardModule());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package sonia.scm.update;
|
||||||
|
|
||||||
|
import com.google.inject.servlet.ServletModule;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.PushStateDispatcher;
|
||||||
|
import sonia.scm.WebResourceServlet;
|
||||||
|
|
||||||
|
class MigrationWizardModule extends ServletModule {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardModule.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configureServlets() {
|
||||||
|
LOG.info("==========================================================");
|
||||||
|
LOG.info("= =");
|
||||||
|
LOG.info("= STARTING MIGRATION SERVLET =");
|
||||||
|
LOG.info("= =");
|
||||||
|
LOG.info("= Open SCM-Manager in a browser to start the wizard. =");
|
||||||
|
LOG.info("= =");
|
||||||
|
LOG.info("==========================================================");
|
||||||
|
bind(PushStateDispatcher.class).toInstance((request, response, uri) -> {});
|
||||||
|
serve("/images/*", "/styles/*", "/favicon.ico").with(WebResourceServlet.class);
|
||||||
|
serve("/*").with(MigrationWizardServlet.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package sonia.scm.update;
|
||||||
|
|
||||||
|
import com.github.mustachejava.DefaultMustacheFactory;
|
||||||
|
import com.github.mustachejava.Mustache;
|
||||||
|
import com.github.mustachejava.MustacheFactory;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.boot.RestartEvent;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategy;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategyDao;
|
||||||
|
import sonia.scm.update.repository.V1Repository;
|
||||||
|
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
|
||||||
|
import sonia.scm.util.ValidationUtil;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static java.util.Comparator.comparing;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MigrationWizardServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class);
|
||||||
|
|
||||||
|
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
|
||||||
|
private final MigrationStrategyDao migrationStrategyDao;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) {
|
||||||
|
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
|
||||||
|
this.migrationStrategyDao = migrationStrategyDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
|
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
|
||||||
|
doGet(req, resp, repositoryLineEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doGet(HttpServletRequest req, HttpServletResponse resp, List<RepositoryLineEntry> repositoryLineEntries) {
|
||||||
|
HashMap<String, Object> model = new HashMap<>();
|
||||||
|
|
||||||
|
model.put("contextPath", req.getContextPath());
|
||||||
|
model.put("submitUrl", req.getRequestURI());
|
||||||
|
model.put("repositories", repositoryLineEntries);
|
||||||
|
model.put("strategies", getMigrationStrategies());
|
||||||
|
model.put("validationErrorsFound", repositoryLineEntries
|
||||||
|
.stream()
|
||||||
|
.anyMatch(entry -> entry.isNamespaceInvalid() || entry.isNameInvalid()));
|
||||||
|
|
||||||
|
respondWithTemplate(resp, model, "templates/repository-migration.mustache");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||||
|
List<RepositoryLineEntry> repositoryLineEntries = getRepositoryLineEntries();
|
||||||
|
|
||||||
|
boolean validationErrorFound = false;
|
||||||
|
for (RepositoryLineEntry repositoryLineEntry : repositoryLineEntries) {
|
||||||
|
String id = repositoryLineEntry.getId();
|
||||||
|
|
||||||
|
String strategy = req.getParameter("strategy-" + id);
|
||||||
|
if (!Strings.isNullOrEmpty(strategy)) {
|
||||||
|
repositoryLineEntry.setSelectedStrategy(MigrationStrategy.valueOf(strategy));
|
||||||
|
}
|
||||||
|
|
||||||
|
String namespace = req.getParameter("namespace-" + id);
|
||||||
|
repositoryLineEntry.setNamespace(namespace);
|
||||||
|
|
||||||
|
String name = req.getParameter("name-" + id);
|
||||||
|
repositoryLineEntry.setName(name);
|
||||||
|
|
||||||
|
if (!ValidationUtil.isRepositoryNameValid(namespace)) {
|
||||||
|
repositoryLineEntry.setNamespaceValid(false);
|
||||||
|
validationErrorFound = true;
|
||||||
|
}
|
||||||
|
if (!ValidationUtil.isRepositoryNameValid(name)) {
|
||||||
|
repositoryLineEntry.setNameValid(false);
|
||||||
|
validationErrorFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationErrorFound) {
|
||||||
|
doGet(req, resp, repositoryLineEntries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
repositoryLineEntries.stream()
|
||||||
|
.map(RepositoryLineEntry::getId)
|
||||||
|
.forEach(
|
||||||
|
id -> {
|
||||||
|
String strategy = req.getParameter("strategy-" + id);
|
||||||
|
String namespace = req.getParameter("namespace-" + id);
|
||||||
|
String name = req.getParameter("name-" + id);
|
||||||
|
migrationStrategyDao.set(id, MigrationStrategy.valueOf(strategy), namespace, name);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> model = Collections.singletonMap("contextPath", req.getContextPath());
|
||||||
|
|
||||||
|
respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache");
|
||||||
|
|
||||||
|
ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RepositoryLineEntry> getRepositoryLineEntries() {
|
||||||
|
List<V1Repository> repositoriesWithoutMigrationStrategies =
|
||||||
|
repositoryV1UpdateStep.getRepositoriesWithoutMigrationStrategies();
|
||||||
|
return repositoriesWithoutMigrationStrategies.stream()
|
||||||
|
.map(RepositoryLineEntry::new)
|
||||||
|
.sorted(comparing(RepositoryLineEntry::getPath))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MigrationStrategy[] getMigrationStrategies() {
|
||||||
|
return MigrationStrategy.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
|
||||||
|
MustacheFactory mf = new DefaultMustacheFactory();
|
||||||
|
Mustache template = mf.compile(templateName);
|
||||||
|
|
||||||
|
PrintWriter writer;
|
||||||
|
try {
|
||||||
|
writer = resp.getWriter();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("could not create writer for response", e);
|
||||||
|
resp.setStatus(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
template.execute(writer, model);
|
||||||
|
writer.flush();
|
||||||
|
resp.setStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RepositoryLineEntry {
|
||||||
|
private final String id;
|
||||||
|
private final String type;
|
||||||
|
private final String path;
|
||||||
|
private MigrationStrategy selectedStrategy;
|
||||||
|
private String namespace;
|
||||||
|
private String name;
|
||||||
|
private boolean namespaceValid = true;
|
||||||
|
private boolean nameValid = true;
|
||||||
|
|
||||||
|
public RepositoryLineEntry(V1Repository repository) {
|
||||||
|
this.id = repository.getId();
|
||||||
|
this.type = repository.getType();
|
||||||
|
this.path = repository.getType() + "/" + repository.getName();
|
||||||
|
this.selectedStrategy = MigrationStrategy.COPY;
|
||||||
|
this.namespace = computeNewNamespace(repository);
|
||||||
|
this.name = computeNewName(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String computeNewNamespace(V1Repository v1Repository) {
|
||||||
|
String[] nameParts = getNameParts(v1Repository.getName());
|
||||||
|
return nameParts.length > 1 ? nameParts[0] : v1Repository.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String computeNewName(V1Repository v1Repository) {
|
||||||
|
String[] nameParts = getNameParts(v1Repository.getName());
|
||||||
|
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] getNameParts(String v1Name) {
|
||||||
|
return v1Name.split("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String concatPathElements(String[] nameParts) {
|
||||||
|
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNamespace() {
|
||||||
|
return namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MigrationStrategy getSelectedStrategy() {
|
||||||
|
return selectedStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RepositoryLineMigrationStrategy> getStrategies() {
|
||||||
|
return Arrays.stream(MigrationStrategy.values())
|
||||||
|
.map(s -> new RepositoryLineMigrationStrategy(s.name(), selectedStrategy == s))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNamespace(String namespace) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNamespaceValid(boolean namespaceValid) {
|
||||||
|
this.namespaceValid = namespaceValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNameValid(boolean nameValid) {
|
||||||
|
this.nameValid = nameValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedStrategy(MigrationStrategy selectedStrategy) {
|
||||||
|
this.selectedStrategy = selectedStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNamespaceInvalid() {
|
||||||
|
return !namespaceValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNameInvalid() {
|
||||||
|
return !nameValid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RepositoryLineMigrationStrategy {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final boolean selected;
|
||||||
|
|
||||||
|
private RepositoryLineMigrationStrategy(String name, boolean selected) {
|
||||||
|
this.name = name;
|
||||||
|
this.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelected() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package sonia.scm.update.repository;
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
@@ -7,9 +9,14 @@ import sonia.scm.repository.RepositoryLocationResolver;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
|
||||||
class CopyMigrationStrategy extends BaseMigrationStrategy {
|
class CopyMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class);
|
||||||
|
|
||||||
private final RepositoryLocationResolver locationResolver;
|
private final RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -19,13 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Path migrate(String id, String name, String type) {
|
public Optional<Path> migrate(String id, String name, String type) {
|
||||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
|
||||||
Path targetDataPath = repositoryBasePath
|
Path targetDataPath = repositoryBasePath
|
||||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
Path sourceDataPath = getSourceDataPath(name, type);
|
Path sourceDataPath = getSourceDataPath(name, type);
|
||||||
|
LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath);
|
||||||
copyData(sourceDataPath, targetDataPath);
|
copyData(sourceDataPath, targetDataPath);
|
||||||
return repositoryBasePath;
|
return of(repositoryBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyData(Path sourceDirectory, Path targetDirectory) {
|
private void copyData(Path sourceDirectory, Path targetDirectory) {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.util.IOUtil;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class DeleteMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DeleteMigrationStrategy.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DeleteMigrationStrategy(SCMContextProvider contextProvider) {
|
||||||
|
super(contextProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Path> migrate(String id, String name, String type) {
|
||||||
|
Path sourceDataPath = getSourceDataPath(name, type);
|
||||||
|
try {
|
||||||
|
IOUtil.delete(sourceDataPath.toFile(), true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("could not delete old repository path for repository {} with type {} and id {}", name, type, id);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
|
||||||
|
public class IgnoreMigrationStrategy implements MigrationStrategy.Instance {
|
||||||
|
@Override
|
||||||
|
public Optional<Path> migrate(String id, String name, String type) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,47 @@
|
|||||||
package sonia.scm.update.repository;
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.repository.RepositoryDirectoryHandler;
|
import sonia.scm.repository.RepositoryDirectoryHandler;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
|
||||||
class InlineMigrationStrategy extends BaseMigrationStrategy {
|
class InlineMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(InlineMigrationStrategy.class);
|
||||||
|
|
||||||
|
private final RepositoryLocationResolver locationResolver;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
|
public InlineMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||||
super(contextProvider);
|
super(contextProvider);
|
||||||
|
this.locationResolver = locationResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Path migrate(String id, String name, String type) {
|
public Optional<Path> migrate(String id, String name, String type) {
|
||||||
Path repositoryBasePath = getSourceDataPath(name, type);
|
Path repositoryBasePath = getSourceDataPath(name, type);
|
||||||
|
locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath);
|
||||||
Path targetDataPath = repositoryBasePath
|
Path targetDataPath = repositoryBasePath
|
||||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
|
LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath);
|
||||||
moveData(repositoryBasePath, targetDataPath);
|
moveData(repositoryBasePath, targetDataPath);
|
||||||
return repositoryBasePath;
|
return of(repositoryBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
private void moveData(Path sourceDirectory, Path targetDirectory) {
|
||||||
|
moveData(sourceDirectory, targetDirectory, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveData(Path sourceDirectory, Path targetDirectory, boolean deleteDirectory) {
|
||||||
createDataDirectory(targetDirectory);
|
createDataDirectory(targetDirectory);
|
||||||
listSourceDirectory(sourceDirectory)
|
listSourceDirectory(sourceDirectory)
|
||||||
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
|
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
|
||||||
@@ -31,11 +49,18 @@ class InlineMigrationStrategy extends BaseMigrationStrategy {
|
|||||||
sourceFile -> {
|
sourceFile -> {
|
||||||
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
|
||||||
if (Files.isDirectory(sourceFile)) {
|
if (Files.isDirectory(sourceFile)) {
|
||||||
moveData(sourceFile, targetFile);
|
moveData(sourceFile, targetFile, true);
|
||||||
} else {
|
} else {
|
||||||
moveFile(sourceFile, targetFile);
|
moveFile(sourceFile, targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (deleteDirectory) {
|
||||||
|
try {
|
||||||
|
Files.delete(sourceDirectory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("could not delete source repository directory {}", sourceDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.migration.UpdateException;
|
||||||
|
import sonia.scm.migration.UpdateStep;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.repository.HealthCheckFailure;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
|
import sonia.scm.repository.RepositoryRole;
|
||||||
|
import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor;
|
||||||
|
import sonia.scm.security.SystemRepositoryPermissionProvider;
|
||||||
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.xml.bind.JAXBContext;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import javax.xml.bind.Marshaller;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlElementWrapper;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class MigrateVerbsToPermissionRoles implements UpdateStep {
|
||||||
|
|
||||||
|
public static final Logger LOG = LoggerFactory.getLogger(MigrateVerbsToPermissionRoles.class);
|
||||||
|
|
||||||
|
private final SingleRepositoryUpdateProcessor updateProcessor;
|
||||||
|
private final SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||||
|
private final JAXBContext jaxbContextNewRepository;
|
||||||
|
private final JAXBContext jaxbContextOldRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MigrateVerbsToPermissionRoles(SingleRepositoryUpdateProcessor updateProcessor, SystemRepositoryPermissionProvider systemRepositoryPermissionProvider) {
|
||||||
|
this.updateProcessor = updateProcessor;
|
||||||
|
this.systemRepositoryPermissionProvider = systemRepositoryPermissionProvider;
|
||||||
|
jaxbContextNewRepository = createJAXBContext(Repository.class);
|
||||||
|
jaxbContextOldRepository = createJAXBContext(OldRepository.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doUpdate() {
|
||||||
|
updateProcessor.doUpdate(this::update);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(String repositoryId, Path path) {
|
||||||
|
LOG.info("updating repository {}", repositoryId);
|
||||||
|
OldRepository oldRepository = readOldRepository(path);
|
||||||
|
Repository newRepository = createNewRepository(oldRepository);
|
||||||
|
writeNewRepository(path, newRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeNewRepository(Path path, Repository newRepository) {
|
||||||
|
try {
|
||||||
|
Marshaller marshaller = jaxbContextNewRepository.createMarshaller();
|
||||||
|
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||||
|
marshaller.marshal(newRepository, path.resolve("metadata.xml").toFile());
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new UpdateException("could not read old repository structure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OldRepository readOldRepository(Path path) {
|
||||||
|
try {
|
||||||
|
return (OldRepository) jaxbContextOldRepository.createUnmarshaller().unmarshal(path.resolve("metadata.xml").toFile());
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new UpdateException("could not read old repository structure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Repository createNewRepository(OldRepository oldRepository) {
|
||||||
|
Repository repository = new Repository(
|
||||||
|
oldRepository.id,
|
||||||
|
oldRepository.type,
|
||||||
|
oldRepository.namespace,
|
||||||
|
oldRepository.name,
|
||||||
|
oldRepository.contact,
|
||||||
|
oldRepository.description,
|
||||||
|
oldRepository.permissions.stream().map(this::updatePermission).toArray(RepositoryPermission[]::new)
|
||||||
|
);
|
||||||
|
repository.setCreationDate(oldRepository.creationDate);
|
||||||
|
repository.setHealthCheckFailures(oldRepository.healthCheckFailures);
|
||||||
|
repository.setLastModified(oldRepository.lastModified);
|
||||||
|
repository.setPublicReadable(oldRepository.publicReadable);
|
||||||
|
repository.setArchived(oldRepository.archived);
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryPermission updatePermission(RepositoryPermission repositoryPermission) {
|
||||||
|
return findMatchingRole(repositoryPermission.getVerbs())
|
||||||
|
.map(roleName -> copyRepositoryPermissionWithRole(repositoryPermission, roleName))
|
||||||
|
.orElse(repositoryPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryPermission copyRepositoryPermissionWithRole(RepositoryPermission repositoryPermission, String roleName) {
|
||||||
|
return new RepositoryPermission(repositoryPermission.getName(), roleName, repositoryPermission.isGroupPermission());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> findMatchingRole(Collection<String> verbs) {
|
||||||
|
return systemRepositoryPermissionProvider.availableRoles()
|
||||||
|
.stream()
|
||||||
|
.filter(r -> roleMatchesVerbs(verbs, r))
|
||||||
|
.map(RepositoryRole::getName)
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean roleMatchesVerbs(Collection<String> verbs, RepositoryRole r) {
|
||||||
|
return verbs.size() == r.getVerbs().size() && r.getVerbs().containsAll(verbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JAXBContext createJAXBContext(Class<?> clazz) {
|
||||||
|
try {
|
||||||
|
return JAXBContext.newInstance(clazz);
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new UpdateException("could not create XML marshaller", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Version getTargetVersion() {
|
||||||
|
return Version.parse("2.0.2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffectedDataType() {
|
||||||
|
return "sonia.scm.repository.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "repositories")
|
||||||
|
private static class OldRepository {
|
||||||
|
private String contact;
|
||||||
|
private Long creationDate;
|
||||||
|
private String description;
|
||||||
|
@XmlElement(name = "healthCheckFailure")
|
||||||
|
@XmlElementWrapper(name = "healthCheckFailures")
|
||||||
|
private List<HealthCheckFailure> healthCheckFailures;
|
||||||
|
private String id;
|
||||||
|
private Long lastModified;
|
||||||
|
private String namespace;
|
||||||
|
private String name;
|
||||||
|
@XmlElement(name = "permission")
|
||||||
|
private final Set<RepositoryPermission> permissions = new HashSet<>();
|
||||||
|
@XmlElement(name = "public")
|
||||||
|
private boolean publicReadable = false;
|
||||||
|
private boolean archived = false;
|
||||||
|
private String type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,41 @@ package sonia.scm.update.repository;
|
|||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
enum MigrationStrategy {
|
public enum MigrationStrategy {
|
||||||
|
|
||||||
COPY(CopyMigrationStrategy.class),
|
COPY(CopyMigrationStrategy.class,
|
||||||
MOVE(MoveMigrationStrategy.class),
|
"Copy the repository data files to the new native location inside SCM-Manager home directory. " +
|
||||||
INLINE(InlineMigrationStrategy.class);
|
"This will keep the original directory."),
|
||||||
|
MOVE(MoveMigrationStrategy.class,
|
||||||
|
"Move the repository data files to the new native location inside SCM-Manager home directory. " +
|
||||||
|
"The original directory will be deleted."),
|
||||||
|
INLINE(InlineMigrationStrategy.class,
|
||||||
|
"Use the current directory where the repository data files are stored, but modify the directory " +
|
||||||
|
"structure so that it can be used for SCM-Manager v2. The repository data files will be moved to a new " +
|
||||||
|
"subdirectory 'data' inside the current directory."),
|
||||||
|
IGNORE(IgnoreMigrationStrategy.class,
|
||||||
|
"The repository will not be migrated and will not be visible inside SCM-Manager. " +
|
||||||
|
"The data files will be kept at the current location."),
|
||||||
|
DELETE(DeleteMigrationStrategy.class,
|
||||||
|
"The repository will not be migrated and will not be visible inside SCM-Manager. " +
|
||||||
|
"The data files will be deleted!");
|
||||||
|
|
||||||
private Class<? extends Instance> implementationClass;
|
private final Class<? extends Instance> implementationClass;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
MigrationStrategy(Class<? extends Instance> implementationClass) {
|
MigrationStrategy(Class<? extends Instance> implementationClass, String description) {
|
||||||
this.implementationClass = implementationClass;
|
this.implementationClass = implementationClass;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends Instance> getImplementationClass() {
|
||||||
|
return implementationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance from(Injector injector) {
|
Instance from(Injector injector) {
|
||||||
@@ -21,6 +45,6 @@ enum MigrationStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Instance {
|
interface Instance {
|
||||||
Path migrate(String id, String name, String type);
|
Optional<Path> migrate(String id, String name, String type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore;
|
|||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
public class MigrationStrategyDao {
|
public class MigrationStrategyDao {
|
||||||
|
|
||||||
private final RepositoryMigrationPlan plan;
|
private final RepositoryMigrationPlan plan;
|
||||||
@@ -17,12 +19,12 @@ public class MigrationStrategyDao {
|
|||||||
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
|
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<MigrationStrategy> get(String id) {
|
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
|
||||||
return plan.get(id);
|
return plan.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set(String repositoryId, MigrationStrategy strategy) {
|
public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) {
|
||||||
plan.set(repositoryId, strategy);
|
plan.set(repositoryId, strategy, newNamespace, newName);
|
||||||
store.set(plan);
|
store.set(plan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
|
||||||
class MoveMigrationStrategy extends BaseMigrationStrategy {
|
class MoveMigrationStrategy extends BaseMigrationStrategy {
|
||||||
|
|
||||||
@@ -27,14 +29,15 @@ class MoveMigrationStrategy extends BaseMigrationStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Path migrate(String id, String name, String type) {
|
public Optional<Path> migrate(String id, String name, String type) {
|
||||||
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
|
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
|
||||||
Path targetDataPath = repositoryBasePath
|
Path targetDataPath = repositoryBasePath
|
||||||
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
|
||||||
Path sourceDataPath = getSourceDataPath(name, type);
|
Path sourceDataPath = getSourceDataPath(name, type);
|
||||||
|
LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath);
|
||||||
moveData(sourceDataPath, targetDataPath);
|
moveData(sourceDataPath, targetDataPath);
|
||||||
deleteOldDataDir(getTypeDependentPath(type), name);
|
deleteOldDataDir(getTypeDependentPath(type), name);
|
||||||
return repositoryBasePath;
|
return of(repositoryBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteOldDataDir(Path rootPath, String name) {
|
private void deleteOldDataDir(Path rootPath, String name) {
|
||||||
|
|||||||
@@ -13,57 +13,74 @@ import static java.util.Arrays.asList;
|
|||||||
@XmlRootElement(name = "repository-migration")
|
@XmlRootElement(name = "repository-migration")
|
||||||
class RepositoryMigrationPlan {
|
class RepositoryMigrationPlan {
|
||||||
|
|
||||||
private List<RepositoryEntry> entries;
|
private List<RepositoryMigrationEntry> entries;
|
||||||
|
|
||||||
RepositoryMigrationPlan() {
|
RepositoryMigrationPlan() {
|
||||||
this(new RepositoryEntry[0]);
|
this(new RepositoryMigrationEntry[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
RepositoryMigrationPlan(RepositoryEntry... entries) {
|
RepositoryMigrationPlan(RepositoryMigrationEntry... entries) {
|
||||||
this.entries = new ArrayList<>(asList(entries));
|
this.entries = new ArrayList<>(asList(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<MigrationStrategy> get(String repositoryId) {
|
Optional<RepositoryMigrationEntry> 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()
|
return entries.stream()
|
||||||
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
|
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) {
|
||||||
|
Optional<RepositoryMigrationEntry> entry = get(repositoryId);
|
||||||
|
if (entry.isPresent()) {
|
||||||
|
entry.get().setStrategy(strategy);
|
||||||
|
entry.get().setNewNamespace(newNamespace);
|
||||||
|
entry.get().setNewName(newName);
|
||||||
|
} else {
|
||||||
|
entries.add(new RepositoryMigrationEntry(repositoryId, strategy, newNamespace, newName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@XmlRootElement(name = "entries")
|
@XmlRootElement(name = "entries")
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
static class RepositoryEntry {
|
static class RepositoryMigrationEntry {
|
||||||
|
|
||||||
private String repositoryId;
|
private String repositoryId;
|
||||||
private MigrationStrategy dataMigrationStrategy;
|
private MigrationStrategy dataMigrationStrategy;
|
||||||
|
private String newNamespace;
|
||||||
|
private String newName;
|
||||||
|
|
||||||
RepositoryEntry() {
|
RepositoryMigrationEntry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
|
RepositoryMigrationEntry(String repositoryId, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) {
|
||||||
this.repositoryId = repositoryId;
|
this.repositoryId = repositoryId;
|
||||||
this.dataMigrationStrategy = dataMigrationStrategy;
|
this.dataMigrationStrategy = dataMigrationStrategy;
|
||||||
|
this.newNamespace = newNamespace;
|
||||||
|
this.newName = newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MigrationStrategy getDataMigrationStrategy() {
|
public MigrationStrategy getDataMigrationStrategy() {
|
||||||
return dataMigrationStrategy;
|
return dataMigrationStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNewNamespace() {
|
||||||
|
return newNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewName() {
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
private void setStrategy(MigrationStrategy strategy) {
|
private void setStrategy(MigrationStrategy strategy) {
|
||||||
this.dataMigrationStrategy = strategy;
|
this.dataMigrationStrategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setNewNamespace(String newNamespace) {
|
||||||
|
this.newNamespace = newNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNewName(String newName) {
|
||||||
|
this.newName = newName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "permissions")
|
||||||
|
class V1Permission {
|
||||||
|
private boolean groupPermission;
|
||||||
|
private String name;
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
public boolean isGroupPermission() {
|
||||||
|
return groupPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import sonia.scm.update.properties.V1Properties;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
@XmlRootElement(name = "repositories")
|
||||||
|
public 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;
|
||||||
|
|
||||||
|
public V1Repository() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public V1Repository(String id, String type, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContact() {
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreationDate() {
|
||||||
|
return creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLastModified() {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPublic() {
|
||||||
|
return isPublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isArchived() {
|
||||||
|
return archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<V1Permission> getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public V1Properties getProperties() {
|
||||||
|
return 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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import sonia.scm.SCMContextProvider;
|
|||||||
import sonia.scm.migration.UpdateStep;
|
import sonia.scm.migration.UpdateStep;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||||
|
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
import sonia.scm.store.StoreConstants;
|
import sonia.scm.store.StoreConstants;
|
||||||
import sonia.scm.version.Version;
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
@@ -27,10 +28,12 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
|
|||||||
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
|
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
|
||||||
|
|
||||||
private final SCMContextProvider contextProvider;
|
private final SCMContextProvider contextProvider;
|
||||||
|
private final XmlRepositoryDAO repositoryDAO;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider) {
|
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
|
||||||
this.contextProvider = contextProvider;
|
this.contextProvider = contextProvider;
|
||||||
|
this.repositoryDAO = repositoryDAO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -41,6 +44,7 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
|
|||||||
if (Files.exists(oldRepositoriesFile)) {
|
if (Files.exists(oldRepositoriesFile)) {
|
||||||
LOG.info("moving old repositories database files to repository-paths file");
|
LOG.info("moving old repositories database files to repository-paths file");
|
||||||
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
|
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
|
||||||
|
repositoryDAO.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Optional.empty;
|
import static java.util.Optional.empty;
|
||||||
import static java.util.Optional.of;
|
import static java.util.Optional.of;
|
||||||
import static sonia.scm.version.Version.parse;
|
import static sonia.scm.version.Version.parse;
|
||||||
@@ -102,13 +103,30 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
|||||||
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
|
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
|
||||||
readV1Database(jaxbContext).ifPresent(
|
readV1Database(jaxbContext).ifPresent(
|
||||||
v1Database -> {
|
v1Database -> {
|
||||||
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
|
v1Database.repositoryList.repositories.forEach(this::readMigrationEntry);
|
||||||
v1Database.repositoryList.repositories.forEach(this::update);
|
v1Database.repositoryList.repositories.forEach(this::update);
|
||||||
backupOldRepositoriesFile();
|
backupOldRepositoriesFile();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<V1Repository> getRepositoriesWithoutMigrationStrategies() {
|
||||||
|
if (!resolveV1File().exists()) {
|
||||||
|
LOG.info("no v1 repositories database file found");
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class);
|
||||||
|
return readV1Database(jaxbContext)
|
||||||
|
.map(v1Database -> v1Database.repositoryList.repositories.stream())
|
||||||
|
.orElse(Stream.empty())
|
||||||
|
.filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new UpdateException("could not read v1 repository database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void backupOldRepositoriesFile() {
|
private void backupOldRepositoriesFile() {
|
||||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||||
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
|
||||||
@@ -122,61 +140,59 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void update(V1Repository v1Repository) {
|
private void update(V1Repository v1Repository) {
|
||||||
Path destination = handleDataDirectory(v1Repository);
|
RepositoryMigrationPlan.RepositoryMigrationEntry repositoryMigrationEntry = readMigrationEntry(v1Repository);
|
||||||
|
Optional<Path> destination = handleDataDirectory(v1Repository, repositoryMigrationEntry.getDataMigrationStrategy());
|
||||||
|
LOG.info("using strategy {} to migrate repository {} with id {} using new namespace {} and name {}",
|
||||||
|
repositoryMigrationEntry.getDataMigrationStrategy().getClass(),
|
||||||
|
v1Repository.getName(),
|
||||||
|
v1Repository.getId(),
|
||||||
|
repositoryMigrationEntry.getNewNamespace(),
|
||||||
|
repositoryMigrationEntry.getNewName());
|
||||||
|
destination.ifPresent(
|
||||||
|
newPath -> {
|
||||||
Repository repository = new Repository(
|
Repository repository = new Repository(
|
||||||
v1Repository.id,
|
v1Repository.getId(),
|
||||||
v1Repository.type,
|
v1Repository.getType(),
|
||||||
getNamespace(v1Repository),
|
repositoryMigrationEntry.getNewNamespace(),
|
||||||
getName(v1Repository),
|
repositoryMigrationEntry.getNewName(),
|
||||||
v1Repository.contact,
|
v1Repository.getContact(),
|
||||||
v1Repository.description,
|
v1Repository.getDescription(),
|
||||||
createPermissions(v1Repository));
|
createPermissions(v1Repository));
|
||||||
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
|
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.getName(), newPath);
|
||||||
repositoryDao.add(repository, destination);
|
repositoryDao.add(repository, newPath);
|
||||||
propertyStore.put(v1Repository.id, v1Repository.properties);
|
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path handleDataDirectory(V1Repository v1Repository) {
|
private Optional<Path> handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) {
|
||||||
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
|
return dataMigrationStrategy
|
||||||
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
|
.from(injector)
|
||||||
|
.migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
|
private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) {
|
||||||
return migrationStrategyDao.get(v1Repository.id)
|
return findMigrationStrategy(v1Repository)
|
||||||
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
|
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.getId() + " and name " + v1Repository.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> findMigrationStrategy(V1Repository v1Repository) {
|
||||||
|
return migrationStrategyDao.get(v1Repository.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
|
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
|
||||||
if (v1Repository.permissions == null) {
|
if (v1Repository.getPermissions() == null) {
|
||||||
return new RepositoryPermission[0];
|
return new RepositoryPermission[0];
|
||||||
}
|
}
|
||||||
return v1Repository.permissions
|
return v1Repository.getPermissions()
|
||||||
.stream()
|
.stream()
|
||||||
.map(this::createPermission)
|
.map(this::createPermission)
|
||||||
.toArray(RepositoryPermission[]::new);
|
.toArray(RepositoryPermission[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RepositoryPermission createPermission(V1Permission v1Permission) {
|
private RepositoryPermission createPermission(V1Permission v1Permission) {
|
||||||
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
|
LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName());
|
||||||
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
|
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission());
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
|
||||||
@@ -195,45 +211,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
|
|||||||
).toFile();
|
).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 {
|
private static class RepositoryList {
|
||||||
@XmlElement(name = "repository")
|
@XmlElement(name = "repository")
|
||||||
private List<V1Repository> repositories;
|
private List<V1Repository> repositories;
|
||||||
|
|||||||
14
scm-webapp/src/main/resources/templates/error.mustache
Normal file
14
scm-webapp/src/main/resources/templates/error.mustache
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{{< layout}}
|
||||||
|
|
||||||
|
{{$title}}SCM-Manager Error{{/title}}
|
||||||
|
|
||||||
|
{{$content}}
|
||||||
|
<h2 class="subtitle">An error occurred during SCM-Manager startup.</h2>
|
||||||
|
|
||||||
|
<div class="notification is-danger">
|
||||||
|
<pre>
|
||||||
|
{{ error }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
{{/content}}
|
||||||
|
{{/ layout}}
|
||||||
34
scm-webapp/src/main/resources/templates/layout.mustache
Normal file
34
scm-webapp/src/main/resources/templates/layout.mustache
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{$title}}SCM-Manager{{/title}}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ contextPath }}/styles/scm.css">
|
||||||
|
<link rel="shortcut icon" href="{{ contextPath }}/favicon.ico">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="App">
|
||||||
|
|
||||||
|
<section class="hero is-dark is-small">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-vcentered">
|
||||||
|
<div class="column"><img src="{{ contextPath }}/images/logo.png" alt="SCM-Manager"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">{{$title}}SCM-Manager{{/title}}</h1>
|
||||||
|
{{$content}}<!-- no content defined -->{{/content}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{$script}}<!-- no script defined -->{{/script}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{{<layout}}
|
||||||
|
|
||||||
|
{{$title}}SCM-Manager will restart to migrate the data{{/title}}
|
||||||
|
|
||||||
|
{{$content}}
|
||||||
|
<p class="has-text-centered">
|
||||||
|
<svg width="200px" version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
|
||||||
|
<path fill="#33B2E8" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
|
||||||
|
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="2s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="360 50 50"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
<path fill="#33B2E8" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
|
||||||
|
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="1s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="-360 50 50"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
<path fill="#33B2E8" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
|
||||||
|
L82,35.7z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="2s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="360 50 50"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</p>
|
||||||
|
{{/content}}
|
||||||
|
|
||||||
|
{{$script}}
|
||||||
|
<script>
|
||||||
|
setInterval(function () {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
|
||||||
|
request.open('GET', '{{ contextPath }}/api/v2/', true);
|
||||||
|
|
||||||
|
request.onload = function () {
|
||||||
|
if (this.readyState === 4 && this.status === 200 && this.response.toString().indexOf("_links") > 0) {
|
||||||
|
location.href = '{{ contextPath }}';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.send();
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
{{/script}}
|
||||||
|
|
||||||
|
{{/layout}}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{{< layout}}
|
||||||
|
|
||||||
|
{{$title}}SCM-Manager Migration{{/title}}
|
||||||
|
|
||||||
|
{{$content}}
|
||||||
|
<h2 class="subtitle">You have migrated from SCM-Manager v1 to SCM-Manager v2.</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To migrate the existing repositories you have to specify a namespace and a name for each on them
|
||||||
|
as well as a migration strategy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The strategies are the following:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
{{#strategies}}
|
||||||
|
<tr>
|
||||||
|
<th>{{name}}</th>
|
||||||
|
<td>{{description}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/strategies}}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{#validationErrorsFound}}
|
||||||
|
<div class="notification is-danger">Please correct the invalid namespaces or names below and try again.</div>
|
||||||
|
<hr>
|
||||||
|
{{/validationErrorsFound}}
|
||||||
|
|
||||||
|
<form action="{{submitUrl}}" method="post">
|
||||||
|
<table class="card-table table is-hoverable is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
<th>Original name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>New namespace
|
||||||
|
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The namespace of the repository. This will be part op the url. The new namespace must consist of letters, digits, dots, dashes and underscores (it must not start with a dash or a underscore)."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
|
||||||
|
</th>
|
||||||
|
<th>New name
|
||||||
|
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The name of the repository. This will be part op the url. The new name must consist of letters, digits, dots, dashes and underscores (it must not start with a dash or a underscore)."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
|
||||||
|
</th>
|
||||||
|
<th>Strategy
|
||||||
|
<span class="tooltip is-tooltip-right is-tooltip-multiline t-tooltip-0-1-38" data-tooltip="The strategy used to migrate the data directory of the repository. See above for the means of the different strategies."><i class="fa fa-question-circle has-text-info t-textinfo-0-1-10"></i></span>
|
||||||
|
<br>Change all:
|
||||||
|
<div class="field">
|
||||||
|
<div class="control select">
|
||||||
|
<select id="changeAll">
|
||||||
|
{{#strategies}}
|
||||||
|
<option>{{name}}</option>
|
||||||
|
{{/strategies}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{{#repositories}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{path}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{type}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="input {{#namespaceInvalid}}is-danger{{/namespaceInvalid}}" type="text" name="namespace-{{id}}" value="{{namespace}}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="input {{#nameInvalid}}is-danger{{/nameInvalid}}" type="text" name="name-{{id}}" value="{{name}}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control select">
|
||||||
|
<select class="strategy-select" name="strategy-{{id}}">
|
||||||
|
{{#strategies}}
|
||||||
|
<option{{#selected}} selected{{/selected}}>{{name}}</option>
|
||||||
|
{{/strategies}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/repositories}}
|
||||||
|
</table>
|
||||||
|
<button class="button is-primary" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
{{/content}}
|
||||||
|
|
||||||
|
{{$script}}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var changeAllSelector = document.getElementById('changeAll');
|
||||||
|
changeAllSelector.onchange = function () {
|
||||||
|
var strategySelects = document.getElementsByClassName('strategy-select');
|
||||||
|
for (var index in strategySelects) {
|
||||||
|
strategySelects[index].value = changeAllSelector.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{/script}}
|
||||||
|
|
||||||
|
{{/ layout}}
|
||||||
@@ -1,102 +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
|
|
||||||
|
|
||||||
|
|
||||||
-->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SCM-Manager support information</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin: 10px;
|
|
||||||
color: #202020;
|
|
||||||
font-family: Verdana,Helvetica,Arial,sans-serif;
|
|
||||||
font-size: 75%;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: #D20005;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 18px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
a:link, a:visited {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:link:hover, a:visited:hover {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border: 0 none;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 100%;
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
padding: 3px;
|
|
||||||
vertical-align: top;
|
|
||||||
border: 1px solid #CCCCCC;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.small {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SCM-Manager Repositories</h1>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{{#repositories}}
|
|
||||||
<li>
|
|
||||||
<a href="{{url}}">{{name}}</a>
|
|
||||||
</li>
|
|
||||||
{{/repositories}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,150 +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
|
|
||||||
|
|
||||||
|
|
||||||
-->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SCM-Manager support information</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin: 10px;
|
|
||||||
color: #202020;
|
|
||||||
font-family: Verdana,Helvetica,Arial,sans-serif;
|
|
||||||
font-size: 75%;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: #D20005;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 18px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
a:link, a:visited {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:link:hover, a:visited:hover {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border: 0 none;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 100%;
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
padding: 3px;
|
|
||||||
vertical-align: top;
|
|
||||||
border: 1px solid #CCCCCC;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.small {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SCM-Manager support information</h1>
|
|
||||||
|
|
||||||
<p>Information for SCM-Manager support.</p>
|
|
||||||
|
|
||||||
<h2>Version</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Version: {{version.version}}</li>
|
|
||||||
<li>Stage: {{version.stage}}</li>
|
|
||||||
<li>StoreFactory: {{version.storeFactory}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Configuration</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Anonymous Access Enabled: {{configuration.anonymousAccessEnabled}}</li>
|
|
||||||
<li>Enable Proxy: {{configuration.enableProxy}}</li>
|
|
||||||
<li>Force Base Url: {{configuration.forceBaseUrl}}</li>
|
|
||||||
<li>Disable Grouping Grid: {{configuration.disableGroupingGrid}}</li>
|
|
||||||
<li>Enable Repository Archive: {{configuration.enableRepositoryArchive}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Installed Plugins</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{{#pluginManager.installed}}
|
|
||||||
<li>{{id}}</li>
|
|
||||||
{{/pluginManager.installed}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Runtime</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Free Memory: {{runtime.freeMemory}}</li>
|
|
||||||
<li>Total Memory: {{runtime.totalMemory}}</li>
|
|
||||||
<li>Max Memory: {{runtime.maxMemory}}</li>
|
|
||||||
<li>Available Processors: {{runtime.availableProcessors}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>System</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>OS: {{system.os}}</li>
|
|
||||||
<li>Architecture: {{system.arch}}</li>
|
|
||||||
<li>ServletContainer: {{system.container}}</li>
|
|
||||||
<li>Java: {{system.java}}</li>
|
|
||||||
<li>Local: {{system.locale}}</li>
|
|
||||||
<li>TimeZone: {{system.timeZone}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Repository Handlers</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{{#repositoryHandlers}}
|
|
||||||
<li>{{type.displayName}}/{{type.name}} ({{versionInformation}})</li>
|
|
||||||
{{/repositoryHandlers}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
14
scm-webapp/src/main/resources/templates/too-old.mustache
Normal file
14
scm-webapp/src/main/resources/templates/too-old.mustache
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{{< layout}}
|
||||||
|
|
||||||
|
{{$title}}SCM-Manager Error{{/title}}
|
||||||
|
|
||||||
|
{{$content}}
|
||||||
|
<h2 class="subtitle">An error occurred during SCM-Manager startup.</h2>
|
||||||
|
|
||||||
|
<p class="notification is-danger">
|
||||||
|
We cannot migrate your SCM-Manager 1 installation,
|
||||||
|
because the version is too old.<br />
|
||||||
|
Please migrate to version 1.60 or newer, before migration to 2.x.
|
||||||
|
</p>
|
||||||
|
{{/content}}
|
||||||
|
{{/ layout}}
|
||||||
@@ -1,102 +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
|
|
||||||
|
|
||||||
|
|
||||||
-->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SCM-Manager Error</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<style type="text/css">
|
|
||||||
body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin: 10px;
|
|
||||||
color: #202020;
|
|
||||||
font-family: Verdana,Helvetica,Arial,sans-serif;
|
|
||||||
font-size: 75%;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5 {
|
|
||||||
font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: #D20005;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 18px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid #AFAFAF;
|
|
||||||
}
|
|
||||||
a:link, a:visited {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:link:hover, a:visited:hover {
|
|
||||||
color: #045491;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border: 0 none;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 100%;
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
padding: 3px;
|
|
||||||
vertical-align: top;
|
|
||||||
border: 1px solid #CCCCCC;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.small {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SCM-Manager Error</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
There is an error occurred during SCM-Manager startup.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
{{error}}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.template.Template;
|
||||||
|
import sonia.scm.template.TemplateEngine;
|
||||||
|
import sonia.scm.template.TemplateEngineFactory;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SingleViewServletTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TemplateEngineFactory templateEngineFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TemplateEngine templateEngine;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Template template;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletResponse response;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PrintWriter writer;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ViewController controller;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRenderTheTemplateOnGet() throws IOException {
|
||||||
|
prepareTemplate("/template");
|
||||||
|
doReturn(new View(200, "hello")).when(controller).createView(request);
|
||||||
|
|
||||||
|
new SingleViewServlet(templateEngineFactory, controller).doGet(request, response);
|
||||||
|
|
||||||
|
verifyResponse(200, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyResponse(int sc, Object model) throws IOException {
|
||||||
|
verify(response).setStatus(sc);
|
||||||
|
verify(response).setContentType("text/html");
|
||||||
|
verify(response).setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
verify(template).execute(writer, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRenderTheTemplateOnPost() throws IOException {
|
||||||
|
prepareTemplate("/template");
|
||||||
|
|
||||||
|
doReturn(new View(201, "hello")).when(controller).createView(request);
|
||||||
|
|
||||||
|
new SingleViewServlet(templateEngineFactory, controller).doPost(request, response);
|
||||||
|
|
||||||
|
verifyResponse(201, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowIllegalStateExceptionOnIOException() throws IOException {
|
||||||
|
doReturn("/template").when(controller).getTemplate();
|
||||||
|
doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension("/template");
|
||||||
|
doThrow(IOException.class).when(templateEngine).getTemplate("/template");
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> new SingleViewServlet(templateEngineFactory, controller));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTemplate(String templatePath) throws IOException {
|
||||||
|
doReturn(templateEngine).when(templateEngineFactory).getEngineByExtension(templatePath);
|
||||||
|
doReturn(template).when(templateEngine).getTemplate(templatePath);
|
||||||
|
doReturn(templatePath).when(controller).getTemplate();
|
||||||
|
|
||||||
|
doReturn(writer).when(response).getWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
111
scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java
Normal file
111
scm-webapp/src/test/java/sonia/scm/boot/SingleViewTest.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import com.google.inject.servlet.GuiceFilter;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.ServletContextEvent;
|
||||||
|
import javax.servlet.ServletContextListener;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SingleViewTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ServletContext servletContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Injector> captor;
|
||||||
|
|
||||||
|
private GuiceFilter guiceFilter;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpGuiceFilter() throws ServletException {
|
||||||
|
guiceFilter = new GuiceFilter();
|
||||||
|
FilterConfig config = mock(FilterConfig.class);
|
||||||
|
doReturn(servletContext).when(config).getServletContext();
|
||||||
|
guiceFilter.init(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDownGuiceFilter() {
|
||||||
|
guiceFilter.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateViewControllerForView() {
|
||||||
|
ServletContextListener listener = SingleView.view("/my-template", 409);
|
||||||
|
when(request.getContextPath()).thenReturn("/scm");
|
||||||
|
|
||||||
|
ViewController instance = findViewController(listener);
|
||||||
|
assertThat(instance.getTemplate()).isEqualTo("/my-template");
|
||||||
|
|
||||||
|
View view = instance.createView(request);
|
||||||
|
assertThat(view.getStatusCode()).isEqualTo(409);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateViewControllerForError() {
|
||||||
|
ServletContextListener listener = SingleView.error(new IOException("awesome io"));
|
||||||
|
when(request.getContextPath()).thenReturn("/scm");
|
||||||
|
|
||||||
|
ViewController instance = findViewController(listener);
|
||||||
|
assertErrorViewController(instance, "awesome io");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBindServlets() {
|
||||||
|
ServletContextListener listener = SingleView.error(new IOException("awesome io"));
|
||||||
|
Injector injector = findInjector(listener);
|
||||||
|
|
||||||
|
assertThat(injector.getInstance(StaticResourceServlet.class)).isNotNull();
|
||||||
|
assertThat(injector.getInstance(SingleViewServlet.class)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void assertErrorViewController(ViewController instance, String contains) {
|
||||||
|
assertThat(instance.getTemplate()).isEqualTo("/templates/error.mustache");
|
||||||
|
|
||||||
|
View view = instance.createView(request);
|
||||||
|
assertThat(view.getStatusCode()).isEqualTo(500);
|
||||||
|
assertThat(view.getModel()).isInstanceOfSatisfying(Map.class, map -> {
|
||||||
|
assertThat(map).containsEntry("contextPath", "/scm");
|
||||||
|
String error = (String) map.get("error");
|
||||||
|
assertThat(error).contains(contains);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ViewController findViewController(ServletContextListener listener) {
|
||||||
|
Injector injector = findInjector(listener);
|
||||||
|
return injector.getInstance(ViewController.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Injector findInjector(ServletContextListener listener) {
|
||||||
|
listener.contextInitialized(new ServletContextEvent(servletContext));
|
||||||
|
|
||||||
|
verify(servletContext).setAttribute(anyString(), captor.capture());
|
||||||
|
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.ServletOutputStream;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class StaticResourceServletTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ServletOutputStream stream;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletResponse response;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ServletContext context;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldServeResource() throws IOException {
|
||||||
|
doReturn("/scm").when(request).getContextPath();
|
||||||
|
doReturn("/scm/resource.txt").when(request).getRequestURI();
|
||||||
|
doReturn(context).when(request).getServletContext();
|
||||||
|
URL resource = Resources.getResource("sonia/scm/boot/resource.txt");
|
||||||
|
doReturn(resource).when(context).getResource("/resource.txt");
|
||||||
|
doReturn(stream).when(response).getOutputStream();
|
||||||
|
|
||||||
|
StaticResourceServlet servlet = new StaticResourceServlet();
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(HttpServletResponse.SC_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNotFound() throws IOException {
|
||||||
|
doReturn("/scm").when(request).getContextPath();
|
||||||
|
doReturn("/scm/resource.txt").when(request).getRequestURI();
|
||||||
|
doReturn(context).when(request).getServletContext();
|
||||||
|
|
||||||
|
StaticResourceServlet servlet = new StaticResourceServlet();
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java
Normal file
86
scm-webapp/src/test/java/sonia/scm/boot/VersionsTest.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package sonia.scm.boot;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.SCMContextProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
|
||||||
|
@ExtendWith({MockitoExtension.class, TempDirectory.class})
|
||||||
|
class VersionsTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SCMContextProvider contextProvider;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private Versions versions;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueForVersionsPreviousTo160(@TempDirectory.TempDir Path directory) throws IOException {
|
||||||
|
setVersion(directory, "1.59");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isTrue();
|
||||||
|
|
||||||
|
setVersion(directory, "1.12");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseForVersion160(@TempDirectory.TempDir Path directory) throws IOException {
|
||||||
|
setVersion(directory, "1.60");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailIfVersionContainsLineBreak(@TempDirectory.TempDir Path directory) throws IOException {
|
||||||
|
setVersion(directory, "1.59\n");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseForVersionsNewerAs160(@TempDirectory.TempDir Path directory) throws IOException {
|
||||||
|
setVersion(directory, "1.61");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isFalse();
|
||||||
|
|
||||||
|
setVersion(directory, "1.82");
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseForNonExistingVersionFile(@TempDirectory.TempDir Path directory) {
|
||||||
|
setVersionFile(directory.resolve("version.txt"));
|
||||||
|
assertThat(versions.isPreviousVersionTooOld()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWriteNewVersion(@TempDirectory.TempDir Path directory) {
|
||||||
|
Path config = directory.resolve("config");
|
||||||
|
doReturn(config).when(contextProvider).resolve(Paths.get("config"));
|
||||||
|
doReturn("2.0.0").when(contextProvider).getVersion();
|
||||||
|
|
||||||
|
versions.writeNewVersion();
|
||||||
|
|
||||||
|
Path versionFile = config.resolve("version.txt");
|
||||||
|
assertThat(versionFile).exists().hasContent("2.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVersion(Path directory, String version) throws IOException {
|
||||||
|
Path file = directory.resolve("version.txt");
|
||||||
|
Files.write(file, version.getBytes(StandardCharsets.UTF_8));
|
||||||
|
setVersionFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVersionFile(Path file) {
|
||||||
|
doReturn(file).when(contextProvider).resolve(Paths.get("config", "version.txt"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,16 +35,21 @@ package sonia.scm.template;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.Test;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
import javax.servlet.ServletContext;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -68,7 +73,10 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
|
|||||||
when(loader.getUberClassLoader()).thenReturn(
|
when(loader.getUberClassLoader()).thenReturn(
|
||||||
Thread.currentThread().getContextClassLoader());
|
Thread.currentThread().getContextClassLoader());
|
||||||
|
|
||||||
return new MustacheTemplateEngine(context, loader);
|
MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder();
|
||||||
|
holder.pluginLoader = loader;
|
||||||
|
|
||||||
|
return new MustacheTemplateEngine(context, holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
@@ -116,4 +124,18 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
|
|||||||
return MustacheTemplateEngineTest.class.getResourceAsStream(
|
return MustacheTemplateEngineTest.class.getResourceAsStream(
|
||||||
"/sonia/scm/template/".concat(resource).concat(".mustache"));
|
"/sonia/scm/template/".concat(resource).concat(".mustache"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateEngineWithoutPluginLoader() throws IOException {
|
||||||
|
ServletContext context = mock(ServletContext.class);
|
||||||
|
MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder();
|
||||||
|
MustacheTemplateEngine engine = new MustacheTemplateEngine(context, holder);
|
||||||
|
|
||||||
|
Template template = engine.getTemplate(getTemplateResource());
|
||||||
|
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
template.execute(writer, ImmutableMap.of("name", "World"));
|
||||||
|
|
||||||
|
Assertions.assertThat(writer.toString()).isEqualTo("Hello World!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package sonia.scm.update;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategy;
|
||||||
|
import sonia.scm.update.repository.MigrationStrategyDao;
|
||||||
|
import sonia.scm.update.repository.V1Repository;
|
||||||
|
import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class MigrationWizardServletTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
XmlRepositoryV1UpdateStep updateStep;
|
||||||
|
@Mock
|
||||||
|
MigrationStrategyDao migrationStrategyDao;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
HttpServletRequest request;
|
||||||
|
@Mock
|
||||||
|
HttpServletResponse response;
|
||||||
|
|
||||||
|
String renderedTemplateName;
|
||||||
|
Map<String, Object> renderedModel;
|
||||||
|
|
||||||
|
MigrationWizardServlet servlet;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initServlet() {
|
||||||
|
servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) {
|
||||||
|
@Override
|
||||||
|
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
|
||||||
|
renderedTemplateName = templateName;
|
||||||
|
renderedModel = model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "simple"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("git");
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("name")
|
||||||
|
.contains("simple");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "two/dirs"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("two");
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("name")
|
||||||
|
.contains("dirs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "more/than/two/dirs"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("namespace")
|
||||||
|
.contains("more");
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("name")
|
||||||
|
.contains("than_two_dirs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseTypeAndNameAsPath() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("path")
|
||||||
|
.contains("git/name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepId() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("id")
|
||||||
|
.contains("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotBeInvalidAtFirstRequest() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(false);
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("namespaceInvalid")
|
||||||
|
.contains(false);
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("nameInvalid")
|
||||||
|
.contains(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldValidateNamespaceAndNameOnPost() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
doReturn("invalid namespace").when(request).getParameter("namespace-id");
|
||||||
|
doReturn("invalid name").when(request).getParameter("name-id");
|
||||||
|
doReturn("COPY").when(request).getParameter("strategy-id");
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(true);
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("namespaceInvalid")
|
||||||
|
.contains(true);
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("nameInvalid")
|
||||||
|
.contains(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepSelectedMigrationStrategy() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
|
||||||
|
doReturn("we need an").when(request).getParameter("namespace-id");
|
||||||
|
doReturn("error for this test").when(request).getParameter("name-id");
|
||||||
|
doReturn("INLINE").when(request).getParameter("strategy-id");
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("selectedStrategy")
|
||||||
|
.contains(MigrationStrategy.INLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseCopyWithoutMigrationStrategy() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
|
||||||
|
doReturn("we need an").when(request).getParameter("namespace-id");
|
||||||
|
doReturn("error for this test").when(request).getParameter("name-id");
|
||||||
|
doReturn("").when(request).getParameter("strategy-id");
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
assertThat(renderedModel.get("repositories"))
|
||||||
|
.asList()
|
||||||
|
.extracting("selectedStrategy")
|
||||||
|
.contains(MigrationStrategy.COPY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldStoreValidMigration() {
|
||||||
|
when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
|
||||||
|
Collections.singletonList(new V1Repository("id", "git", "name"))
|
||||||
|
);
|
||||||
|
doReturn("namespace").when(request).getParameter("namespace-id");
|
||||||
|
doReturn("name").when(request).getParameter("name-id");
|
||||||
|
doReturn("COPY").when(request).getParameter("strategy-id");
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
verify(migrationStrategyDao).set("id", MigrationStrategy.COPY, "namespace", "name");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,18 +43,18 @@ class CopyMigrationStrategyTest {
|
|||||||
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||||
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||||
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||||
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
|
||||||
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
|
||||||
assertThat(target.resolve("data")).exists();
|
assertThat(target.resolve("data")).exists();
|
||||||
Path originalDataDir = tempDir
|
Path originalDataDir = tempDir
|
||||||
.resolve("repositories")
|
.resolve("repositories")
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import org.junitpioneer.jupiter.TempDirectory;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
|
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(TempDirectory.class)
|
@ExtendWith(TempDirectory.class)
|
||||||
@@ -20,9 +23,14 @@ class InlineMigrationStrategyTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
SCMContextProvider contextProvider;
|
SCMContextProvider contextProvider;
|
||||||
|
@Mock
|
||||||
|
PathBasedRepositoryLocationResolver locationResolver;
|
||||||
|
@Mock
|
||||||
|
RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance);
|
||||||
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,13 +41,14 @@ class InlineMigrationStrategyTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
|
||||||
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
|
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
|
||||||
|
verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
||||||
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
|
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sonia.scm.update.repository;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.RepositoryRole;
|
||||||
|
import sonia.scm.repository.xml.SingleRepositoryUpdateProcessor;
|
||||||
|
import sonia.scm.security.SystemRepositoryPermissionProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class MigrateVerbsToPermissionRolesTest {
|
||||||
|
|
||||||
|
private static final String EXISTING_REPOSITORY_ID = "id";
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SingleRepositoryUpdateProcessor singleRepositoryUpdateProcessor;
|
||||||
|
@Mock
|
||||||
|
private SystemRepositoryPermissionProvider systemRepositoryPermissionProvider;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private MigrateVerbsToPermissionRoles migration;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
URL metadataUrl = Resources.getResource("sonia/scm/update/repository/metadataWithoutRoles.xml");
|
||||||
|
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
((BiConsumer<String, Path>) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir);
|
||||||
|
return null;
|
||||||
|
}).when(singleRepositoryUpdateProcessor).doUpdate(any());
|
||||||
|
when(systemRepositoryPermissionProvider.availableRoles()).thenReturn(Collections.singletonList(new RepositoryRole("ROLE", asList("read", "write"), "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateToRolesIfPossible(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
migration.doUpdate();
|
||||||
|
|
||||||
|
List<String> newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml"));
|
||||||
|
Assertions.assertThat(newMetadata.stream().map(String::trim)).
|
||||||
|
containsSubsequence(
|
||||||
|
"<groupPermission>false</groupPermission>",
|
||||||
|
"<name>user</name>",
|
||||||
|
"<role>ROLE</role>"
|
||||||
|
)
|
||||||
|
.containsSubsequence(
|
||||||
|
"<groupPermission>true</groupPermission>",
|
||||||
|
"<name>group</name>",
|
||||||
|
"<verb>special</verb>"
|
||||||
|
)
|
||||||
|
.doesNotContain(
|
||||||
|
"<verb>read</verb>",
|
||||||
|
"<verb>write</verb>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||||
import sonia.scm.update.repository.MigrationStrategy;
|
|
||||||
import sonia.scm.update.repository.MigrationStrategyDao;
|
|
||||||
|
|
||||||
import javax.xml.bind.JAXBException;
|
import javax.xml.bind.JAXBException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -37,23 +35,31 @@ class MigrationStrategyDaoTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
|
void shouldReturnEmptyOptionalWhenStoreIsEmpty() {
|
||||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
Optional<MigrationStrategy> strategy = dao.get("any");
|
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("any");
|
||||||
|
|
||||||
Assertions.assertThat(strategy).isEmpty();
|
Assertions.assertThat(entry).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnNewValue() throws JAXBException {
|
void shouldReturnNewValue() {
|
||||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
dao.set("id", INLINE);
|
dao.set("id", INLINE, "space", "name");
|
||||||
|
|
||||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("id");
|
||||||
|
|
||||||
Assertions.assertThat(strategy).contains(INLINE);
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy)
|
||||||
|
.contains(INLINE);
|
||||||
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace)
|
||||||
|
.contains("space");
|
||||||
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName)
|
||||||
|
.contains("name");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -62,16 +68,24 @@ class MigrationStrategyDaoTest {
|
|||||||
void initExistingDatabase() throws JAXBException {
|
void initExistingDatabase() throws JAXBException {
|
||||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
dao.set("id", INLINE);
|
dao.set("id", INLINE, "space", "name");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindExistingValue() throws JAXBException {
|
void shouldFindExistingValue() throws JAXBException {
|
||||||
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
|
||||||
|
|
||||||
Optional<MigrationStrategy> strategy = dao.get("id");
|
Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> entry = dao.get("id");
|
||||||
|
|
||||||
Assertions.assertThat(strategy).contains(INLINE);
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy)
|
||||||
|
.contains(INLINE);
|
||||||
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace)
|
||||||
|
.contains("space");
|
||||||
|
Assertions.assertThat(entry)
|
||||||
|
.map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName)
|
||||||
|
.contains("name");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package sonia.scm.update.repository;
|
|||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import sonia.scm.update.repository.MigrationStrategy.Instance;
|
import sonia.scm.update.repository.MigrationStrategy.Instance;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Optional.of;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -20,6 +23,13 @@ class MigrationStrategyMock {
|
|||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
|
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (MigrationStrategy strategy : MigrationStrategy.values()) {
|
||||||
|
MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass());
|
||||||
|
when(strategyMock.migrate(any(), any(), any())).thenReturn(of(Paths.get("")));
|
||||||
|
lenient().when(mock.getInstance((Class<MigrationStrategy.Instance>) strategy.getImplementationClass())).thenReturn(strategyMock);
|
||||||
|
}
|
||||||
|
|
||||||
return mock;
|
return mock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,18 +40,18 @@ class MoveMigrationStrategyTest {
|
|||||||
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
|
||||||
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
|
||||||
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
|
||||||
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
|
||||||
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
|
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
|
||||||
assertThat(target.resolve("data")).exists();
|
assertThat(target.resolve("data")).exists();
|
||||||
Path originalDataDir = tempDir
|
Path originalDataDir = tempDir
|
||||||
.resolve("repositories")
|
.resolve("repositories")
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.junitpioneer.jupiter.TempDirectory;
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||||
|
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
|
|
||||||
import javax.xml.bind.JAXBException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -16,12 +16,14 @@ import java.nio.file.Path;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(TempDirectory.class)
|
@ExtendWith(TempDirectory.class)
|
||||||
class XmlRepositoryFileNameUpdateStepTest {
|
class XmlRepositoryFileNameUpdateStepTest {
|
||||||
|
|
||||||
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
|
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
|
||||||
|
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
|
||||||
@@ -29,8 +31,8 @@ class XmlRepositoryFileNameUpdateStepTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
|
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider);
|
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
|
||||||
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
||||||
Path configDir = tempDir.resolve("config");
|
Path configDir = tempDir.resolve("config");
|
||||||
Files.createDirectories(configDir);
|
Files.createDirectories(configDir);
|
||||||
@@ -40,5 +42,6 @@ class XmlRepositoryFileNameUpdateStepTest {
|
|||||||
|
|
||||||
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
|
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
|
||||||
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
|
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
|
||||||
|
verify(repositoryDAO).refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.mockito.ArgumentCaptor;
|
|||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryPermission;
|
import sonia.scm.repository.RepositoryPermission;
|
||||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
@@ -33,11 +34,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
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;
|
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -89,9 +89,14 @@ class XmlRepositoryV1UpdateStepTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void createMigrationPlan() {
|
void createMigrationPlan() {
|
||||||
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
|
Answer<Object> planAnswer = invocation -> {
|
||||||
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
|
String id = invocation.getArgument(0).toString();
|
||||||
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
|
return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, MOVE, "namespace-" + id, "name-" + id));
|
||||||
|
};
|
||||||
|
|
||||||
|
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenAnswer(planAnswer);
|
||||||
|
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenAnswer(planAnswer);
|
||||||
|
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenAnswer(planAnswer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,56 +109,20 @@ class XmlRepositoryV1UpdateStepTest {
|
|||||||
void shouldMapAttributes() throws JAXBException {
|
void shouldMapAttributes() throws JAXBException {
|
||||||
updateStep.doUpdate();
|
updateStep.doUpdate();
|
||||||
|
|
||||||
Optional<Repository> repository = findByNamespace("git");
|
Optional<Repository> repository = findByNamespace("namespace-3b91caa5-59c3-448f-920b-769aaa56b761");
|
||||||
|
|
||||||
assertThat(repository)
|
assertThat(repository)
|
||||||
.get()
|
.get()
|
||||||
.hasFieldOrPropertyWithValue("type", "git")
|
.hasFieldOrPropertyWithValue("type", "git")
|
||||||
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
|
||||||
.hasFieldOrPropertyWithValue("description", "A simple repository without directories.");
|
.hasFieldOrPropertyWithValue("description", "A repository with two folders.");
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
@Test
|
||||||
void shouldMapPermissions() throws JAXBException {
|
void shouldMapPermissions() throws JAXBException {
|
||||||
updateStep.doUpdate();
|
updateStep.doUpdate();
|
||||||
|
|
||||||
Optional<Repository> repository = findByNamespace("git");
|
Optional<Repository> repository = findByNamespace("namespace-454972da-faf9-4437-b682-dc4a4e0aa8eb");
|
||||||
|
|
||||||
assertThat(repository.get().getPermissions())
|
assertThat(repository.get().getPermissions())
|
||||||
.hasSize(3)
|
.hasSize(3)
|
||||||
@@ -176,14 +145,27 @@ class XmlRepositoryV1UpdateStepTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
|
||||||
Path targetDir = tempDir.resolve("someDir");
|
Path targetDir = tempDir.resolve("someDir");
|
||||||
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class);
|
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class);
|
||||||
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir);
|
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir));
|
||||||
|
|
||||||
updateStep.doUpdate();
|
updateStep.doUpdate();
|
||||||
|
|
||||||
assertThat(locationCaptor.getAllValues()).contains(targetDir);
|
assertThat(locationCaptor.getAllValues()).contains(targetDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSkipWhenStrategyGivesNoNewPath() throws JAXBException {
|
||||||
|
for (MigrationStrategy strategy : MigrationStrategy.values()) {
|
||||||
|
MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass());
|
||||||
|
lenient().when(strategyMock.migrate(any(), any(), any())).thenReturn(empty());
|
||||||
|
lenient().when(injectorMock.getInstance((Class<MigrationStrategy.Instance>) strategy.getImplementationClass())).thenReturn(strategyMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStep.doUpdate();
|
||||||
|
|
||||||
|
assertThat(locationCaptor.getAllValues()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFailForMissingMigrationStrategy() {
|
void shouldFailForMissingMigrationStrategy() {
|
||||||
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
|
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
|
||||||
@@ -221,6 +203,25 @@ class XmlRepositoryV1UpdateStepTest {
|
|||||||
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
|
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetNoMissingStrategiesWithFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
createFormerV2RepositoriesFile(tempDir);
|
||||||
|
|
||||||
|
assertThat(updateStep.getRepositoriesWithoutMigrationStrategies()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindMissingStrategies(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
V1RepositoryFileSystem.createV1Home(tempDir);
|
||||||
|
|
||||||
|
assertThat(updateStep.getRepositoriesWithoutMigrationStrategies())
|
||||||
|
.extracting("id")
|
||||||
|
.contains(
|
||||||
|
"3b91caa5-59c3-448f-920b-769aaa56b761",
|
||||||
|
"c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f",
|
||||||
|
"454972da-faf9-4437-b682-dc4a4e0aa8eb");
|
||||||
|
}
|
||||||
|
|
||||||
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
|
||||||
Path configDir = tempDir.resolve("config");
|
Path configDir = tempDir.resolve("config");
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Resource for testing
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<repositories>
|
||||||
|
<properties/>
|
||||||
|
<contact>ich@du.er</contact>
|
||||||
|
<creationDate>1557729536519</creationDate>
|
||||||
|
<description/>
|
||||||
|
<id>B3RQKYNzo2</id>
|
||||||
|
<lastModified>1557825677782</lastModified>
|
||||||
|
<namespace>scmadmin</namespace>
|
||||||
|
<name>git</name>
|
||||||
|
<permission>
|
||||||
|
<groupPermission>false</groupPermission>
|
||||||
|
<name>user</name>
|
||||||
|
<verb>read</verb>
|
||||||
|
<verb>write</verb>
|
||||||
|
</permission>
|
||||||
|
<permission>
|
||||||
|
<groupPermission>true</groupPermission>
|
||||||
|
<name>group</name>
|
||||||
|
<verb>special</verb>
|
||||||
|
</permission>
|
||||||
|
<public>false</public>
|
||||||
|
<archived>false</archived>
|
||||||
|
<type>git</type>
|
||||||
|
</repositories>
|
||||||
Reference in New Issue
Block a user