Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-06-13 11:24:32 +02:00
85 changed files with 2644 additions and 1255 deletions

View File

@@ -33,14 +33,16 @@ package sonia.scm.boot;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Stage;
import sonia.scm.event.Event;
/**
* 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
* the whole server, to see our changes. The restart event can only be used in
* stage {@link Stage#DEVELOPMENT}.
* the whole server, to see our changes. The restart event could also be used
* 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
* @since 2.0.0

View File

@@ -13,8 +13,27 @@ public abstract class RepositoryLocationResolver {
return create(type);
}
@FunctionalInterface
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);
/**
* 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);
}
}

View File

@@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler {
* @return decrypted value
*/
public String decode(char[] plainKey, String value) {
String result = null;
Base64.Decoder decoder = Base64.getUrlDecoder();
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[] encoded = new byte[encodedInput.length - SALT_LENGTH];
@@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler {
byte[] decoded = cipher.doFinal(encoded);
result = new String(decoded, ENCODING);
return new String(decoded, ENCODING);
} catch (IOException | GeneralSecurityException ex) {
throw new CipherException("could not decode string", ex);
}
return result;
}
@Override

View File

@@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable
@Override
public int hashCode()
{
return value.hashCode();
return value == null? -1: value.hashCode();
}
/**

View File

@@ -147,6 +147,10 @@ public class ValidationUtilTest
public void testIsRepositoryNameValid() {
String[] validPaths = {
"scm",
"scm-",
"scm_",
"s_cm",
"s-cm",
"s",
"sc",
".hiddenrepo",
@@ -206,7 +210,8 @@ public class ValidationUtilTest
"a/..b",
"scm/main",
"scm/plugins/git-plugin",
"scm/plugins/git-plugin"
"_scm",
"-scm"
};
for (String path : validPaths) {

View File

@@ -1,14 +1,14 @@
package sonia.scm.repository.xml;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.BasicRepositoryLocationResolver;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.store.StoreConstants;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Map;
@@ -28,12 +28,14 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
*
* @since 2.0.0
*/
@Singleton
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
public static final String STORE_NAME = "repository-paths";
private final SCMContextProvider contextProvider;
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
private final FileSystem fileSystem;
private final PathDatabase pathDatabase;
private final Map<String, Path> pathById;
@@ -44,14 +46,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
private Long lastModified;
@Inject
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) {
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);
this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
this.fileSystem = fileSystem;
this.pathById = new ConcurrentHashMap<>();
this.clock = clock;
@@ -64,23 +67,43 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
@Override
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
return repositoryId -> {
return new RepositoryLocationResolverInstance<T>() {
@Override
public T getLocation(String repositoryId) {
if (pathById.containsKey(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 {
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 path = initialRepositoryLocationResolver.getPath(repositoryId);
pathById.put(repositoryId, path);
writePathDatabase();
setLocation(repositoryId, path);
Path resolvedPath = contextProvider.resolve(path);
try {
Files.createDirectories(resolvedPath);
} catch (IOException e) {
fileSystem.create(resolvedPath.toFile());
} catch (Exception e) {
throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e);
}
return resolvedPath;
@@ -138,4 +161,13 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
private void setLocation(String repositoryId, Path repositoryBasePath) {
pathById.put(repositoryId, repositoryBasePath);
writePathDatabase();
}
public void refresh() {
this.read();
}
}

View File

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

View File

@@ -198,4 +198,11 @@ public class XmlRepositoryDAO implements RepositoryDAO {
public Long getLastModified() {
return repositoryLocationResolver.getLastModified();
}
public void refresh() {
repositoryLocationResolver.refresh();
byNamespaceAndName.clear();
byId.clear();
init();
}
}

View File

@@ -11,6 +11,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import java.io.IOException;
@@ -41,6 +43,8 @@ class PathBasedRepositoryLocationResolverTest {
@Mock
private Clock clock;
private final FileSystem fileSystem = new DefaultFileSystem();
private Path basePath;
private PathBasedRepositoryLocationResolver resolver;
@@ -57,7 +61,7 @@ class PathBasedRepositoryLocationResolverTest {
@Test
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).isDirectory();
@@ -65,7 +69,7 @@ class PathBasedRepositoryLocationResolverTest {
@Test
void shouldPersistInitialDirectory() {
resolver.forClass(Path.class).getLocation("newId");
resolver.forClass(Path.class).createLocation("newId");
String content = getXmlFileContent();
@@ -78,7 +82,7 @@ class PathBasedRepositoryLocationResolverTest {
long now = CREATION_TIME + 100;
when(clock.millis()).thenReturn(now);
resolver.forClass(Path.class).getLocation("newId");
resolver.forClass(Path.class).createLocation("newId");
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
@@ -91,7 +95,7 @@ class PathBasedRepositoryLocationResolverTest {
long now = CREATION_TIME + 100;
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.getLastModified()).isEqualTo(now);
@@ -108,8 +112,8 @@ class PathBasedRepositoryLocationResolverTest {
@BeforeEach
void createExistingDatabase() {
resolver.forClass(Path.class).getLocation("existingId_1");
resolver.forClass(Path.class).getLocation("existingId_2");
resolver.forClass(Path.class).createLocation("existingId_1");
resolver.forClass(Path.class).createLocation("existingId_2");
resolverWithExistingData = createResolver();
}
@@ -159,7 +163,7 @@ class PathBasedRepositoryLocationResolverTest {
}
private PathBasedRepositoryLocationResolver createResolver() {
return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock);
return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock);
}
private String content(Path storePath) {

View File

@@ -8,8 +8,6 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -19,6 +17,7 @@ import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryPermission;
import java.io.IOException;
@@ -32,7 +31,9 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -47,16 +48,29 @@ class XmlRepositoryDAOTest {
@Mock
private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao;
@BeforeEach
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.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
}
@@ -268,22 +282,6 @@ class XmlRepositoryDAOTest {
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) {
Path storePath = metadataFile(id);
@@ -303,8 +301,61 @@ class XmlRepositoryDAOTest {
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);
}
}

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package sonia.scm;
import sonia.scm.repository.BasicRepositoryLocationResolver;
import sonia.scm.repository.RepositoryLocationResolver;
import java.io.File;
import java.nio.file.Path;
@@ -16,6 +15,21 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe
@Override
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");
}
};
}
}

View File

@@ -34,6 +34,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
import org.mockito.stubbing.Answer;
import sonia.scm.AbstractTestBase;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
@@ -82,11 +83,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.create(any())).thenReturn(instanceMock);
when(locationResolver.supportsLocationType(any())).thenReturn(true);
when(instanceMock.getLocation(anyString())).then(ic -> {
Answer<Object> pathAnswer = ic -> {
String id = ic.getArgument(0);
return baseDirectory.toPath().resolve(id);
});
};
when(instanceMock.getLocation(anyString())).then(pathAnswer);
when(instanceMock.createLocation(anyString())).then(pathAnswer);
handler = createRepositoryHandler(storeFactory, locationResolver, baseDirectory);
}

View 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)}/>
);
}
}

