Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-06-11 13:55:43 +02:00
110 changed files with 7002 additions and 4053 deletions

14
pom.xml
View File

@@ -409,8 +409,9 @@
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.2.0</version>
<version>2.3.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
@@ -432,6 +433,12 @@
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
</plugin>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-4</version>
</plugin>
</plugins>
</pluginManagement>
@@ -838,8 +845,8 @@
<quartz.version>2.2.3</quartz.version>
<!-- frontend -->
<nodejs.version>8.11.4</nodejs.version>
<yarn.version>1.9.4</yarn.version>
<nodejs.version>10.16.0</nodejs.version>
<yarn.version>1.16.0</yarn.version>
<!-- build properties -->
<project.build.javaLevel>1.8</project.build.javaLevel>
@@ -855,7 +862,6 @@
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
<node.version>8.11.4</node.version>
<sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable>

View File

@@ -3,11 +3,80 @@ package sonia.scm.migration;
import sonia.scm.plugin.ExtensionPoint;
import sonia.scm.version.Version;
/**
* This is the main interface for data migration/update. Using this interface, SCM-Manager provides the possibility to
* change data structures between versions for a given type of data.
* <p>The data type can be an arbitrary string, but it is considered a best practice to use a qualified name, for
* example
* <ul>
* <li><code>com.example.myPlugin.configuration</code></li> for data in plugins, or
* <li><code>com.cloudogu.scm.repository</code></li> for core data structures.
* </ul>
* </p>
* <p>The version is unrelated to other versions and therefore can be chosen freely, so that a data type can be updated
* without in various ways independent of other data types or the official version of the plugin or the core.
* A coordination between different data types and their versions is only necessary, when update steps of different data
* types rely on each other. If a update step of data type <i>A</i> has to run <b>before</b> another step for data type
* <i>B</i>, the version number of the second step has to be greater in regards to {@link Version#compareTo(Version)}.
* </p>
* <p>The algorithm looks something like this:<br>
* Whenever the SCM-Manager starts,
* <ul>
* <li>it creates a so called <i>bootstrap guice context</i>, that contains
* <ul>
* <li>a {@link sonia.scm.security.KeyGenerator},</li>
* <li>the {@link sonia.scm.repository.RepositoryLocationResolver},</li>
* <li>the {@link sonia.scm.io.FileSystem},</li>
* <li>the {@link sonia.scm.security.CipherHandler},</li>
* <li>a {@link sonia.scm.store.ConfigurationStoreFactory},</li>
* <li>a {@link sonia.scm.store.ConfigurationEntryStoreFactory},</li>
* <li>a {@link sonia.scm.store.DataStoreFactory},</li>
* <li>a {@link sonia.scm.store.BlobStoreFactory}, and</li>
* <li>the {@link sonia.scm.plugin.PluginLoader}.</li>
* </ul>
* Mind, that there are no DAOs, Managers or the like available at this time!
* </li>
* <li>It then checks whether there are instances of this interface that have not run before, that is either
* <ul>
* <li>their version number given by {@link #getTargetVersion()} is bigger than the last recorded target version of an
* executed update step for the data type given by {@link #getAffectedDataType()}, or
* </li>
* <li>there is no version number known for the given data type.
* </li>
* </ul>
* These are the <i>relevant</i> update steps.
* </li>
* <li>These relevant update steps are then sorted ascending by their target version given by
* {@link #getTargetVersion()}.
* </li>
* <li>Finally, these sorted steps are executed one after another calling {@link #doUpdate()} of each step, updating the
* version for the data type accordingly.
* </li>
* <li>If all works well, SCM-Manager then creates the runtime guice context by loading all further modules.</li>
* <li>If any of the update steps fails, the whole process is interrupted and SCM-Manager will not start up and will
* not record the version number of this update step.
* </li>
* </ul>
* </p>
*/
@ExtensionPoint
public interface UpdateStep {
/**
* Implement this to update the data to the new version. If any {@link Exception} is thrown, SCM-Manager will not
* start up.
*/
void doUpdate() throws Exception;
/**
* Declares the new version of the data type given by {@link #getAffectedDataType()}. A update step will only be
* executed, when this version is bigger than the last recorded version for its data type according to
* {@link Version#compareTo(Version)}
*/
Version getTargetVersion();
/**
* Declares the data type this update step will take care of. This should be a qualified name, like
* <code>com.example.myPlugin.configuration</code>.
*/
String getAffectedDataType();
}

View File

@@ -59,9 +59,6 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
public static final String DEFAULT_VERSION_INFORMATION = "unknown";
public static final String DOT = ".";
static final String REPOSITORIES_NATIVE_DIRECTORY = "data";
/**
* the logger for AbstractSimpleRepositoryHandler
*/

View File

@@ -42,6 +42,8 @@ import java.io.File;
*/
public interface RepositoryDirectoryHandler extends RepositoryHandler {
String REPOSITORIES_NATIVE_DIRECTORY = "data";
/**
* Get the current directory of the repository for the given id.
* @return the current directory of the given repository

View File

@@ -57,7 +57,7 @@ import static java.util.Collections.unmodifiableSet;
* Custom role with specific permissions related to {@link Repository}.
* This object should be immutable, but could not be due to mapstruct.
*/
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"read", "modify"})
@StaticPermissions(value = "repositoryRole", permissions = {}, globalPermissions = {"write"})
@XmlRootElement(name = "roles")
@XmlAccessorType(XmlAccessType.FIELD)
public class RepositoryRole implements ModelObject, PermissionObject {
@@ -121,7 +121,10 @@ public class RepositoryRole implements ModelObject, PermissionObject {
* @return the hash code value for the {@link RepositoryRole}
*/
@Override
public int hashCode() {
public int hashCode()
{
// Normally we do not have a log of repository permissions having the same size of verbs, but different content.
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
return Objects.hashCode(name, verbs == null? -1: verbs.size());
}

View File

@@ -105,6 +105,24 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
this.mail = mail;
}
/**
* Constructs ...
*
*
* @param name
* @param displayName
* @param mail
*/
public User(String name, String displayName, String mail, String password, String type, boolean active)
{
this.name = name;
this.displayName = displayName;
this.mail = mail;
this.password = password;
this.type = type;
this.active = active;
}
//~--- methods --------------------------------------------------------------
/**

View File

@@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.store.StoreConstants;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -28,9 +29,10 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
*
* @since 2.0.0
*/
@Singleton
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
private static final String STORE_NAME = "repositories";
public static final String STORE_NAME = "repository-paths";
private final SCMContextProvider contextProvider;
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
@@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
}
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
super(Path.class);
this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
@@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
public void refresh() {
this.read();
}
}

View File

@@ -95,10 +95,17 @@ public class XmlRepositoryDAO implements RepositoryDAO {
@Override
public void add(Repository repository) {
add(repository, repositoryLocationResolver.create(repository.getId()));
}
public void add(Repository repository, Object location) {
if (!(location instanceof Path)) {
throw new IllegalArgumentException("can only handle locations of type " + Path.class.getName() + ", not of type " + location.getClass().getName());
}
Repository clone = repository.clone();
synchronized (this) {
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
Path repositoryPath = (Path) location;
try {
Path metadataPath = resolveDataPath(repositoryPath);
@@ -111,10 +118,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
byId.put(repository.getId(), clone);
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
}
}
@Override
public boolean contains(Repository repository) {
return byId.containsKey(repository.getId());
@@ -193,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

@@ -152,7 +152,7 @@ class PathBasedRepositoryLocationResolverTest {
}
private String getXmlFileContent() {
Path storePath = basePath.resolve("config").resolve("repositories.xml");
Path storePath = basePath.resolve("config").resolve("repository-paths.xml");
assertThat(storePath).isRegularFile();
return content(storePath);

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;
@@ -32,7 +30,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,9 +47,6 @@ class XmlRepositoryDAOTest {
@Mock
private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao;
@@ -268,22 +265,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 +284,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

@@ -112,7 +112,6 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-3</version>
<extensions>true</extensions>
</plugin>

View File

@@ -12,6 +12,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28"
"@scm-manager/ui-bundler": "^0.0.29"
}
}

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

@@ -117,6 +117,6 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
initRepository();
File path = repositoryHandler.getDirectory(repository.getId());
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28"
"@scm-manager/ui-bundler": "^0.0.29"
}
}

View File

@@ -96,6 +96,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
initRepository();
File path = repositoryHandler.getDirectory(repository.getId());
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28"
"@scm-manager/ui-bundler": "^0.0.29"
}
}

View File

@@ -112,6 +112,6 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
initRepository();
File path = repositoryHandler.getDirectory(repository.getId());
assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString()+File.separator+ RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
File repoDirectory = new File(baseDirectory, repository.getId());
repoPath = repoDirectory.toPath();
// when(repoDao.getPath(repository.getId())).thenReturn(repoPath);
return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
return new File(repoDirectory, RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
}
protected File baseDirectory;

View File