View File

@@ -22,7 +22,6 @@ class Radio extends React.Component<Props> {
render() {
return (
<label className="radio" disabled={this.props.disabled}>
<input
type="radio"

View File

@@ -9,6 +9,7 @@ export { validation, urls, repositories };
export { default as DateFromNow } from "./DateFromNow.js";
export { default as ErrorNotification } from "./ErrorNotification.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 Loading } from "./Loading.js";
export { default as Logo } from "./Logo.js";

View File

@@ -1,5 +1,6 @@
//@flow
import * as React from "react";
import classNames from "classnames";
import {Link, Route} from "react-router-dom";
// TODO mostly copy of PrimaryNavigationLink
@@ -28,7 +29,7 @@ class NavLink extends React.Component<Props> {
let showIcon = null;
if (icon) {
showIcon = (<><i className={icon} />{" "}</>);
showIcon = (<><i className={classNames(icon, "fa-fw")} />{" "}</>);
}
return (

View File

@@ -1,6 +1,7 @@
//@flow
import * as React from "react";
import { Link, Route } from "react-router-dom";
import classNames from "classnames";
type Props = {
to: string,
@@ -37,7 +38,7 @@ class SubNavigation extends React.Component<Props> {
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={defaultIcon} /> {label}
<i className={classNames(defaultIcon, "fa-fw")} /> {label}
</Link>
{children}
</li>

View File

@@ -6,6 +6,7 @@
"lastModified": "Zuletzt bearbeitet",
"type": "Typ",
"external": "Extern",
"internal": "Intern",
"members": "Mitglieder"
},
"groups": {

View File

@@ -5,6 +5,7 @@
"mail": "E-Mail",
"password": "Passwort",
"active": "Aktiv",
"inactive": "Inaktiv",
"type": "Typ",
"creationDate": "Erstellt",
"lastModified": "Zuletzt bearbeitet"

View File

@@ -6,6 +6,7 @@
"lastModified": "Last Modified",
"type": "Type",
"external": "External",
"internal": "Internal",
"members": "Members"
},
"groups": {

View File

@@ -5,6 +5,7 @@
"mail": "E-Mail",
"password": "Password",
"active": "Active",
"inactive": "Inactive",
"type": "Type",
"creationDate": "Creation Date",
"lastModified": "Last Modified"

View File

@@ -123,7 +123,7 @@ class GroupForm extends React.Component<Props, State> {
);
};
isExistingGroup = () => !! this.props.group;
isExistingGroup = () => !!this.props.group;
render() {
const { loading, t } = this.props;

View File

@@ -1,29 +1,38 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Link } from "react-router-dom";
import type { Group } from "@scm-manager/ui-types";
import { Checkbox } from "@scm-manager/ui-components";
import { Icon } from "@scm-manager/ui-components";
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) {
return <Link to={to}>{label}</Link>;
}
render() {
const { group } = this.props;
const { group, t } = this.props;
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 (
<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>
<Checkbox checked={group.external} />
</td>
</tr>
);
}
}
export default translate("groups")(GroupRow);

View File

@@ -18,7 +18,6 @@ class GroupTable extends React.Component<Props> {
<tr>
<th>{t("group.name")}</th>
<th className="is-hidden-mobile">{t("group.description")}</th>
<th>{t("group.external")}</th>
</tr>
</thead>
<tbody>

View File

@@ -11,7 +11,7 @@ import {
} from "../modules/permissions";
import { connect } from "react-redux";
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 RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
@@ -49,9 +49,6 @@ type State = {
};
const styles = {
iconColor: {
color: "#9a9a9a"
},
centerMiddle: {
display: "table-cell",
verticalAlign: "middle !important"
@@ -148,15 +145,9 @@ class SinglePermission extends React.Component<Props, State> {
const iconType =
permission && permission.groupPermission ? (
<i
title={t("permission.group")}
className={classNames("fas fa-user-friends", classes.iconColor)}
/>
<Icon title={t("permission.group")} name="user-friends" />
) : (
<i
title={t("permission.user")}
className={classNames("fas fa-user", classes.iconColor)}
/>
<Icon title={t("permission.user")} name="user" />
);
return (
@@ -171,7 +162,7 @@ class SinglePermission extends React.Component<Props, State> {
action={this.handleDetailedPermissionsPressed}
/>
</td>
<td className={classes.centerMiddle}>
<td className={classNames("is-darker", classes.centerMiddle)}>
<DeletePermissionButton
permission={permission}
namespace={namespace}

View File

@@ -80,7 +80,6 @@ class UserForm extends React.Component<Props, State> {
return (
this.props.user.displayName === user.displayName &&
this.props.user.mail === user.mail &&
this.props.user.admin === user.admin &&
this.props.user.active === user.active
);
} else {

View File

@@ -1,31 +1,43 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Link } from "react-router-dom";
import type { User } from "@scm-manager/ui-types";
import { Icon } from "@scm-manager/ui-components";
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) {
return <Link to={to}>{label}</Link>;
}
render() {
const { user } = this.props;
const { user, t } = this.props;
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 (
<tr>
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
<td>{this.renderLink(to, user.displayName)}</td>
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
<td>{iconType} {this.renderLink(to, user.name)}</td>
<td className="is-hidden-mobile">
{this.renderLink(to, user.displayName)}
</td>
<td>
<a href={`mailto:${user.mail}`}>{user.mail}</a>
</td>
<td className="is-hidden-mobile">
<input type="checkbox" id="active" checked={user.active} readOnly />
</td>
</tr>
);
}
}
export default translate("users")(UserRow);

View File

@@ -19,7 +19,6 @@ class UserTable extends React.Component<Props> {
<th className="is-hidden-mobile">{t("user.name")}</th>
<th>{t("user.displayName")}</th>
<th>{t("user.mail")}</th>
<th className="is-hidden-mobile">{t("user.active")}</th>
</tr>
</thead>
<tbody>

View File

@@ -219,16 +219,23 @@ ul.is-separated {
// card tables
.card-table {
border-collapse: separate;
border-spacing: 0px 5px;
border-spacing: 0 5px;
tr {
a {
color: #363636;
}
&.border-is-green td:first-child {
border-left-color: $green;
}
&.border-is-yellow td:first-child {
border-left-color: $yellow;
}
&:hover {
td {
background-color: whitesmoke;
&:nth-child(4) {
&.is-darker {
background-color: #e1e1e1;
}
}
@@ -238,13 +245,14 @@ ul.is-separated {
}
}
td {
border-bottom: 1px solid whitesmoke;
background-color: #fafafa;
padding: 1em 1.25em;
background-color: #fafafa;
border-bottom: 1px solid whitesmoke;
&:first-child {
border-left: 3px solid $mint;
border-left: 3px solid $grey;
}
&:nth-child(4) {
&.is-darker {
background-color: whitesmoke;
}
}
@@ -318,6 +326,10 @@ form .field:not(.is-grouped) {
}
}
.is-icon {
color: $grey-light;
}
// label with help-icon compensation
.label-icon-spacing {
margin-top: 30px;

View File

@@ -58,6 +58,8 @@ import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -109,13 +111,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
@Override
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);
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
protected void withInjector(Injector injector) {
this.injector = injector;
@@ -183,6 +174,18 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
}
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() {
@@ -205,6 +208,4 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
private void destroyServletContextListeners(ServletContextEvent event) {
injector.getInstance(ServletContextListenerHolder.class).contextDestroyed(event);
}
}

View File

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

View File

@@ -37,8 +37,6 @@ import com.github.legman.Subscribe;
import com.google.inject.servlet.GuiceFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.Stage;
import sonia.scm.event.RecreateEventBusEvent;
import sonia.scm.event.ScmEventBus;
@@ -104,12 +102,9 @@ public class BootstrapContextFilter extends GuiceFilter
initGuice();
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)
{
logger.info("register for restart events");
ScmEventBus.getInstance().register(this);
}
}
public void initGuice() throws ServletException {
super.init(filterConfig);

View File

@@ -44,6 +44,7 @@ import sonia.scm.SCMContext;
import sonia.scm.ScmContextListener;
import sonia.scm.ScmEventBusModule;
import sonia.scm.ScmInitializerModule;
import sonia.scm.migration.UpdateException;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginException;
@@ -52,6 +53,7 @@ import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive;
import sonia.scm.update.MigrationWizardContextListener;
import sonia.scm.update.UpdateEngine;
import sonia.scm.util.ClassLoaders;
import sonia.scm.util.IOUtil;
@@ -59,13 +61,13 @@ import sonia.scm.util.IOUtil;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
@@ -108,18 +110,6 @@ public class BootstrapContextListener implements ServletContextListener {
public void contextDestroyed(ServletContextEvent 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;
contextListener = null;
}
@@ -134,35 +124,79 @@ public class BootstrapContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
context = sce.getServletContext();
File pluginDirectory = getPluginDirectory();
createContextListener(pluginDirectory);
createContextListener();
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 {
File pluginDirectory = getPluginDirectory();
renameOldPluginsFolder(pluginDirectory);
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory);
} else {
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) {
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) {
@@ -393,7 +427,7 @@ public class BootstrapContextListener implements ServletContextListener {
private ServletContext context;
/** Field description */
private ScmContextListener contextListener;
private ServletContextListener contextListener;
private static class ScmContextListenerModule extends AbstractModule {
@Override

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,11 @@
package sonia.scm.boot;
import javax.servlet.http.HttpServletRequest;
public interface ViewController {
String getTemplate();
View createView(HttpServletRequest request);
}

View File

@@ -25,7 +25,7 @@ import java.util.Set;
import static java.util.Collections.unmodifiableCollection;
import static java.util.stream.Collectors.toList;
class SystemRepositoryPermissionProvider {
public class SystemRepositoryPermissionProvider {
private static final Logger logger = LoggerFactory.getLogger(SystemRepositoryPermissionProvider.class);
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";

View File

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

View File

@@ -37,27 +37,22 @@ package sonia.scm.template;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheException;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Default;
import sonia.scm.plugin.PluginLoader;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.Reader;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import javax.servlet.ServletContext;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -67,6 +62,14 @@ import javax.servlet.ServletContext;
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 */
public static final TemplateType TYPE = new TemplateType("mustache",
"Mustache", "mustache");
@@ -87,13 +90,12 @@ public class MustacheTemplateEngine implements TemplateEngine
*
*
* @param context
* @param pluginLoader
* @param pluginLoaderHolder
*/
@Inject
public MustacheTemplateEngine(@Default ServletContext context,
PluginLoader pluginLoader)
public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder)
{
factory = new ServletMustacheFactory(context, pluginLoader);
factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader));
ThreadFactory threadFactory =
new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build();
@@ -101,6 +103,13 @@ public class MustacheTemplateEngine implements TemplateEngine
factory.setExecutorService(Executors.newCachedThreadPool(threadFactory));
}
private ClassLoader createClassLoader(PluginLoader pluginLoader) {
if (pluginLoader == null) {
return Thread.currentThread().getContextClassLoader();
}
return pluginLoader.getUberClassLoader();
}
//~--- get methods ----------------------------------------------------------
/**
@@ -112,12 +121,9 @@ public class MustacheTemplateEngine implements TemplateEngine
*
* @return
*
* @throws IOException
*/
@Override
public Template getTemplate(String templateIdentifier, Reader reader)
throws IOException
{
public Template getTemplate(String templateIdentifier, Reader reader) {
if (logger.isTraceEnabled())
{
logger.trace("try to create mustache template from reader with id {}",

View File

@@ -36,22 +36,17 @@ package sonia.scm.template;
//~--- non-JDK imports --------------------------------------------------------
import com.github.mustachejava.DefaultMustacheFactory;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.PluginLoader;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.ServletContext;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -73,13 +68,12 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
*
*
* @param servletContext
* @param pluginLoader
* @param classLoader
*/
public ServletMustacheFactory(ServletContext servletContext,
PluginLoader pluginLoader)
public ServletMustacheFactory(ServletContext servletContext, ClassLoader classLoader)
{
this.servletContext = servletContext;
this.pluginLoader = pluginLoader;
this.classLoader = classLoader;
}
//~--- get methods ----------------------------------------------------------
@@ -116,7 +110,7 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
resourceName = resourceName.substring(1);
}
is = pluginLoader.getUberClassLoader().getResourceAsStream(resourceName);
is = classLoader.getResourceAsStream(resourceName);
}
if (is != null)
@@ -144,9 +138,8 @@ public class ServletMustacheFactory extends DefaultMustacheFactory
//~--- fields ---------------------------------------------------------------
/** Field description */
private final PluginLoader pluginLoader;
/** Field description */
private ServletContext servletContext;
private ClassLoader classLoader;
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
@@ -7,9 +9,14 @@ import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.of;
class CopyMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(CopyMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
@@ -19,13 +26,14 @@ class CopyMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
LOG.info("copying repository data from {} to {}", sourceDataPath, targetDataPath);
copyData(sourceDataPath, targetDataPath);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void copyData(Path sourceDirectory, Path targetDirectory) {

View File

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

View File

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

View File

@@ -1,29 +1,47 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.of;
class InlineMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(InlineMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
public InlineMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@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);
locationResolver.forClass(Path.class).setLocation(id, repositoryBasePath);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
LOG.info("moving repository data from {} to {}", repositoryBasePath, targetDataPath);
moveData(repositoryBasePath, targetDataPath);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
moveData(sourceDirectory, targetDirectory, false);
}
private void moveData(Path sourceDirectory, Path targetDirectory, boolean deleteDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory)
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
@@ -31,11 +49,18 @@ class InlineMigrationStrategy extends BaseMigrationStrategy {
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
moveData(sourceFile, targetFile, true);
} else {
moveFile(sourceFile, targetFile);
}
}
);
if (deleteDirectory) {
try {
Files.delete(sourceDirectory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", sourceDirectory);
}
}
}
}

View File

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

View File

@@ -3,17 +3,41 @@ package sonia.scm.update.repository;
import com.google.inject.Injector;
import java.nio.file.Path;
import java.util.Optional;
enum MigrationStrategy {
public enum MigrationStrategy {
COPY(CopyMigrationStrategy.class),
MOVE(MoveMigrationStrategy.class),
INLINE(InlineMigrationStrategy.class);
COPY(CopyMigrationStrategy.class,
"Copy the repository data files to the new native location inside SCM-Manager home directory. " +
"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.description = description;
}
public Class<? extends Instance> getImplementationClass() {
return implementationClass;
}
public String getDescription() {
return description;
}
Instance from(Injector injector) {
@@ -21,6 +45,6 @@ enum MigrationStrategy {
}
interface Instance {
Path migrate(String id, String name, String type);
Optional<Path> migrate(String id, String name, String type);
}
}

View File

@@ -4,8 +4,10 @@ import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class MigrationStrategyDao {
private final RepositoryMigrationPlan plan;
@@ -17,12 +19,12 @@ public class MigrationStrategyDao {
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<MigrationStrategy> get(String id) {
public Optional<RepositoryMigrationPlan.RepositoryMigrationEntry> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, MigrationStrategy strategy) {
plan.set(repositoryId, strategy);
public void set(String repositoryId, MigrationStrategy strategy, String newNamespace, String newName) {
plan.set(repositoryId, strategy, newNamespace, newName);
store.set(plan);
}
}

View File

@@ -11,8 +11,10 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
import static java.util.Optional.of;
class MoveMigrationStrategy extends BaseMigrationStrategy {
@@ -27,14 +29,15 @@ class MoveMigrationStrategy extends BaseMigrationStrategy {
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
public Optional<Path> migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).createLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
LOG.info("moving repository data from {} to {}", sourceDataPath, targetDataPath);
moveData(sourceDataPath, targetDataPath);
deleteOldDataDir(getTypeDependentPath(type), name);
return repositoryBasePath;
return of(repositoryBasePath);
}
private void deleteOldDataDir(Path rootPath, String name) {

View File

@@ -13,57 +13,74 @@ import static java.util.Arrays.asList;
@XmlRootElement(name = "repository-migration")
class RepositoryMigrationPlan {
private List<RepositoryEntry> entries;
private List<RepositoryMigrationEntry> entries;
RepositoryMigrationPlan() {
this(new RepositoryEntry[0]);
this(new RepositoryMigrationEntry[0]);
}
RepositoryMigrationPlan(RepositoryEntry... entries) {
RepositoryMigrationPlan(RepositoryMigrationEntry... entries) {
this.entries = new ArrayList<>(asList(entries));
}
Optional<MigrationStrategy> get(String repositoryId) {
return findEntry(repositoryId)
.map(RepositoryEntry::getDataMigrationStrategy);
}
public void set(String repositoryId, MigrationStrategy strategy) {
Optional<RepositoryEntry> entry = findEntry(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
} else {
entries.add(new RepositoryEntry(repositoryId, strategy));
}
}
private Optional<RepositoryEntry> findEntry(String repositoryId) {
Optional<RepositoryMigrationEntry> get(String repositoryId) {
return entries.stream()
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
.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")
@XmlAccessorType(XmlAccessType.FIELD)
static class RepositoryEntry {
static class RepositoryMigrationEntry {
private String repositoryId;
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.dataMigrationStrategy = dataMigrationStrategy;
this.newNamespace = newNamespace;
this.newName = newName;
}
public MigrationStrategy getDataMigrationStrategy() {
return dataMigrationStrategy;
}
public String getNewNamespace() {
return newNamespace;
}
public String getNewName() {
return newName;
}
private void setStrategy(MigrationStrategy strategy) {
this.dataMigrationStrategy = strategy;
}
private void setNewNamespace(String newNamespace) {
this.newNamespace = newNamespace;
}
private void setNewName(String newName) {
this.newName = newName;
}
}
}

View File

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

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -6,6 +6,7 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
@@ -27,10 +28,12 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDAO;
@Inject
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider) {
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
}
@Override
@@ -41,6 +44,7 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
if (Files.exists(oldRepositoriesFile)) {
LOG.info("moving old repositories database files to repository-paths file");
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
repositoryDAO.refresh();
}
}

View File

@@ -28,11 +28,12 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@@ -102,13 +103,30 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
readV1Database(jaxbContext).ifPresent(
v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
v1Database.repositoryList.repositories.forEach(this::readMigrationEntry);
v1Database.repositoryList.repositories.forEach(this::update);
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() {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
@@ -122,61 +140,59 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
}
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(
v1Repository.id,
v1Repository.type,
getNamespace(v1Repository),
getName(v1Repository),
v1Repository.contact,
v1Repository.description,
v1Repository.getId(),
v1Repository.getType(),
repositoryMigrationEntry.getNewNamespace(),
repositoryMigrationEntry.getNewName(),
v1Repository.getContact(),
v1Repository.getDescription(),
createPermissions(v1Repository));
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
repositoryDao.add(repository, destination);
propertyStore.put(v1Repository.id, v1Repository.properties);
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.getName(), newPath);
repositoryDao.add(repository, newPath);
propertyStore.put(v1Repository.getId(), v1Repository.getProperties());
}
);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
private Optional<Path> handleDataDirectory(V1Repository v1Repository, MigrationStrategy dataMigrationStrategy) {
return dataMigrationStrategy
.from(injector)
.migrate(v1Repository.getId(), v1Repository.getName(), v1Repository.getType());
}
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
private RepositoryMigrationPlan.RepositoryMigrationEntry readMigrationEntry(V1Repository v1Repository) {
return findMigrationStrategy(v1Repository)
.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) {
if (v1Repository.permissions == null) {
if (v1Repository.getPermissions() == null) {
return new RepositoryPermission[0];
}
return v1Repository.permissions
return v1Repository.getPermissions()
.stream()
.map(this::createPermission)
.toArray(RepositoryPermission[]::new);
}
private RepositoryPermission createPermission(V1Permission v1Permission) {
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
}
private String getNamespace(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
}
private String getName(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
}
private String concatPathElements(String[] nameParts) {
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
}
private String[] getNameParts(String v1Name) {
return v1Name.split("/");
LOG.info("creating permission {} for {}", v1Permission.getType(), v1Permission.getName());
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission());
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
@@ -195,45 +211,6 @@ public class XmlRepositoryV1UpdateStep implements UpdateStep {
).toFile();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "permissions")
private static class V1Permission {
private boolean groupPermission;
private String name;
private String type;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class V1Repository {
private String contact;
private long creationDate;
private Long lastModified;
private String description;
private String id;
private String name;
private boolean isPublic;
private boolean archived;
private String type;
private List<V1Permission> permissions;
private V1Properties properties;
@Override
public String toString() {
return "V1Repository{" +
", contact='" + contact + '\'' +
", creationDate=" + creationDate +
", lastModified=" + lastModified +
", description='" + description + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", isPublic=" + isPublic +
", archived=" + archived +
", type='" + type + '\'' +
'}';
}
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;

View 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}}

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

View File

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

View File

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

View File

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

View File

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

View 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}}

View File

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

View File

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

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

View File

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

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

View File

@@ -35,16 +35,21 @@ package sonia.scm.template;
//~--- 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 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 ------------------------------------------------------------
import java.io.InputStream;
import javax.servlet.ServletContext;
/**
*
* @author Sebastian Sdorra
@@ -68,7 +73,10 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
when(loader.getUberClassLoader()).thenReturn(
Thread.currentThread().getContextClassLoader());
return new MustacheTemplateEngine(context, loader);
MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder();
holder.pluginLoader = loader;
return new MustacheTemplateEngine(context, holder);
}
//~--- get methods ----------------------------------------------------------
@@ -116,4 +124,18 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
return MustacheTemplateEngineTest.class.getResourceAsStream(
"/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!");
}
}

View File

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

View File

@@ -43,18 +43,18 @@ class CopyMigrationStrategyTest {
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
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"));
}
@Test
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")

View File

@@ -7,11 +7,14 @@ import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@@ -20,9 +23,14 @@ class InlineMigrationStrategyTest {
@Mock
SCMContextProvider contextProvider;
@Mock
PathBasedRepositoryLocationResolver locationResolver;
@Mock
RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance);
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@@ -33,13 +41,14 @@ class InlineMigrationStrategyTest {
@Test
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));
verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target);
}
@Test
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();
}

View File

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

View File

@@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.update.repository.MigrationStrategy;
import sonia.scm.update.repository.MigrationStrategyDao;
import javax.xml.bind.JAXBException;
import java.nio.file.Path;
@@ -37,23 +35,31 @@ class MigrationStrategyDaoTest {
}
@Test
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
void shouldReturnEmptyOptionalWhenStoreIsEmpty() {
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
void shouldReturnNewValue() throws JAXBException {
void shouldReturnNewValue() {
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
@@ -62,16 +68,24 @@ class MigrationStrategyDaoTest {
void initExistingDatabase() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
dao.set("id", INLINE);
dao.set("id", INLINE, "space", "name");
}
@Test
void shouldFindExistingValue() throws JAXBException {
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");
}
}
}

View File

@@ -3,10 +3,13 @@ package sonia.scm.update.repository;
import com.google.inject.Injector;
import sonia.scm.update.repository.MigrationStrategy.Instance;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static java.util.Optional.of;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -20,6 +23,13 @@ class MigrationStrategyMock {
.thenAnswer(
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;
}
}

View File

@@ -40,18 +40,18 @@ class MoveMigrationStrategyTest {
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
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"));
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")

View File

@@ -7,8 +7,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.net.URL;
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.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
class XmlRepositoryFileNameUpdateStepTest {
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
@@ -29,8 +31,8 @@ class XmlRepositoryFileNameUpdateStepTest {
}
@Test
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider);
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
@@ -40,5 +42,6 @@ class XmlRepositoryFileNameUpdateStepTest {
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
verify(repositoryDAO).refresh();
}
}

View File

@@ -11,6 +11,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.update.repository.MigrationStrategy.COPY;
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@@ -89,9 +89,14 @@ class XmlRepositoryV1UpdateStepTest {
@BeforeEach
void createMigrationPlan() {
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
Answer<Object> planAnswer = invocation -> {
String id = invocation.getArgument(0).toString();
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
@@ -104,56 +109,20 @@ class XmlRepositoryV1UpdateStepTest {
void shouldMapAttributes() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
Optional<Repository> repository = findByNamespace("namespace-3b91caa5-59c3-448f-920b-769aaa56b761");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("type", "git")
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
.hasFieldOrPropertyWithValue("description", "A simple repository without directories.");
}
@Test
void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "git")
.hasFieldOrPropertyWithValue("name", "simple");
}
@Test
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("one");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "one")
.hasFieldOrPropertyWithValue("name", "directory");
}
@Test
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("some");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "some")
.hasFieldOrPropertyWithValue("name", "more_directories_than_one");
.hasFieldOrPropertyWithValue("description", "A repository with two folders.");
}
@Test
void shouldMapPermissions() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
Optional<Repository> repository = findByNamespace("namespace-454972da-faf9-4437-b682-dc4a4e0aa8eb");
assertThat(repository.get().getPermissions())
.hasSize(3)
@@ -176,14 +145,27 @@ class XmlRepositoryV1UpdateStepTest {
@Test
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
Path targetDir = tempDir.resolve("someDir");
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class);
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir);
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(MoveMigrationStrategy.class);
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(of(targetDir));
updateStep.doUpdate();
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
void shouldFailForMissingMigrationStrategy() {
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();
}
@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 {
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");

View File

@@ -0,0 +1 @@
Resource for testing

View File

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