@@ -153,7 +153,12 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
*/
private void extract(File folder) throws IOException
{
URL url = Resources.getResource(getZippedRepositoryResource());
String zippedRepositoryResource = getZippedRepositoryResource();
extract(folder, zippedRepositoryResource);
}
public static void extract(File targetFolder, String zippedRepositoryResource) throws IOException {
URL url = Resources.getResource(zippedRepositoryResource);
ZipInputStream zip = null;
try
@@ -164,7 +169,7 @@ public abstract class ZippedRepositoryTestBase extends AbstractTestBase
while (entry != null)
{
File file = new File(folder, entry.getName());
File file = new File(targetFolder, entry.getName());
File parent = file.getParentFile();
if (!parent.exists())

View File

@@ -14,7 +14,7 @@
"eslint-fix": "eslint src --fix"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28",
"@scm-manager/ui-bundler": "^0.0.29",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",

View File

@@ -0,0 +1,47 @@
// @flow
import * as React from "react";
import { withRouter } from "react-router-dom";
import { withContextPath } from "./urls";
/**
* Adds anchor links to markdown headings.
*
* @see <a href="https://github.com/rexxars/react-markdown/issues/69">Headings are missing anchors / ids</a>
*/
type Props = {
children: React.Node,
level: number,
location: any
};
function flatten(text: string, child: any) {
return typeof child === "string"
? text + child
: React.Children.toArray(child.props.children).reduce(flatten, text);
}
/**
* Turns heading text into a anchor id
*
* @VisibleForTesting
*/
export function headingToAnchorId(heading: string) {
return heading.toLowerCase().replace(/\W/g, "-");
}
function MarkdownHeadingRenderer(props: Props) {
const children = React.Children.toArray(props.children);
const heading = children.reduce(flatten, "");
const anchorId = headingToAnchorId(heading);
const headingElement = React.createElement("h" + props.level, {}, props.children);
const href = withContextPath(props.location.pathname + "#" + anchorId);
return (
<a id={`${anchorId}`} className="anchor" href={href}>
{headingElement}
</a>
);
}
export default withRouter(MarkdownHeadingRenderer);

View File

@@ -0,0 +1,18 @@
// @flow
import React from "react";
import { headingToAnchorId } from "./MarkdownHeadingRenderer";
describe("headingToAnchorId tests", () => {
it("should lower case the text", () => {
expect(headingToAnchorId("Hello")).toBe("hello");
expect(headingToAnchorId("HeLlO")).toBe("hello");
expect(headingToAnchorId("HELLO")).toBe("hello");
});
it("should replace spaces with hyphen", () => {
expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff");
expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f");
});
});

View File

@@ -3,17 +3,49 @@ import React from "react";
import SyntaxHighlighter from "./SyntaxHighlighter";
import Markdown from "react-markdown/with-html";
import {binder} from "@scm-manager/ui-extensions";
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
import { withRouter } from "react-router-dom";
type Props = {
content: string,
renderContext?: Object,
renderers?: Object,
enableAnchorHeadings: boolean,
// context props
location: any
};
class MarkdownView extends React.Component<Props> {
static defaultProps = {
enableAnchorHeadings: false
};
contentRef: ?HTMLDivElement;
constructor(props: Props) {
super(props);
}
componentDidUpdate() {
// we have to use componentDidUpdate, because we have to wait until all
// children are rendered and componentDidMount is called before the
// markdown content was rendered.
const hash = this.props.location.hash;
if (this.contentRef && hash) {
// we query only child elements, to avoid strange scrolling with multiple
// markdown elements on one page.
const element = this.contentRef.querySelector(hash);
if (element && element.scrollIntoView) {
element.scrollIntoView();
}
}
}
render() {
const {content, renderers, renderContext} = this.props;
const {content, renderers, renderContext, enableAnchorHeadings} = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
@@ -26,11 +58,16 @@ class MarkdownView extends React.Component<Props> {
rendererList = {};
}
if (enableAnchorHeadings) {
rendererList.heading = MarkdownHeadingRenderer;
}
if (!rendererList.code){
rendererList.code = SyntaxHighlighter;
}
return (
<div ref={el => (this.contentRef = el)}>
<Markdown
className="content"
skipHtml={true}
@@ -38,8 +75,9 @@ class MarkdownView extends React.Component<Props> {
source={content}
renderers={rendererList}
/>
</div>
);
}
}
export default MarkdownView;
export default withRouter(MarkdownView);

View File

@@ -17,13 +17,25 @@ const styles = {
panel: {
fontSize: "1rem"
},
/* breaks into a second row
when buttons and title become too long */
level: {
flexWrap: "wrap"
},
titleHeader: {
display: "flex",
maxWidth: "100%",
cursor: "pointer"
},
title: {
marginLeft: ".25rem",
fontSize: "1rem"
},
/* align child to right */
buttonHeader: {
display: "flex",
marginLeft: "auto"
},
hunkDivider: {
margin: ".5rem 0"
},
@@ -143,14 +155,41 @@ class DiffFile extends React.Component<Props, State> {
return file.newPath;
};
hoverFileTitle = (file: any) => {
if (
file.oldPath !== file.newPath &&
(file.type === "copy" || file.type === "rename")
) {
return (
<>
{file.oldPath} > {file.newPath}
</>
);
} else if (file.type === "delete") {
return file.oldPath;
}
return file.newPath;
};
renderChangeTag = (file: any) => {
const { t } = this.props;
const { t, classes } = this.props;
const key = "diff.changes." + file.type;
let value = t(key);
if (key === value) {
value = file.type;
}
return <span className="tag is-info has-text-weight-normal">{value}</span>;
return (
<span
className={classNames(
"tag",
"is-info",
"has-text-weight-normal",
classes.changeType
)}
>
{value}
</span>
);
};
render() {
@@ -187,20 +226,21 @@ class DiffFile extends React.Component<Props, State> {
return (
<div className={classNames("panel", classes.panel)}>
<div className="panel-heading">
<div className="level">
<div className={classNames("level", classes.level)}>
<div
className={classNames("level-left", classes.titleHeader)}
onClick={this.toggleCollapse}
title={this.hoverFileTitle(file)}
>
<i className={icon} />
<span className={classes.title}>
<span
className={classNames("is-ellipsis-overflow", classes.title)}
>
{this.renderFileTitle(file)}
</span>
<span className={classes.changeType}>
{this.renderChangeTag(file)}
</span>
</div>
<div className="level-right">
<div className={classNames("level-right", classes.buttonHeader)}>
<Button action={this.toggleSideBySide} className="reduced-mobile">
<span className="icon is-small">
<i

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"check": "flow check"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28"
"@scm-manager/ui-bundler": "^0.0.29"
},
"browserify": {
"transform": [

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,7 @@
"pre-commit": "jest && flow && eslint src"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28",
"@scm-manager/ui-bundler": "^0.0.29",
"concat": "^1.0.3",
"copyfiles": "^2.0.0",
"enzyme": "^3.3.0",

View File

@@ -9,29 +9,35 @@
"repositoryRole": {
"navLink": "Berechtigungsrollen",
"title": "Berechtigungsrollen",
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Berechtigungsrollen Fehler",
"createSubtitle": "Berechtigungsrolle erstellen",
"editSubtitle": "Berechtigungsrolle bearbeiten",
"overview": {
"title": "Übersicht aller verfügbaren Berechtigungsrollen",
"noPermissionRoles": "Keine Berechtigungsrollen gefunden.",
"system": "System",
"createButton": "Berechtigungsrolle erstellen",
"createButton": "Berechtigungsrolle erstellen"
},
"editButton": "Bearbeiten",
"name": "Name",
"type": "Typ",
"verbs": "Verben",
"button": {
"edit": "Bearbeiten"
},
"create": {
"name": "Name"
},
"edit": "Berechtigungsrolle bearbeiten",
"verbs": "Berechtigungen",
"system": "System",
"form": {
"subtitle": "Berechtigungsrolle bearbeiten",
"name": "Name",
"permissions": "Berechtigungen",
"submit": "Speichern"
}
},
"role": {
"name": "Name",
"system": "System"
"deleteRole" : {
"button": "Löschen",
"subtitle": "Berechtigungsrolle löschen",
"confirmAlert": {
"title": "Berechtigungsrolle löschen",
"message": "Soll die Berechtigungsrolle wirklich gelöscht werden? Alle Nutzer dieser Rolle verlieren Ihre Berechtigungen.",
"submit": "Ja",
"cancel": "Nein"
}
},
"config-form": {
"submit": "Speichern",

View File

@@ -9,29 +9,35 @@
"repositoryRole": {
"navLink": "Permission Roles",
"title": "Permission Roles",
"errorTitle": "Error",
"errorSubtitle": "Unknown Permission Role Error",
"createSubtitle": "Create Permission Role",
"editSubtitle": "Edit Permission Role",
"overview": {
"title": "Overview of all permission roles",
"noPermissionRoles": "No permission roles found.",
"system": "System",
"createButton": "Create Permission Role",
"createButton": "Create Permission Role"
},
"editButton": "Edit",
"name": "Name",
"type": "Type",
"verbs": "Verbs",
"edit": "Edit Permission Role",
"button": {
"edit": "Edit"
},
"create": {
"name": "Name"
},
"verbs": "Permissions",
"system": "System",
"form": {
"subtitle": "Edit Permission Role",
"name": "Name",
"permissions": "Permissions",
"submit": "Save"
}
},
"role": {
"name": "Name",
"system": "System"
"deleteRole": {
"button": "Delete",
"subtitle": "Delete Permission Role",
"confirmAlert": {
"title": "Delete Permission Role",
"message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.",
"submit": "Yes",
"cancel": "No"
}
},
"config-form": {
"submit": "Submit",

View File

@@ -74,7 +74,6 @@ class Config extends React.Component<Props> {
path={`${url}/roles/create`}
render={() => (
<CreateRepositoryRole
disabled={false}
history={this.props.history}
/>
)}
@@ -104,6 +103,7 @@ class Config extends React.Component<Props> {
to={`${url}/roles/`}
label={t("repositoryRole.navLink")}
activeWhenMatch={this.matchesRoles}
activeOnlyWhenExact={false}
/>
<ExtensionPoint
name="config.navigation"

View File

@@ -1,13 +1,15 @@
//@flow
import React from "react";
import type { RepositoryRole } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { compose } from "redux";
import injectSheet from "react-jss";
import { translate } from "react-i18next";
import type { RepositoryRole } from "@scm-manager/ui-types";
type Props = {
role: RepositoryRole,
// context props
classes: any,
t: string => string
};

View File

@@ -2,9 +2,9 @@
import React from "react";
import { translate } from "react-i18next";
import type { RepositoryRole } from "@scm-manager/ui-types";
import ExtensionPoint from "@scm-manager/ui-extensions/lib/ExtensionPoint";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
import { Button, Subtitle } from "@scm-manager/ui-components";
import { Button } from "@scm-manager/ui-components";
type Props = {
role: RepositoryRole,
@@ -20,7 +20,7 @@ class PermissionRoleDetails extends React.Component<Props> {
if (!!this.props.role._links.update) {
return (
<Button
label={t("repositoryRole.button.edit")}
label={t("repositoryRole.editButton")}
link={`${url}/edit`}
color="primary"
/>
@@ -33,18 +33,16 @@ class PermissionRoleDetails extends React.Component<Props> {
const { role } = this.props;
return (
<div>
<>
<PermissionRoleDetailsTable role={role} />
<hr />
{this.renderEditButton()}
<div className="content">
<ExtensionPoint
name="repositoryRole.role-details.information"
renderAll={true}
props={{ role }}
/>
</div>
</div>
</>
);
}
}

View File

@@ -6,6 +6,7 @@ import AvailableVerbs from "./AvailableVerbs";
type Props = {
role: RepositoryRole,
// context props
t: string => string
};

View File

@@ -8,6 +8,7 @@ type Props = {
baseUrl: string,
roles: RepositoryRole[],
// context props
t: string => string
};
@@ -18,7 +19,7 @@ class PermissionRoleTable extends React.Component<Props> {
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("repositoryRole.form.name")}</th>
<th>{t("repositoryRole.name")}</th>
</tr>
</thead>
<tbody>

View File

@@ -6,6 +6,8 @@ import { translate } from "react-i18next";
type Props = {
system?: boolean,
// context props
classes: any,
t: string => string
};
@@ -24,7 +26,7 @@ class SystemRoleTag extends React.Component<Props> {
if (system) {
return (
<span className={classNames("tag is-dark", classes.tag)}>
{t("role.system")}
{t("repositoryRole.system")}
</span>
);
}

View File

@@ -3,7 +3,7 @@ import React from "react";
import RepositoryRoleForm from "./RepositoryRoleForm";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { ErrorNotification, Title } from "@scm-manager/ui-components";
import {ErrorNotification, Subtitle, Title} from "@scm-manager/ui-components";
import {
createRole,
getCreateRoleFailure,
@@ -15,11 +15,12 @@ import {
getRepositoryRolesLink,
getRepositoryVerbsLink
} from "../../../modules/indexResource";
import type {History} from "history";
type Props = {
disabled: boolean,
repositoryRolesLink: string,
error?: Error,
history: History,
//dispatch function
addRole: (link: string, role: RepositoryRole, callback?: () => void) => void,
@@ -50,8 +51,8 @@ class CreateRepositoryRole extends React.Component<Props> {
return (
<>
<Title title={t("repositoryRole.title")} />
<Subtitle subtitle={t("repositoryRole.createSubtitle")} />
<RepositoryRoleForm
disabled={this.props.disabled}
submitForm={role => this.createRepositoryRole(role)}
/>
</>

View File

@@ -0,0 +1,113 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { RepositoryRole } from "@scm-manager/ui-types";
import {
Subtitle,
DeleteButton,
confirmAlert,
ErrorNotification
} from "@scm-manager/ui-components";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
import {
deleteRole,
getDeleteRoleFailure,
isDeleteRolePending
} from "../modules/roles";
type Props = {
loading: boolean,
error: Error,
role: RepositoryRole,
confirmDialog?: boolean,
deleteRole: (role: RepositoryRole, callback?: () => void) => void,
// context props
history: History,
t: string => string
};
class DeleteRepositoryRole extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
roleDeleted = () => {
this.props.history.push("/config/roles/");
};
deleteRole = () => {
this.props.deleteRole(this.props.role, this.roleDeleted);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteRole.confirmAlert.title"),
message: t("deleteRole.confirmAlert.message"),
buttons: [
{
label: t("deleteRole.confirmAlert.submit"),
onClick: () => this.deleteRole()
},
{
label: t("deleteRole.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.role._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRole;
if (!this.isDeletable()) {
return null;
}
return (
<>
<Subtitle subtitle={t("deleteRole.subtitle")} />
<div className="columns">
<div className="column">
<ErrorNotification error={error} />
<DeleteButton
label={t("deleteRole.button")}
action={action}
loading={loading}
/>
</div>
</div>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isDeleteRolePending(state, ownProps.role.name);
const error = getDeleteRoleFailure(state, ownProps.role.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
deleteRole: (role: RepositoryRole, callback?: () => void) => {
dispatch(deleteRole(role, callback));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(translate("config")(DeleteRepositoryRole)));

View File

@@ -8,8 +8,10 @@ import {
isModifyRolePending,
modifyRole
} from "../modules/roles";
import { ErrorNotification } from "@scm-manager/ui-components";
import { ErrorNotification, Subtitle } from "@scm-manager/ui-components";
import type { RepositoryRole } from "@scm-manager/ui-types";
import type { History } from "history";
import DeleteRepositoryRole from "./DeleteRepositoryRole";
type Props = {
disabled: boolean,
@@ -17,26 +19,25 @@ type Props = {
repositoryRolesLink: string,
error?: Error,
// context objects
t: string => string,
history: History,
//dispatch function
updateRole: (
link: string,
role: RepositoryRole,
callback?: () => void
) => void
updateRole: (role: RepositoryRole, callback?: () => void) => void
};
class EditRepositoryRole extends React.Component<Props> {
repositoryRoleUpdated = (role: RepositoryRole) => {
const { history } = this.props;
history.push("/config/roles/");
repositoryRoleUpdated = () => {
this.props.history.push("/config/roles/");
};
updateRepositoryRole = (role: RepositoryRole) => {
this.props.updateRole(role, () => this.repositoryRoleUpdated(role));
this.props.updateRole(role, this.repositoryRoleUpdated);
};
render() {
const { error } = this.props;
const { error, t } = this.props;
if (error) {
return <ErrorNotification error={error} />;
@@ -44,18 +45,20 @@ class EditRepositoryRole extends React.Component<Props> {
return (
<>
<Subtitle subtitle={t("repositoryRole.editSubtitle")} />
<RepositoryRoleForm
nameDisabled={true}
role={this.props.role}
submitForm={role => this.updateRepositoryRole(role)}
/>
<hr/>
<DeleteRepositoryRole role={this.props.role}/>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isModifyRolePending(state);
const loading = isModifyRolePending(state, ownProps.role.name);
const error = getModifyRoleFailure(state, ownProps.role.name);
return {

View File

@@ -19,7 +19,6 @@ import {
type Props = {
role?: RepositoryRole,
loading?: boolean,
nameDisabled: boolean,
availableVerbs: string[],
verbsLink: string,
submitForm: RepositoryRole => void,
@@ -103,7 +102,7 @@ class RepositoryRoleForm extends React.Component<Props, State> {
};
render() {
const { loading, availableVerbs, nameDisabled, t } = this.props;
const { loading, availableVerbs, t } = this.props;
const { role } = this.state;
const verbSelectBoxes = !availableVerbs
@@ -119,28 +118,25 @@ class RepositoryRoleForm extends React.Component<Props, State> {
return (
<form onSubmit={this.submit}>
<div className="columns">
<div className="column">
<InputField
name="name"
label={t("repositoryRole.create.name")}
label={t("repositoryRole.form.name")}
onChange={this.handleNameChange}
value={role.name ? role.name : ""}
disabled={nameDisabled}
disabled={!!this.props.role}
/>
<div className="field">
<label className="label">
{t("repositoryRole.form.permissions")}
</label>
{verbSelectBoxes}
</div>
</div>
<>{verbSelectBoxes}</>
<hr />
<div className="columns">
<div className="column">
<SubmitButton
loading={loading}
label={t("repositoryRole.form.submit")}
disabled={!this.isValid()}
/>
</div>
</div>
</form>
);
}

View File

@@ -7,6 +7,7 @@ import type { History } from "history";
import type { RepositoryRole, PagedCollection } from "@scm-manager/ui-types";
import {
Title,
Subtitle,
Loading,
Notification,
LinkPaginator,
@@ -22,7 +23,8 @@ import {
getFetchRolesFailure
} from "../modules/roles";
import PermissionRoleTable from "../components/PermissionRoleTable";
import { getRolesLink } from "../../../modules/indexResource";
import { getRepositoryRolesLink } from "../../../modules/indexResource";
type Props = {
baseUrl: string,
roles: RepositoryRole[],
@@ -36,6 +38,7 @@ type Props = {
// context objects
t: string => string,
history: History,
location: any,
// dispatch functions
fetchRolesByPage: (link: string, page: number) => void
@@ -61,8 +64,7 @@ class RepositoryRoles extends React.Component<Props> {
if (page !== statePage || prevProps.location.search !== location.search) {
fetchRolesByPage(
rolesLink,
page,
urls.getQueryStringFromLocation(location)
page
);
}
}
@@ -76,11 +78,12 @@ class RepositoryRoles extends React.Component<Props> {
}
return (
<div>
<>
<Title title={t("repositoryRole.title")} />
<Subtitle subtitle={t("repositoryRole.overview.title")} />
{this.renderPermissionsTable()}
{this.renderCreateButton()}
</div>
</>
);
}
@@ -96,7 +99,7 @@ class RepositoryRoles extends React.Component<Props> {
}
return (
<Notification type="info">
{t("repositoryRole.noPermissionRoles")}
{t("repositoryRole.overview.noPermissionRoles")}
</Notification>
);
}
@@ -106,7 +109,7 @@ class RepositoryRoles extends React.Component<Props> {
if (canAddRoles) {
return (
<CreateButton
label={t("repositoryRole.createButton")}
label={t("repositoryRole.overview.createButton")}
link={`${baseUrl}/create`}
/>
);
@@ -123,7 +126,7 @@ const mapStateToProps = (state, ownProps) => {
const page = urls.getPageFromMatch(match);
const canAddRoles = isPermittedToCreateRoles(state);
const list = selectListAsCollection(state);
const rolesLink = getRolesLink(state);
const rolesLink = getRepositoryRolesLink(state);
return {
roles,

View File

@@ -81,8 +81,6 @@ class SingleRepositoryRole extends React.Component<Props> {
return (
<>
<Title title={t("repositoryRole.title")} />
<div className="columns">
<div className="column is-three-quarters">
<Route
path={`${url}/info`}
component={() => <PermissionRoleDetail role={role} url={url} />}
@@ -99,8 +97,6 @@ class SingleRepositoryRole extends React.Component<Props> {
props={extensionProps}
renderAll={true}
/>
</div>
</div>
</>
);
}

View File

@@ -39,7 +39,7 @@ export class DeleteGroup extends React.Component<Props> {
};
groupDeleted = () => {
this.props.history.push("/groups");
this.props.history.push("/groups/");
};
confirmDelete = () => {

View File

@@ -159,10 +159,6 @@ export function getSvnConfigLink(state: Object) {
return getLink(state, "svnConfig");
}
export function getRolesLink(state: Object) {
return getLink(state, "repositoryRoles");
}
export function getUserAutoCompleteLink(state: Object): string {
const link = getLinkCollection(state, "autocomplete").find(
i => i.name === "users"

View File

@@ -35,7 +35,7 @@ class DeleteRepo extends React.Component<Props> {
};
deleted = () => {
this.props.history.push("/repos");
this.props.history.push("/repos/");
};
deleteRepo = () => {

View File

@@ -161,7 +161,7 @@ class Permissions extends React.Component<Props> {
return (
<div>
<Subtitle subtitle={t("permission.title")} />
<table className="has-background-light table is-hoverable is-fullwidth">
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>

View File

@@ -35,7 +35,7 @@ class DeleteUser extends React.Component<Props> {
};
userDeleted = () => {
this.props.history.push("/users");
this.props.history.push("/users/");
};
deleteUser = () => {

View File

@@ -275,6 +275,14 @@ ul.is-separated {
.panel-block {
display: block;
border: none;
& .comment-wrapper:first-child div:first-child {
border-top: none;
}
& .diff-widget-content div {
border-bottom: none;
}
}
.panel-footer {

File diff suppressed because it is too large Load Diff

View File

@@ -461,7 +461,6 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-2</version>
<configuration>
<artifactItems>
<artifactItem>

View File

@@ -52,7 +52,6 @@ import sonia.scm.plugin.ExtensionProcessor;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.schedule.Scheduler;
import sonia.scm.upgrade.UpgradeManager;
import sonia.scm.user.UserManager;
import sonia.scm.util.IOUtil;
@@ -102,15 +101,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
}
private void beforeInjectorCreation() {
upgradeIfNecessary();
}
private void upgradeIfNecessary() {
if (!hasStartupErrors()) {
UpgradeManager upgradeHandler = new UpgradeManager();
upgradeHandler.doUpgrade();
}
}
private boolean hasStartupErrors() {
@@ -132,9 +122,6 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList
List<Module> moduleList = Lists.newArrayList();
moduleList.add(new ResteasyModule());
moduleList.add(new ScmInitializerModule());
moduleList.add(new ScmEventBusModule());
moduleList.add(new EagerSingletonModule());
moduleList.add(ShiroWebModule.guiceFilterModule());
moduleList.add(new WebElementModule(pluginLoader));
moduleList.add(new ScmServletModule(context, pluginLoader, overrides));

View File

@@ -53,8 +53,6 @@ import sonia.scm.group.GroupDisplayManager;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.net.SSLContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.ContentTransformer;
@@ -63,7 +61,6 @@ import sonia.scm.net.ahc.JsonContentTransformer;
import sonia.scm.net.ahc.XmlContentTransformer;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.DefaultPluginManager;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginManager;
import sonia.scm.repository.DefaultRepositoryManager;
import sonia.scm.repository.DefaultRepositoryProvider;
@@ -87,23 +84,11 @@ import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.AuthorizationChangedEventProducer;
import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.DefaultSecuritySystem;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.SecuritySystem;
import sonia.scm.store.BlobStoreFactory;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.FileBlobStoreFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.store.JAXBDataStoreFactory;
import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory;

View File

@@ -63,9 +63,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
if (RepositoryRolePermissions.read().isPermitted()) {
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
}
} else {
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
}

View File

@@ -7,12 +7,15 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PermissionListDto extends HalRepresentation {
@NotNull
private String[] permissions;
@Override

View File

@@ -25,7 +25,7 @@ public class RepositoryRoleCollectionToDtoMapper extends BasicCollectionToDtoMap
}
Optional<String> createCreateLink() {
return RepositoryRolePermissions.modify().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
return RepositoryRolePermissions.write().isPermitted() ? of(resourceLinks.repositoryRoleCollection().create()): empty();
}
String createSelfLink() {

View File

@@ -27,7 +27,7 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper
@ObjectFactory
RepositoryRoleDto createDto(RepositoryRole repositoryRole) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName()));
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) {
if (!"system".equals(repositoryRole.getType()) && RepositoryRolePermissions.write().isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName())));
linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName())));
}

View File

@@ -8,6 +8,7 @@ import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
@@ -69,7 +70,7 @@ public class UserPermissionResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
public Response overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) {
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
.map(PermissionDescriptor::new)
.collect(Collectors.toList());

View File

@@ -39,14 +39,18 @@ import com.google.inject.Module;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingletonModule;
import sonia.scm.SCMContext;
import sonia.scm.ScmContextListener;
import sonia.scm.ScmEventBusModule;
import sonia.scm.ScmInitializerModule;
import sonia.scm.Stage;
import sonia.scm.event.ScmEventBus;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.Plugin;
import sonia.scm.plugin.PluginException;
import sonia.scm.plugin.PluginLoadException;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.plugin.PluginsInternal;
import sonia.scm.plugin.SmpArchive;
@@ -134,6 +138,19 @@ public class BootstrapContextListener implements ServletContextListener {
File pluginDirectory = getPluginDirectory();
createContextListener(pluginDirectory);
contextListener.contextInitialized(sce);
// register for restart events
if (!registered && (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
logger.info("register for restart events");
ScmEventBus.getInstance().register(this);
registered = true;
}
}
private void createContextListener(File pluginDirectory) {
try {
if (!isCorePluginExtractionDisabled()) {
extractCorePlugins(context, pluginDirectory);
@@ -145,12 +162,9 @@ public class BootstrapContextListener implements ServletContextListener {
Set<PluginWrapper> plugins = PluginsInternal.collectPlugins(cl, pluginDirectory.toPath());
DefaultPluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
PluginLoader pluginLoader = new DefaultPluginLoader(context, cl, plugins);
Module scmContextListenerModule = new ScmContextListenerModule();
BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader);
Injector bootstrapInjector = Guice.createInjector(bootstrapModule, scmContextListenerModule);
Injector bootstrapInjector = createBootstrapInjector(pluginLoader);
processUpdates(pluginLoader, bootstrapInjector);
@@ -158,19 +172,25 @@ public class BootstrapContextListener implements ServletContextListener {
} catch (IOException ex) {
throw new PluginLoadException("could not load plugins", ex);
}
contextListener.contextInitialized(sce);
// register for restart events
if (!registered
&& (SCMContext.getContext().getStage() == Stage.DEVELOPMENT)) {
logger.info("register for restart events");
ScmEventBus.getInstance().register(this);
registered = true;
}
}
private void processUpdates(DefaultPluginLoader pluginLoader, Injector bootstrapInjector) {
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
Module scmContextListenerModule = new ScmContextListenerModule();
BootstrapModule bootstrapModule = new BootstrapModule(pluginLoader);
ScmInitializerModule scmInitializerModule = new ScmInitializerModule();
EagerSingletonModule eagerSingletonModule = new EagerSingletonModule();
ScmEventBusModule scmEventBusModule = new ScmEventBusModule();
return Guice.createInjector(
bootstrapModule,
scmContextListenerModule,
scmEventBusModule,
scmInitializerModule,
eagerSingletonModule
);
}
private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) {
Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader));
UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class);
@@ -390,7 +410,6 @@ public class BootstrapContextListener implements ServletContextListener {
private static class ScmContextListenerModule extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder().build(ScmContextListener.Factory.class));
}
}

View File

@@ -9,7 +9,6 @@ import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.plugin.DefaultPluginLoader;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
@@ -33,7 +32,7 @@ public class BootstrapModule extends AbstractModule {
private final ClassOverrides overrides;
private final PluginLoader pluginLoader;
BootstrapModule(DefaultPluginLoader pluginLoader) {
BootstrapModule(PluginLoader pluginLoader) {
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
this.pluginLoader = pluginLoader;
}

View File

@@ -88,7 +88,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
return managerDaoAdapter.create(
repositoryRole,
RepositoryRolePermissions::modify,
RepositoryRolePermissions::write,
newRepositoryRole -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepositoryRole),
newRepositoryRole -> fireEvent(HandlerEventType.CREATE, newRepositoryRole)
);
@@ -100,7 +100,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
logger.info("delete repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
managerDaoAdapter.delete(
repositoryRole,
RepositoryRolePermissions::modify,
RepositoryRolePermissions::write,
toDelete -> fireEvent(HandlerEventType.BEFORE_DELETE, toDelete),
toDelete -> fireEvent(HandlerEventType.DELETE, toDelete)
);
@@ -116,7 +116,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
logger.info("modify repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
managerDaoAdapter.modify(
repositoryRole,
x -> RepositoryRolePermissions.modify(),
x -> RepositoryRolePermissions.write(),
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, repositoryRole, notModified),
notModified -> fireEvent(HandlerEventType.MODIFY, repositoryRole, notModified));
}
@@ -125,7 +125,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
public void refresh(RepositoryRole repositoryRole) {
logger.info("refresh repositoryRole {} of type {}", repositoryRole.getName(), repositoryRole.getType());
RepositoryRolePermissions.read().check();
RepositoryRole fresh = repositoryRoleDAO.get(repositoryRole.getName());
if (fresh == null) {
@@ -135,8 +134,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
@Override
public RepositoryRole get(String id) {
RepositoryRolePermissions.read().check();
return findSystemRole(id).orElse(findCustomRole(id));
}
@@ -168,9 +165,6 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager
public List<RepositoryRole> getAll() {
List<RepositoryRole> repositoryRoles = new ArrayList<>();
if (!RepositoryRolePermissions.read().isPermitted()) {
return Collections.emptyList();
}
for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) {
repositoryRoles.add(repositoryRole.clone());
}

View File

@@ -0,0 +1,161 @@
package sonia.scm.update.group;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.group.Group;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlGroupV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlGroupV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlGroupDAO groupDAO;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlGroupV1UpdateStep(
SCMContextProvider contextProvider,
XmlGroupDAO groupDAO,
ConfigurationEntryStoreFactory configurationEntryStoreFactory
) {
this.contextProvider = contextProvider;
this.groupDAO = groupDAO;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("group-properties-v1")
.build();
}
@Override
public void doUpdate() throws JAXBException {
Optional<Path> v1GroupsFile = determineV1File();
if (!v1GroupsFile.isPresent()) {
LOG.info("no v1 file for groups found");
return;
}
XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get());
if (v1Database.groupList != null && v1Database.groupList.groups != null) {
v1Database.groupList.groups.forEach(this::update);
}
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.group.xml";
}
private void update(V1Group v1Group) {
LOG.debug("updating group {}", v1Group.name);
Group group = new Group(
v1Group.type,
v1Group.name,
v1Group.members);
group.setDescription(v1Group.description);
group.setCreationDate(v1Group.creationDate);
group.setLastModified(v1Group.lastModified);
groupDAO.add(group);
propertyStore.put(v1Group.name, v1Group.properties);
}
private XmlGroupV1UpdateStep.V1GroupDatabase readV1Database(Path v1GroupsFile) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlGroupV1UpdateStep.V1GroupDatabase.class);
return (XmlGroupV1UpdateStep.V1GroupDatabase) jaxbContext.createUnmarshaller().unmarshal(v1GroupsFile.toFile());
}
private Optional<Path> determineV1File() {
Path existingGroupsFile = resolveConfigFile("groups");
Path groupsV1File = resolveConfigFile("groupsV1");
if (existingGroupsFile.toFile().exists()) {
try {
Files.move(existingGroupsFile, groupsV1File);
} catch (IOException e) {
throw new UpdateException("could not move old groups file to " + groupsV1File.toAbsolutePath());
}
LOG.info("moved old groups file to {}", groupsV1File.toAbsolutePath());
return of(groupsV1File);
}
return empty();
}
private Path resolveConfigFile(String name) {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "group")
private static class V1Group {
private V1Properties properties;
private long creationDate;
private String description;
private Long lastModified;
private String name;
private String type;
@XmlElement(name = "members")
private List<String> members;
@Override
public String toString() {
return "V1Group{" +
"properties=" + properties +
", creationDate=" + creationDate + '\'' +
", description=" + description + '\'' +
", lastModified=" + lastModified + '\'' +
", name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
private static class GroupList {
@XmlElement(name = "group")
private List<V1Group> groups;
}
@XmlRootElement(name = "group-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1GroupDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "groups")
private XmlGroupV1UpdateStep.GroupList groupList;
}
}

View File

@@ -0,0 +1,14 @@
package sonia.scm.update.properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "properties")
public class V1Properties {
@XmlElement(name = "item")
private List<V1Property> properties;
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.update.properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class V1Property {
private String key;
private String value;
}

View File

@@ -0,0 +1,60 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Stream;
abstract class BaseMigrationStrategy implements MigrationStrategy.Instance {
private final SCMContextProvider contextProvider;
BaseMigrationStrategy(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
Path getSourceDataPath(String name, String type) {
return Arrays.stream(name.split("/"))
.reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1);
}
Path getTypeDependentPath(String type) {
return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type);
}
Stream<Path> listSourceDirectory(Path sourceDirectory) {
try {
return Files.list(sourceDirectory);
} catch (IOException e) {
throw new UpdateException("could not read original directory", e);
}
}
void createDataDirectory(Path target) {
try {
Files.createDirectories(target);
} catch (IOException e) {
throw new UpdateException("could not create data directory " + target, e);
}
}
void moveFile(Path sourceFile, Path targetFile) {
try {
Files.move(sourceFile, targetFile);
} catch (IOException e) {
throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e);
}
}
void copyFile(Path sourceFile, Path targetFile) {
try {
Files.copy(sourceFile, targetFile);
} catch (IOException e) {
throw new UpdateException("could not copy original file from " + sourceFile + " to " + targetFile, e);
}
}
}

View File

@@ -0,0 +1,44 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
class CopyMigrationStrategy extends BaseMigrationStrategy {
private final RepositoryLocationResolver locationResolver;
@Inject
public CopyMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
copyData(sourceDataPath, targetDataPath);
return repositoryBasePath;
}
private void copyData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory).forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
copyData(sourceFile, targetFile);
} else {
copyFile(sourceFile, targetFile);
}
}
);
}
}

View File

@@ -0,0 +1,41 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
class InlineMigrationStrategy extends BaseMigrationStrategy {
@Inject
public InlineMigrationStrategy(SCMContextProvider contextProvider) {
super(contextProvider);
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = getSourceDataPath(name, type);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
moveData(repositoryBasePath, targetDataPath);
return repositoryBasePath;
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory)
.filter(sourceFile -> !targetDirectory.equals(sourceFile))
.forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
} else {
moveFile(sourceFile, targetFile);
}
}
);
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.repository.update;
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -0,0 +1,26 @@
package sonia.scm.update.repository;
import com.google.inject.Injector;
import java.nio.file.Path;
enum MigrationStrategy {
COPY(CopyMigrationStrategy.class),
MOVE(MoveMigrationStrategy.class),
INLINE(InlineMigrationStrategy.class);
private Class<? extends Instance> implementationClass;
MigrationStrategy(Class<? extends Instance> implementationClass) {
this.implementationClass = implementationClass;
}
Instance from(Injector injector) {
return injector.getInstance(implementationClass);
}
interface Instance {
Path migrate(String id, String name, String type);
}
}

View File

@@ -0,0 +1,28 @@
package sonia.scm.update.repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import java.util.Optional;
public class MigrationStrategyDao {
private final RepositoryMigrationPlan plan;
private final ConfigurationStore<RepositoryMigrationPlan> store;
@Inject
public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) {
store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build();
this.plan = store.getOptional().orElse(new RepositoryMigrationPlan());
}
public Optional<MigrationStrategy> get(String id) {
return plan.get(id);
}
public void set(String repositoryId, MigrationStrategy strategy) {
plan.set(repositoryId, strategy);
store.set(plan);
}
}

View File

@@ -0,0 +1,75 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryDirectoryHandler;
import sonia.scm.repository.RepositoryLocationResolver;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static java.util.Arrays.asList;
class MoveMigrationStrategy extends BaseMigrationStrategy {
private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class);
private final RepositoryLocationResolver locationResolver;
@Inject
public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
super(contextProvider);
this.locationResolver = locationResolver;
}
@Override
public Path migrate(String id, String name, String type) {
Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id);
Path targetDataPath = repositoryBasePath
.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
Path sourceDataPath = getSourceDataPath(name, type);
moveData(sourceDataPath, targetDataPath);
deleteOldDataDir(getTypeDependentPath(type), name);
return repositoryBasePath;
}
private void deleteOldDataDir(Path rootPath, String name) {
delete(rootPath, asList(name.split("/")));
}
private void delete(Path rootPath, List<String> directories) {
if (directories.isEmpty()) {
return;
}
Path directory = rootPath.resolve(directories.get(0));
delete(directory, directories.subList(1, directories.size()));
try {
Files.deleteIfExists(directory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", directory);
}
}
private void moveData(Path sourceDirectory, Path targetDirectory) {
createDataDirectory(targetDirectory);
listSourceDirectory(sourceDirectory).forEach(
sourceFile -> {
Path targetFile = targetDirectory.resolve(sourceFile.getFileName());
if (Files.isDirectory(sourceFile)) {
moveData(sourceFile, targetFile);
} else {
moveFile(sourceFile, targetFile);
}
}
);
try {
Files.delete(sourceDirectory);
} catch (IOException e) {
LOG.warn("could not delete source repository directory {}", sourceDirectory);
}
}
}

View File

@@ -0,0 +1,69 @@
package sonia.scm.update.repository;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repository-migration")
class RepositoryMigrationPlan {
private List<RepositoryEntry> entries;
RepositoryMigrationPlan() {
this(new RepositoryEntry[0]);
}
RepositoryMigrationPlan(RepositoryEntry... entries) {
this.entries = new ArrayList<>(asList(entries));
}
Optional<MigrationStrategy> get(String repositoryId) {
return findEntry(repositoryId)
.map(RepositoryEntry::getDataMigrationStrategy);
}
public void set(String repositoryId, MigrationStrategy strategy) {
Optional<RepositoryEntry> entry = findEntry(repositoryId);
if (entry.isPresent()) {
entry.get().setStrategy(strategy);
} else {
entries.add(new RepositoryEntry(repositoryId, strategy));
}
}
private Optional<RepositoryEntry> findEntry(String repositoryId) {
return entries.stream()
.filter(repositoryEntry -> repositoryId.equals(repositoryEntry.repositoryId))
.findFirst();
}
@XmlRootElement(name = "entries")
@XmlAccessorType(XmlAccessType.FIELD)
static class RepositoryEntry {
private String repositoryId;
private MigrationStrategy dataMigrationStrategy;
RepositoryEntry() {
}
RepositoryEntry(String repositoryId, MigrationStrategy dataMigrationStrategy) {
this.repositoryId = repositoryId;
this.dataMigrationStrategy = dataMigrationStrategy;
}
public MigrationStrategy getDataMigrationStrategy() {
return dataMigrationStrategy;
}
private void setStrategy(MigrationStrategy strategy) {
this.dataMigrationStrategy = strategy;
}
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.repository.update;
package sonia.scm.update.repository;
public class RepositoryUpdates {

View File

@@ -0,0 +1,60 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static sonia.scm.version.Version.parse;
/**
* Moves an existing <code>repositories.xml</code> file to <code>repository-paths.xml</code>.
* Note that this has to run <em>after</em> an old v1 repository database has been migrated to v2
* (see {@link XmlRepositoryV1UpdateStep}).
*/
@Extension
public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDAO;
@Inject
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
}
@Override
public void doUpdate() throws IOException {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
Path newRepositoryPathsFile = configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + StoreConstants.FILE_EXTENSION);
if (Files.exists(oldRepositoriesFile)) {
LOG.info("moving old repositories database files to repository-paths file");
Files.move(oldRepositoriesFile, newRepositoryPathsFile);
repositoryDAO.refresh();
}
}
@Override
public Version getTargetVersion() {
return parse("2.0.1");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
}

View File

@@ -0,0 +1,250 @@
package sonia.scm.update.repository;
import com.google.inject.Injector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
/**
* Migrates SCM-Manager v1 repository data structure to SCM-Manager v2 data structure.
* That is:
* <ul>
* <li>The old <code>repositories.xml</code> file is read</li>
* <li>For each repository in this database,
* <ul>
* <li>a new entry in the new <code>repository-paths.xml</code> database is written,</li>
* <li>the data directory is moved or copied to a SCM v2 consistent directory. How this is done
* can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in
* a database file named <code>migration-plan.xml</code></li> (to create this file, use {@link MigrationStrategyDao}),
* and
* <li>the new <code>metadata.xml</code> file is created.</li>
* </ul>
* </li>
* </ul>
*/
@Extension
public class XmlRepositoryV1UpdateStep implements UpdateStep {
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDao;
private final MigrationStrategyDao migrationStrategyDao;
private final Injector injector;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlRepositoryV1UpdateStep(
SCMContextProvider contextProvider,
XmlRepositoryDAO repositoryDao,
MigrationStrategyDao migrationStrategyDao,
Injector injector,
ConfigurationEntryStoreFactory configurationEntryStoreFactory
) {
this.contextProvider = contextProvider;
this.repositoryDao = repositoryDao;
this.migrationStrategyDao = migrationStrategyDao;
this.injector = injector;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("repository-properties-v1")
.build();
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
@Override
public void doUpdate() throws JAXBException {
if (!resolveV1File().exists()) {
LOG.info("no v1 repositories database file found");
return;
}
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
readV1Database(jaxbContext).ifPresent(
v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationStrategy);
v1Database.repositoryList.repositories.forEach(this::update);
backupOldRepositoriesFile();
}
);
}
private void backupOldRepositoriesFile() {
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Path oldRepositoriesFile = configDir.resolve("repositories.xml");
Path backupFile = configDir.resolve("repositories.xml.v1.backup");
LOG.info("moving old repositories database files to backup file {}", backupFile);
try {
Files.move(oldRepositoriesFile, backupFile);
} catch (IOException e) {
throw new UpdateException("could not backup old repository database file", e);
}
}
private void update(V1Repository v1Repository) {
Path destination = handleDataDirectory(v1Repository);
Repository repository = new Repository(
v1Repository.id,
v1Repository.type,
getNamespace(v1Repository),
getName(v1Repository),
v1Repository.contact,
v1Repository.description,
createPermissions(v1Repository));
LOG.info("creating new repository {} with id {} from old repository {} in directory {}", repository.getNamespaceAndName(), repository.getId(), v1Repository.name, destination);
repositoryDao.add(repository, destination);
propertyStore.put(v1Repository.id, v1Repository.properties);
}
private Path handleDataDirectory(V1Repository v1Repository) {
MigrationStrategy dataMigrationStrategy = readMigrationStrategy(v1Repository);
return dataMigrationStrategy.from(injector).migrate(v1Repository.id, v1Repository.name, v1Repository.type);
}
private MigrationStrategy readMigrationStrategy(V1Repository v1Repository) {
return migrationStrategyDao.get(v1Repository.id)
.orElseThrow(() -> new IllegalStateException("no strategy found for repository with id " + v1Repository.id + " and name " + v1Repository.name));
}
private RepositoryPermission[] createPermissions(V1Repository v1Repository) {
if (v1Repository.permissions == null) {
return new RepositoryPermission[0];
}
return v1Repository.permissions
.stream()
.map(this::createPermission)
.toArray(RepositoryPermission[]::new);
}
private RepositoryPermission createPermission(V1Permission v1Permission) {
LOG.info("creating permission {} for {}", v1Permission.type, v1Permission.name);
return new RepositoryPermission(v1Permission.name, v1Permission.type, v1Permission.groupPermission);
}
private String getNamespace(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length > 1 ? nameParts[0] : v1Repository.type;
}
private String getName(V1Repository v1Repository) {
String[] nameParts = getNameParts(v1Repository.name);
return nameParts.length == 1 ? nameParts[0] : concatPathElements(nameParts);
}
private String concatPathElements(String[] nameParts) {
return Arrays.stream(nameParts).skip(1).collect(Collectors.joining("_"));
}
private String[] getNameParts(String v1Name) {
return v1Name.split("/");
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File());
if (unmarshal instanceof V1RepositoryDatabase) {
return of((V1RepositoryDatabase) unmarshal);
} else {
return empty();
}
}
private File resolveV1File() {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION)
).toFile();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "permissions")
private static class V1Permission {
private boolean groupPermission;
private String name;
private String type;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
private static class V1Repository {
private String contact;
private long creationDate;
private Long lastModified;
private String description;
private String id;
private String name;
private boolean isPublic;
private boolean archived;
private String type;
private List<V1Permission> permissions;
private V1Properties properties;
@Override
public String toString() {
return "V1Repository{" +
", contact='" + contact + '\'' +
", creationDate=" + creationDate +
", lastModified=" + lastModified +
", description='" + description + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", isPublic=" + isPublic +
", archived=" + archived +
", type='" + type + '\'' +
'}';
}
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;
}
@XmlRootElement(name = "repository-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1RepositoryDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "repositories")
private RepositoryList repositoryList;
}
}

View File

@@ -0,0 +1,105 @@
package sonia.scm.update.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AssignedPermission;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.function.Consumer;
import static java.util.Optional.ofNullable;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlSecurityV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
@Inject
public XmlSecurityV1UpdateStep(SCMContextProvider contextProvider, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
this.contextProvider = contextProvider;
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
}
@Override
public void doUpdate() throws JAXBException {
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
forAllAdmins(user -> createSecurityEntry(user, false, securityStore),
group -> createSecurityEntry(group, true, securityStore));
}
private void forAllAdmins(Consumer<String> userConsumer, Consumer<String> groupConsumer) throws JAXBException {
Path configDirectory = determineConfigDirectory();
Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION);
if (existingConfigFile.toFile().exists()) {
forAllAdmins(existingConfigFile, userConsumer, groupConsumer);
}
}
private void forAllAdmins(
Path existingConfigFile, Consumer<String> userConsumer, Consumer<String> groupConsumer
) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Configuration.class);
V1Configuration v1Configuration = (V1Configuration) jaxbContext.createUnmarshaller().unmarshal(existingConfigFile.toFile());
ofNullable(v1Configuration.adminUsers).ifPresent(users -> forAll(users, userConsumer));
ofNullable(v1Configuration.adminGroups).ifPresent(groups -> forAll(groups, groupConsumer));
}
private void forAll(String entries, Consumer<String> consumer) {
Arrays.stream(entries.split(",")).forEach(consumer);
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.security.xml";
}
private void createSecurityEntry(String name, boolean group, ConfigurationEntryStore<AssignedPermission> securityStore) {
LOG.debug("setting admin permissions for {} {}", group? "group": "user", name);
securityStore.put(new AssignedPermission(name, group, "*"));
}
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
}
private Path determineConfigDirectory() {
return new File(contextProvider.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME).toPath();
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "scm-config")
private static class V1Configuration {
@XmlElement(name = "admin-users")
private String adminUsers;
@XmlElement(name = "admin-groups")
private String adminGroups;
}
}

View File

@@ -0,0 +1,175 @@
package sonia.scm.update.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AssignedPermission;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.update.properties.V1Properties;
import sonia.scm.user.User;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.version.Version.parse;
@Extension
public class XmlUserV1UpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlUserV1UpdateStep.class);
private final SCMContextProvider contextProvider;
private final XmlUserDAO userDAO;
private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;
private final ConfigurationEntryStore<V1Properties> propertyStore;
@Inject
public XmlUserV1UpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
this.contextProvider = contextProvider;
this.userDAO = userDAO;
this.configurationEntryStoreFactory = configurationEntryStoreFactory;
this.propertyStore = configurationEntryStoreFactory
.withType(V1Properties.class)
.withName("user-properties-v1")
.build();
}
@Override
public void doUpdate() throws JAXBException {
Optional<Path> v1UsersFile = determineV1File();
if (!v1UsersFile.isPresent()) {
LOG.info("no v1 file for users found");
return;
}
XmlUserV1UpdateStep.V1UserDatabase v1Database = readV1Database(v1UsersFile.get());
ConfigurationEntryStore<AssignedPermission> securityStore = createSecurityStore();
v1Database.userList.users.forEach(user -> update(user, securityStore));
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.user.xml";
}
private void update(V1User v1User, ConfigurationEntryStore<AssignedPermission> securityStore) {
LOG.debug("updating user {}", v1User.name);
User user = new User(
v1User.name,
v1User.displayName,
v1User.mail,
v1User.password,
v1User.type,
v1User.active);
user.setCreationDate(v1User.creationDate);
user.setLastModified(v1User.lastModified);
userDAO.add(user);
if (v1User.admin) {
LOG.debug("setting admin permissions for user {}", v1User.name);
securityStore.put(new AssignedPermission(v1User.name, "*"));
}
propertyStore.put(v1User.name, v1User.properties);
}
private XmlUserV1UpdateStep.V1UserDatabase readV1Database(Path v1UsersFile) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlUserV1UpdateStep.V1UserDatabase.class);
return (XmlUserV1UpdateStep.V1UserDatabase) jaxbContext.createUnmarshaller().unmarshal(v1UsersFile.toFile());
}
private ConfigurationEntryStore<AssignedPermission> createSecurityStore() {
return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build();
}
private Optional<Path> determineV1File() {
Path existingUsersFile = resolveConfigFile("users");
Path usersV1File = resolveConfigFile("usersV1");
if (existingUsersFile.toFile().exists()) {
try {
Files.move(existingUsersFile, usersV1File);
} catch (IOException e) {
throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath());
}
LOG.info("moved old users file to {}", usersV1File.toAbsolutePath());
return of(usersV1File);
}
return empty();
}
private Path resolveConfigFile(String name) {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(name + StoreConstants.FILE_EXTENSION)
);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "user")
private static class V1User {
private V1Properties properties;
private boolean admin;
private long creationDate;
private String displayName;
private Long lastModified;
private String mail;
private String name;
private String password;
private String type;
private boolean active;
@Override
public String toString() {
return "V1User{" +
"properties=" + properties +
", admin='" + admin + '\'' +
", creationDate=" + creationDate + '\'' +
", displayName=" + displayName + '\'' +
", lastModified=" + lastModified + '\'' +
", mail='" + mail + '\'' +
", name='" + name + '\'' +
", type='" + type + '\'' +
", active='" + active + '\'' +
'}';
}
}
private static class UserList {
@XmlElement(name = "user")
private List<V1User> users;
}
@XmlRootElement(name = "user-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1UserDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "users")
private XmlUserV1UpdateStep.UserList userList;
}
}

View File

@@ -1,148 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.upgrade;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author Sebastian Sdorra
*/
public final class ClientDateFormatConverter
{
/** Field description */
private static final String SINGLECHAR_REGEX = "(^|[^%s])[%s]($|[^%s])";
/**
* the logger for DateFormatConverter
*/
private static final Logger logger =
LoggerFactory.getLogger(ClientDateFormatConverter.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private ClientDateFormatConverter() {}
//~--- methods --------------------------------------------------------------
/**
* Documentations:
* - Extjs: http://trac.geoext.org/browser/ext/3.4.0/docs/source/Date.html
* - Moments: http://momentjs.com/docs/#/displaying/format
*
*
* @param value
*
* @return
*/
public static String extjsToMoments(String value)
{
logger.trace(
"try to convert extjs date format \"{}\" to moments date format", value);
String result = replaceDateFormatChars(value, "d", "DD");
result = replaceDateFormatChars(result, "D", "ddd");
result = replaceDateFormatChars(result, "j", "D");
result = replaceDateFormatChars(result, "l", "dddd");
// no replacement found for 1-7, only 0-6 found
result = replaceDateFormatChars(result, "N", "d");
result = replaceDateFormatChars(result, "w", "d");
result = replaceDateFormatChars(result, "z", "DDDD");
result = replaceDateFormatChars(result, "W", "ww");
result = replaceDateFormatChars(result, "M", "MMM");
result = replaceDateFormatChars(result, "F", "MMMM");
result = replaceDateFormatChars(result, "m", "MM");
result = replaceDateFormatChars(result, "n", "M");
result = replaceDateFormatChars(result, "Y", "YYYY");
result = replaceDateFormatChars(result, "o", "YYYY");
result = replaceDateFormatChars(result, "y", "YY");
result = replaceDateFormatChars(result, "H", "HH");
result = replaceDateFormatChars(result, "h", "hh");
result = replaceDateFormatChars(result, "g", "h");
result = replaceDateFormatChars(result, "G", "H");
result = replaceDateFormatChars(result, "i", "mm");
result = replaceDateFormatChars(result, "s", "ss");
result = replaceDateFormatChars(result, "O", "ZZ");
result = replaceDateFormatChars(result, "P", "Z");
result = replaceDateFormatChars(result, "T", "z");
logger.debug(
"converted extjs date format \"{}\" to moments date format \"{}\"",
value, result);
return result;
}
/**
* Method description
*
*
* @param value
* @param c
* @param replacement
*
* @return
*/
private static String replaceDateFormatChars(String value, String c,
String replacement)
{
Pattern p = Pattern.compile(String.format(SINGLECHAR_REGEX, c, c, c));
StringBuffer buffer = new StringBuffer();
Matcher m = p.matcher(value);
while (m.find())
{
m.appendReplacement(buffer, "$1" + replacement + "$2");
}
m.appendTail(buffer);
return buffer.toString();
}
}

View File

@@ -1,159 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.upgrade;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import sonia.scm.SCMContext;
import sonia.scm.version.Version;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
/**
*
* @author Sebastian Sdorra
*/
public class ClientDateFormatUpgradeHandler extends XmlUpgradeHandler
{
/**
* the logger for ClientDateFormatUpgradeHandler
*/
private static final Logger logger =
LoggerFactory.getLogger(ClientDateFormatUpgradeHandler.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param homeDirectory
* @param configDirectory
* @param oldVersion
* @param newVersion
*/
@Override
public void doUpgrade(File homeDirectory, File configDirectory,
Version oldVersion, Version newVersion)
{
if (oldVersion.isOlder("1.23"))
{
if (logger.isInfoEnabled())
{
logger.info("data format is older than 1.23, upgrade to version {}",
SCMContext.getContext().getVersion());
}
updateClientDateFormat(homeDirectory, configDirectory);
}
}
/**
* Method description
*
*
* @param document
*/
private void fixClientDateFormat(Document document)
{
NodeList nodes = document.getElementsByTagName("dateFormat");
if (nodes != null)
{
for (int i = 0; i < nodes.getLength(); i++)
{
Node node = nodes.item(i);
String value = node.getTextContent();
value = ClientDateFormatConverter.extjsToMoments(value);
node.setTextContent(value);
}
}
}
/**
* Method description
*
*
* @param baseDirectory
* @param configDirectory
*
* @throws IOException
*/
private void updateClientDateFormat(File baseDirectory, File configDirectory)
{
File configFile = new File(configDirectory, "config.xml");
if (configFile.exists())
{
try
{
// backup config.xml
File backupDirectory = createBackupDirectory(baseDirectory,
"upgrade to version {0}");
Files.copy(configFile, new File(backupDirectory, "config.xml"));
// change dateformat
DocumentBuilder builder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = builder.parse(configFile);
fixClientDateFormat(document);
writeDocument(document, configFile);
}
catch (Exception ex)
{
logger.error("could not parse document", ex);
}
}
}
}

View File

@@ -1,227 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.upgrade;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import sonia.scm.SCMContext;
import sonia.scm.version.Version;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
/**
*
* @author Sebastian Sdorra
*/
public class TimestampUpgradeHandler extends XmlUpgradeHandler
{
/**
* the logger for TimestampUpgradeHandler
*/
private static final Logger logger =
LoggerFactory.getLogger(TimestampUpgradeHandler.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param homeDirectory
* @param configDirectory
* @param oldVersion
* @param newVersion
*/
@Override
public void doUpgrade(File homeDirectory, File configDirectory,
Version oldVersion, Version newVersion)
{
if (oldVersion.isOlder("1.2"))
{
if (logger.isInfoEnabled())
{
logger.info("data format is older than 1.2, upgrade to version {}",
SCMContext.getContext().getVersion());
}
fixDate(homeDirectory, configDirectory);
}
}
/**
* Method description
*
*
* @param value
*
* @return
*/
private String convertDate(String value)
{
if (!Strings.isNullOrEmpty(value))
{
try
{
Date date = Util.parseDate(value);
if (date != null)
{
value = Long.toString(date.getTime());
}
}
catch (ParseException ex)
{
logger.warn("could not parse date", ex);
}
}
return value;
}
/**
* Method description
*
*
*
* @param baseDirectory
* @param configDirectory
*/
private void fixDate(File baseDirectory, File configDirectory)
{
try
{
DocumentBuilder builder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
File backupDirectory = createBackupDirectory(baseDirectory,
"upgrade to version {0}");
fixDate(builder, configDirectory, backupDirectory, "users.xml");
fixDate(builder, configDirectory, backupDirectory, "groups.xml");
fixDate(builder, configDirectory, backupDirectory, "repositories.xml");
}
catch (Exception ex)
{
logger.error("could not parse document", ex);
}
}
/**
* Method description
*
*
* @param builder
* @param configDirectory
* @param backupDirectory
* @param filename
*
* @throws IOException
* @throws SAXException
* @throws TransformerConfigurationException
* @throws TransformerException
*/
private void fixDate(DocumentBuilder builder, File configDirectory,
File backupDirectory, String filename)
throws SAXException, IOException, TransformerConfigurationException,
TransformerException
{
File configFile = new File(configDirectory, filename);
File backupFile = new File(backupDirectory, filename);
IOUtil.copy(configFile, backupFile);
if (configFile.exists())
{
if (logger.isInfoEnabled())
{
logger.info("fix date elements of {}", configFile.getPath());
}
Document document = builder.parse(configFile);
fixDate(document, "lastModified");
fixDate(document, "creationDate");
fixDate(document, "creationTime");
writeDocument(document, configFile);
}
}
/**
* Method description
*
*
* @param document
* @param element
*/
private void fixDate(Document document, String element)
{
NodeList nodes = document.getElementsByTagName(element);
if (nodes != null)
{
for (int i = 0; i < nodes.getLength(); i++)
{
Node node = nodes.item(i);
String value = node.getTextContent();
value = convertDate(value);
node.setTextContent(value);
}
}
}
}

View File

@@ -1,256 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.upgrade;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.version.Version;
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
/**
*
* @author Sebastian Sdorra
*/
public class UpgradeManager
{
/** the logger for ScmUpgradeHandler */
private static final Logger logger =
LoggerFactory.getLogger(UpgradeManager.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @throws IOException
*/
public void doUpgrade()
{
File baseDirectory = SCMContext.getContext().getBaseDirectory();
File configDirectory = new File(baseDirectory, "config");
File versionFile = new File(configDirectory, "version.txt");
if (configDirectory.exists())
{
boolean writeVersionFile = false;
String newVersion = SCMContext.getContext().getVersion();
if (versionFile.exists())
{
String oldVersion = getVersionString(versionFile);
if (!Strings.isNullOrEmpty(oldVersion) &&!oldVersion.equals(newVersion))
{
if (!newVersion.equals(oldVersion))
{
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
configDirectory, oldVersion, newVersion);
}
}
}
else
{
writeVersionFile = doUpgradesForOldVersion(baseDirectory,
configDirectory, "1.1", newVersion);
}
if (writeVersionFile)
{
writeVersionFile(versionFile);
logger.info("upgrade to version {} was successful", newVersion);
}
}
else
{
// fresh installation
IOUtil.mkdirs(configDirectory);
writeVersionFile(versionFile);
}
}
/**
* Method description
*
*
* @return
*/
private List<UpgradeHandler> collectUpgradeHandlers()
{
List<UpgradeHandler> upgradeHandlers = Lists.newArrayList();
upgradeHandlers.add(new TimestampUpgradeHandler());
upgradeHandlers.add(new ClientDateFormatUpgradeHandler());
// TODO find upgrade handlers on classpath
return upgradeHandlers;
}
/**
* Method description
*
*
* @param baseDirectory
* @param configDirectory
* @param versionString
* @param oldVersionString
* @param newVersionString
*
* @return
*/
private boolean doUpgradesForOldVersion(File baseDirectory,
File configDirectory, String oldVersionString, String newVersionString)
{
logger.info("start upgrade from version \"{}\" to \"{}\"",
oldVersionString, newVersionString);
boolean writeVersionFile = false;
try
{
Version oldVersion = Version.parse(oldVersionString);
Version newVersion = Version.parse(newVersionString);
doUpgradesForOldVersion(baseDirectory, configDirectory, oldVersion,
newVersion);
writeVersionFile = true;
}
catch (Exception ex)
{
logger.error("error upgrade failed", ex);
}
return writeVersionFile;
}
/**
* Method description
*
*
* @param baseDirectory
* @param configDirectory
* @param version
* @param oldVersion
* @param newVersion
*/
private void doUpgradesForOldVersion(File baseDirectory,
File configDirectory, Version oldVersion, Version newVersion)
{
List<UpgradeHandler> upgradeHandlers = collectUpgradeHandlers();
for (UpgradeHandler upgradeHandler : upgradeHandlers)
{
logger.trace("call upgrade handler {}", upgradeHandler.getClass());
upgradeHandler.doUpgrade(baseDirectory, configDirectory, oldVersion,
newVersion);
}
}
/**
* Method description
*
*
* @param versionFile
*/
private void writeVersionFile(File versionFile)
{
OutputStream output = null;
try
{
output = new FileOutputStream(versionFile);
output.write(SCMContext.getContext().getVersion().getBytes());
}
catch (IOException ex)
{
logger.error("could not write version file", ex);
}
finally
{
IOUtil.close(output);
}
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param versionFile
*
* @return
*/
private String getVersionString(File versionFile)
{
String version = null;
try
{
version = Files.toString(versionFile, Charsets.UTF_8).trim();
}
catch (IOException ex)
{
logger.error("could not read version file", ex);
}
return version;
}
}

View File

@@ -1,138 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.upgrade;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import sonia.scm.SCMContext;
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
*
* @author Sebastian Sdorra
*/
public abstract class XmlUpgradeHandler implements UpgradeHandler
{
/**
* the logger for XmlUpgradeHandler
*/
private static final Logger logger =
LoggerFactory.getLogger(XmlUpgradeHandler.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param baseDirectory
* @param note
*
* @return
*/
protected File createBackupDirectory(File baseDirectory, String note)
{
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
File backupDirectory = new File(baseDirectory,
"backups".concat(File.separator).concat(date));
IOUtil.mkdirs(backupDirectory);
FileWriter writer = null;
note = MessageFormat.format(note, SCMContext.getContext().getVersion());
try
{
writer = new FileWriter(new File(backupDirectory, "note.txt"));
writer.write(note);
}
catch (IOException ex)
{
logger.error("could not write note.txt for backup", ex);
}
finally
{
IOUtil.close(writer);
}
return backupDirectory;
}
/**
* Method description
*
*
* @param document
* @param configFile
*
* @throws TransformerConfigurationException
* @throws TransformerException
*/
protected void writeDocument(Document document, File configFile)
throws TransformerConfigurationException, TransformerException
{
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(document),
new StreamResult(configFile));
}
}

View File

@@ -67,7 +67,7 @@
<value>configuration:read,write:*</value>
</permission>
<permission>
<value>repositoryRole:read,write</value>
<value>repositoryRole:write</value>
</permission>
</permissions>

View File

@@ -89,8 +89,7 @@ class DefaultRepositoryRoleManagerTest {
@BeforeEach
void authorizeUser() {
when(subject.isPermitted("repositoryRole:read")).thenReturn(true);
when(subject.isPermitted("repositoryRole:modify")).thenReturn(true);
when(subject.isPermitted("repositoryRole:write")).thenReturn(true);
}
@Test
@@ -184,8 +183,15 @@ class DefaultRepositoryRoleManagerTest {
}
@Test
void shouldThrowException_forGet() {
assertThrows(UnauthorizedException.class, () -> manager.get("any"));
void shouldReturnNull_forNotExistingRole() {
RepositoryRole role = manager.get("noSuchRole");
assertThat(role).isNull();
}
@Test
void shouldReturnRole_forExistingRole() {
RepositoryRole role = manager.get(CUSTOM_ROLE_NAME);
assertThat(role).isNotNull();
}
@Test
@@ -201,18 +207,25 @@ class DefaultRepositoryRoleManagerTest {
}
@Test
void shouldReturnEmptyList() {
assertThat(manager.getAll()).isEmpty();
void shouldReturnAllRoles() {
List<RepositoryRole> allRoles = manager.getAll();
assertThat(allRoles).containsExactly(CUSTOM_ROLE, SYSTEM_ROLE);
}
@Test
void shouldReturnEmptyFilteredList() {
assertThat(manager.getAll(x -> true, null)).isEmpty();
void shouldReturnFilteredList() {
Collection<RepositoryRole> allRoles = manager.getAll(role -> CUSTOM_ROLE_NAME.equals(role.getName()), null);
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
}
@Test
void shouldReturnEmptyPaginatedList() {
assertThat(manager.getAll(1, 1)).isEmpty();
void shouldReturnPaginatedRoles() {
Collection<RepositoryRole> allRoles =
manager.getAll(
Comparator.comparing(RepositoryRole::getType),
1, 1
);
assertThat(allRoles).containsExactly(CUSTOM_ROLE);
}
}
}

View File

@@ -0,0 +1,76 @@
package sonia.scm.update;
import com.google.common.io.Resources;
import org.mockito.Mockito;
import sonia.scm.SCMContextProvider;
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
public class UpdateStepTestUtil {
private final SCMContextProvider contextProvider;
private final Path tempDir;
private final ConfigurationEntryStoreFactory storeFactory;
public UpdateStepTestUtil(Path tempDir) {
this.tempDir = tempDir;
contextProvider = Mockito.mock(SCMContextProvider.class);
storeFactory = new JAXBConfigurationEntryStoreFactory(contextProvider, null, new DefaultKeyGenerator());
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
}
public SCMContextProvider getContextProvider() {
return contextProvider;
}
public ConfigurationEntryStoreFactory getStoreFactory() {
return storeFactory;
}
public void copyConfigFile(String fileName) throws IOException {
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
copyTestDatabaseFile(configDir, fileName);
}
public void copyConfigFile(String fileName, String targetFileName) throws IOException {
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
copyTestDatabaseFile(configDir, fileName, targetFileName);
}
public ConfigurationEntryStore<AssignedPermission> getStoreForConfigFile(String name) {
return storeFactory
.withType(AssignedPermission.class)
.withName(name)
.build();
}
public Path getFile(String name) {
return tempDir.resolve("config").resolve(name);
}
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
Path targetFileName = Paths.get(fileName).getFileName();
copyTestDatabaseFile(configDir, fileName, targetFileName.toString());
}
private void copyTestDatabaseFile(Path configDir, String fileName, String targetFileName) throws IOException {
URL url = Resources.getResource(fileName);
Files.copy(url.openStream(), configDir.resolve(targetFileName));
}
}

View File

@@ -0,0 +1,136 @@
package sonia.scm.update.group;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.group.Group;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.update.UpdateStepTestUtil;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.linesOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlGroupV1UpdateStepTest {
@Mock
XmlGroupDAO groupDAO;
@Captor
ArgumentCaptor<Group> groupCaptor;
XmlGroupV1UpdateStep updateStep;
private UpdateStepTestUtil testUtil;
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
testUtil = new UpdateStepTestUtil(tempDir);
updateStep = new XmlGroupV1UpdateStep(testUtil.getContextProvider(), groupDAO, testUtil.getStoreFactory());
}
@Nested
class WithExistingDatabase {
@BeforeEach
void captureStoredRepositories() {
doNothing().when(groupDAO).add(groupCaptor.capture());
}
@BeforeEach
void createGroupV1XML() throws IOException {
testUtil.copyConfigFile("sonia/scm/update/group/groups.xml");
}
@Test
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
verify(groupDAO, times(2)).add(any());
}
@Test
void shouldMapAttributesFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
Optional<Group> group = groupCaptor.getAllValues().stream().filter(u -> u.getName().equals("normals")).findFirst();
assertThat(group)
.get()
.hasFieldOrPropertyWithValue("name", "normals")
.hasFieldOrPropertyWithValue("description", "Normal people")
.hasFieldOrPropertyWithValue("type", "xml")
.hasFieldOrPropertyWithValue("members", asList("trillian", "dent"))
.hasFieldOrPropertyWithValue("lastModified", 1559550955883L)
.hasFieldOrPropertyWithValue("creationDate", 1559548942457L);
}
@Test
void shouldExtractProperties() throws JAXBException {
updateStep.doUpdate();
Path propertiesFile = testUtil.getFile("group-properties-v1.xml");
assertThat(propertiesFile)
.exists();
assertThat(linesOf(propertiesFile.toFile()))
.extracting(String::trim)
.containsSequence(
"<key>normals</key>",
"<value>",
"<item>",
"<key>mostly</key>",
"<value>humans</value>",
"</item>",
"</value>");
}
}
@Nested
class WithExistingDatabaseWithEmptyList {
@BeforeEach
void createGroupV1XML() throws IOException {
testUtil.copyConfigFile("sonia/scm/update/group/groups_empty_groups.xml", "groups.xml");
}
@Test
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
verify(groupDAO, times(0)).add(any());
}
}
@Nested
class WithExistingDatabaseWithoutList {
@BeforeEach
void createGroupV1XML() throws IOException {
testUtil.copyConfigFile("sonia/scm/update/group/groups_no_groups.xml", "groups.xml");
}
@Test
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
verify(groupDAO, times(0)).add(any());
}
}
@Test
void shouldNotFailForMissingConfigDir() throws JAXBException {
updateStep.doUpdate();
}
}

View File

@@ -0,0 +1,89 @@
package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class CopyMigrationStrategyTest {
@Mock
SCMContextProvider contextProvider;
@Mock
RepositoryLocationResolver locationResolver;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@BeforeEach
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")
.resolve("git")
.resolve("some")
.resolve("more")
.resolve("directories")
.resolve("than")
.resolve("one");
assertDirectoriesEqual(target.resolve("data"), originalDataDir);
}
private void assertDirectoriesEqual(Path targetDataDir, Path originalDataDir) {
Stream<Path> list = null;
try {
list = Files.list(originalDataDir);
} catch (IOException e) {
fail("could not read original directory", e);
}
list.forEach(
original -> {
Path expectedTarget = targetDataDir.resolve(original.getFileName());
assertThat(expectedTarget).exists();
if (Files.isDirectory(original)) {
assertDirectoriesEqual(expectedTarget, original);
} else {
assertThat(expectedTarget).hasSameContentAs(original);
}
}
);
}
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class InlineMigrationStrategyTest {
@Mock
SCMContextProvider contextProvider;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@Test
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
}
private Path resolveOldDirectory(Path tempDir) {
return tempDir.resolve("repositories").resolve("git").resolve("some").resolve("more").resolve("directories").resolve("than").resolve("one");
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.repository.update;
package sonia.scm.update.repository;
import com.google.common.io.Resources;
import org.assertj.core.api.Assertions;
@@ -42,7 +42,7 @@ class MigrateVerbsToPermissionRolesTest {
@BeforeEach
void init(@TempDirectory.TempDir Path tempDir) throws IOException {
URL metadataUrl = Resources.getResource("sonia/scm/repository/update/metadataWithoutRoles.xml");
URL metadataUrl = Resources.getResource("sonia/scm/update/repository/metadataWithoutRoles.xml");
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
doAnswer(invocation -> {
((BiConsumer<String, Path>) invocation.getArgument(0)).accept(EXISTING_REPOSITORY_ID, tempDir);

View File

@@ -0,0 +1,77 @@
package sonia.scm.update.repository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.update.repository.MigrationStrategy;
import sonia.scm.update.repository.MigrationStrategyDao;
import javax.xml.bind.JAXBException;
import java.nio.file.Path;
import java.util.Optional;
import static org.mockito.Mockito.when;
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class MigrationStrategyDaoTest {
@Mock
SCMContextProvider contextProvider;
private ConfigurationStoreFactory storeFactory;
@BeforeEach
void initStore(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
storeFactory = new JAXBConfigurationStoreFactory(contextProvider, null);
}
@Test
void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
Optional<MigrationStrategy> strategy = dao.get("any");
Assertions.assertThat(strategy).isEmpty();
}
@Test
void shouldReturnNewValue() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
dao.set("id", INLINE);
Optional<MigrationStrategy> strategy = dao.get("id");
Assertions.assertThat(strategy).contains(INLINE);
}
@Nested
class WithExistingDatabase {
@BeforeEach
void initExistingDatabase() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
dao.set("id", INLINE);
}
@Test
void shouldFindExistingValue() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
Optional<MigrationStrategy> strategy = dao.get("id");
Assertions.assertThat(strategy).contains(INLINE);
}
}
}

View File

@@ -0,0 +1,25 @@
package sonia.scm.update.repository;
import com.google.inject.Injector;
import sonia.scm.update.repository.MigrationStrategy.Instance;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class MigrationStrategyMock {
static Injector init() {
Map<Class, Instance> mocks = new HashMap<>();
Injector mock = mock(Injector.class);
when(
mock.getInstance(any(Class.class)))
.thenAnswer(
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class<Instance>) key))
);
return mock;
}
}

View File

@@ -0,0 +1,62 @@
package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@ExtendWith(MockitoExtension.class)
class MoveMigrationStrategyTest {
@Mock
SCMContextProvider contextProvider;
@Mock
RepositoryLocationResolver locationResolver;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@BeforeEach
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")
.resolve("git")
.resolve("some");
assertThat(originalDataDir).doesNotExist();
}
}

View File

@@ -0,0 +1,67 @@
package sonia.scm.update.repository;
import sonia.scm.repository.spi.ZippedRepositoryTestBase;
import java.io.IOException;
import java.nio.file.Path;
class V1RepositoryFileSystem {
/**
* Creates the following v1 repositories in the temp dir:
* <pre>
* <repository>
* <properties/>
* <contact>arthur@dent.uk</contact>
* <creationDate>1558423492071</creationDate>
* <description>A repository with two folders.</description>
* <id>3b91caa5-59c3-448f-920b-769aaa56b761</id>
* <name>one/directory</name>
* <public>false</public>
* <archived>false</archived>
* <type>git</type>
* </repository>
* <repository>
* <properties/>
* <contact>arthur@dent.uk</contact>
* <creationDate>1558423543716</creationDate>
* <description>A repository in deeply nested folders.</description>
* <id>c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f</id>
* <name>some/more/directories/than/one</name>
* <public>false</public>
* <archived>false</archived>
* <type>git</type>
* </repository>
* <repository>
* <properties/>
* <contact>arthur@dent.uk</contact>
* <creationDate>1558423440258</creationDate>
* <description>A simple repository without directories.</description>
* <id>454972da-faf9-4437-b682-dc4a4e0aa8eb</id>
* <lastModified>1558425918578</lastModified>
* <name>simple</name>
* <permissions>
* <groupPermission>true</groupPermission>
* <name>mice</name>
* <type>WRITE</type>
* </permissions>
* <permissions>
* <groupPermission>false</groupPermission>
* <name>dent</name>
* <type>OWNER</type>
* </permissions>
* <permissions>
* <groupPermission>false</groupPermission>
* <name>trillian</name>
* <type>READ</type>
* </permissions>
* <public>false</public>
* <archived>false</archived>
* <type>git</type>
* <url>http://localhost:8081/scm/git/simple</url>
* </repository>
* </pre>
*/
static void createV1Home(Path tempDir) throws IOException {
ZippedRepositoryTestBase.extract(tempDir.toFile(), "sonia/scm/update/repository/scm-home.v1.zip");
}
}

View File

@@ -0,0 +1,47 @@
package sonia.scm.update.repository;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
class XmlRepositoryFileNameUpdateStepTest {
SCMContextProvider contextProvider = mock(SCMContextProvider.class);
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@Test
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
updateStep.doUpdate();
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
assertThat(configDir.resolve("repositories.xml")).doesNotExist();
verify(repositoryDAO).refresh();
}
}

View File

@@ -0,0 +1,234 @@
package sonia.scm.update.repository;
import com.google.common.io.Resources;
import com.google.inject.Injector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.InMemoryConfigurationEntryStore;
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
import sonia.scm.update.UpdateStepTestUtil;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.update.repository.MigrationStrategy.COPY;
import static sonia.scm.update.repository.MigrationStrategy.INLINE;
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlRepositoryV1UpdateStepTest {
Injector injectorMock = MigrationStrategyMock.init();
@Mock
XmlRepositoryDAO repositoryDAO;
@Mock
MigrationStrategyDao migrationStrategyDao;
ConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore());
@Captor
ArgumentCaptor<Repository> storeCaptor;
@Captor
ArgumentCaptor<Path> locationCaptor;
UpdateStepTestUtil testUtil;
XmlRepositoryV1UpdateStep updateStep;
@BeforeEach
void createUpdateStepFromMocks(@TempDirectory.TempDir Path tempDir) {
testUtil = new UpdateStepTestUtil(tempDir);
updateStep = new XmlRepositoryV1UpdateStep(
testUtil.getContextProvider(),
repositoryDAO,
migrationStrategyDao,
injectorMock,
configurationEntryStoreFactory
);
}
@Nested
class WithExistingDatabase {
@BeforeEach
void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException {
V1RepositoryFileSystem.createV1Home(tempDir);
}
@BeforeEach
void captureStoredRepositories() {
lenient().doNothing().when(repositoryDAO).add(storeCaptor.capture(), locationCaptor.capture());
}
@BeforeEach
void createMigrationPlan() {
lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
}
@Test
void shouldCreateNewRepositories() throws JAXBException {
updateStep.doUpdate();
verify(repositoryDAO, times(3)).add(any(), any());
}
@Test
void shouldMapAttributes() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("type", "git")
.hasFieldOrPropertyWithValue("contact", "arthur@dent.uk")
.hasFieldOrPropertyWithValue("description", "A simple repository without directories.");
}
@Test
void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "git")
.hasFieldOrPropertyWithValue("name", "simple");
}
@Test
void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("one");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "one")
.hasFieldOrPropertyWithValue("name", "directory");
}
@Test
void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("some");
assertThat(repository)
.get()
.hasFieldOrPropertyWithValue("namespace", "some")
.hasFieldOrPropertyWithValue("name", "more_directories_than_one");
}
@Test
void shouldMapPermissions() throws JAXBException {
updateStep.doUpdate();
Optional<Repository> repository = findByNamespace("git");
assertThat(repository.get().getPermissions())
.hasSize(3)
.contains(
new RepositoryPermission("mice", "WRITE", true),
new RepositoryPermission("dent", "OWNER", false),
new RepositoryPermission("trillian", "READ", false)
);
}
@Test
void shouldExtractPropertiesFromRepositories() throws JAXBException {
updateStep.doUpdate();
ConfigurationEntryStore<Object> store = configurationEntryStoreFactory.withType(null).withName("").build();
assertThat(store.getAll())
.hasSize(3);
}
@Test
void shouldUseDirectoryFromStrategy(@TempDirectory.TempDir Path tempDir) throws JAXBException {
Path targetDir = tempDir.resolve("someDir");
MigrationStrategy.Instance strategyMock = injectorMock.getInstance(InlineMigrationStrategy.class);
when(strategyMock.migrate("454972da-faf9-4437-b682-dc4a4e0aa8eb", "simple", "git")).thenReturn(targetDir);
updateStep.doUpdate();
assertThat(locationCaptor.getAllValues()).contains(targetDir);
}
@Test
void shouldFailForMissingMigrationStrategy() {
lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(empty());
assertThrows(IllegalStateException.class, () -> updateStep.doUpdate());
}
@Test
void shouldBackupOldRepositoryDatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException {
updateStep.doUpdate();
assertThat(tempDir.resolve("config").resolve("repositories.xml")).doesNotExist();
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).exists();
}
}
@Test
void shouldNotFailIfNoOldDatabaseExists() throws JAXBException {
updateStep.doUpdate();
}
@Test
void shouldNotFailIfFormerV2DatabaseExists(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
createFormerV2RepositoriesFile(tempDir);
updateStep.doUpdate();
}
@Test
void shouldNotBackupFormerV2DatabaseFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException {
createFormerV2RepositoriesFile(tempDir);
updateStep.doUpdate();
assertThat(tempDir.resolve("config").resolve("repositories.xml")).exists();
assertThat(tempDir.resolve("config").resolve("repositories.xml.v1.backup")).doesNotExist();
}
private void createFormerV2RepositoriesFile(@TempDirectory.TempDir Path tempDir) throws IOException {
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
Files.copy(url.openStream(), configDir.resolve("repositories.xml"));
}
private Optional<Repository> findByNamespace(String namespace) {
return storeCaptor.getAllValues().stream().filter(r -> r.getNamespace().equals(namespace)).findFirst();
}
}

View File

@@ -0,0 +1,94 @@
package sonia.scm.update.security;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.security.AssignedPermission;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.InMemoryConfigurationEntryStore;
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
import sonia.scm.update.security.XmlSecurityV1UpdateStep;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class XmlSecurityV1UpdateStepTest {
@Mock
SCMContextProvider contextProvider;
XmlSecurityV1UpdateStep updateStep;
ConfigurationEntryStore<AssignedPermission> assignedPermissionStore;
@BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) {
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
assignedPermissionStore = new InMemoryConfigurationEntryStore<>();
ConfigurationEntryStoreFactory inMemoryConfigurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(assignedPermissionStore);
updateStep = new XmlSecurityV1UpdateStep(contextProvider, inMemoryConfigurationEntryStoreFactory);
}
@Nested
class WithExistingDatabase {
@BeforeEach
void createConfigV1XML(@TempDirectory.TempDir Path tempDir) throws IOException {
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
copyTestDatabaseFile(configDir, "config.xml");
}
@Test
void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException {
updateStep.doUpdate();
List<String> assignedPermission =
assignedPermissionStore.getAll().values()
.stream()
.filter(a -> a.getPermission().getValue().equals("*"))
.filter(a -> !a.isGroupPermission())
.map(AssignedPermission::getName)
.collect(toList());
assertThat(assignedPermission).contains("arthur", "dent", "ldap-admin");
}
@Test
void shouldCreatePermissionForGroupsConfiguredAsAdmin() throws JAXBException {
updateStep.doUpdate();
List<String> assignedPermission =
assignedPermissionStore.getAll().values()
.stream()
.filter(a -> a.getPermission().getValue().equals("*"))
.filter(AssignedPermission::isGroupPermission)
.map(AssignedPermission::getName)
.collect(toList());
assertThat(assignedPermission).contains("admins", "vogons");
}
}
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
URL url = Resources.getResource("sonia/scm/update/security/" + fileName);
Files.copy(url.openStream(), configDir.resolve(fileName));
}
@Test
void shouldNotFailForMissingConfigDir() throws JAXBException {
updateStep.doUpdate();
}
}

Some files were not shown because too many files have changed in this diff Show More