merge + refactor getStoreDirectory

This commit is contained in:
Mohamed Karray
2018-11-28 15:14:49 +01:00
98 changed files with 1474 additions and 1534 deletions

View File

@@ -34,16 +34,12 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import com.google.common.io.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ConfigurationException;
import sonia.scm.ContextEntry;
import sonia.scm.io.CommandResult;
import sonia.scm.io.ExtendedCommand;
import sonia.scm.io.FileSystem;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
@@ -62,6 +58,7 @@ 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
@@ -69,73 +66,28 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
private static final Logger logger =
LoggerFactory.getLogger(AbstractSimpleRepositoryHandler.class);
private FileSystem fileSystem;
private final RepositoryLocationResolver repositoryLocationResolver;
public AbstractSimpleRepositoryHandler(ConfigurationStoreFactory storeFactory,
FileSystem fileSystem, RepositoryLocationResolver repositoryLocationResolver) {
RepositoryLocationResolver repositoryLocationResolver) {
super(storeFactory);
this.fileSystem = fileSystem;
this.repositoryLocationResolver = repositoryLocationResolver;
}
@Override
public Repository create(Repository repository) {
File directory = repositoryLocationResolver.getInitialNativeDirectory(repository);
if (directory != null && directory.exists()) {
throw new AlreadyExistsException(repository);
}
checkPath(directory, repository);
File nativeDirectory = resolveNativeDirectory(repository);
try {
fileSystem.create(directory);
create(repository, directory);
postCreate(repository, directory);
return repository;
} catch (Exception ex) {
if (directory != null && directory.exists()) {
logger.warn("delete repository directory {}, because of failed repository creation", directory);
try {
fileSystem.destroy(directory);
} catch (IOException e) {
logger.error("Could not destroy directory", e);
}
}
Throwables.propagateIfPossible(ex, AlreadyExistsException.class);
// This point will never be reached
return null;
create(repository, nativeDirectory);
postCreate(repository, nativeDirectory);
} catch (IOException e) {
throw new InternalRepositoryException(repository, "could not create native repository directory", e);
}
}
@Override
public String createResourcePath(Repository repository) {
StringBuilder path = new StringBuilder("/");
path.append(getType().getName()).append("/").append(repository.getId());
return path.toString();
return repository;
}
@Override
public void delete(Repository repository) {
File directory = null;
try {
directory = repositoryLocationResolver.getRepositoryDirectory(repository);
} catch (IOException e) {
throw new InternalRepositoryException(repository, "Cannot get the repository directory");
}
try {
if (directory.exists()) {
fileSystem.destroy(directory);
} else {
logger.warn("repository {} not found", repository.getNamespaceAndName());
}
} catch (IOException e) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("directory", directory.toString()).in(repository), "could not delete repository directory", e);
}
}
@Override
@@ -157,22 +109,13 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
public File getDirectory(Repository repository) {
File directory;
if (isConfigured()) {
try {
directory = repositoryLocationResolver.getNativeDirectory(repository);
} catch (IOException e) {
throw new ConfigurationException("Error on getting the current repository directory");
}
directory = resolveNativeDirectory(repository);
} else {
throw new ConfigurationException("RepositoryHandler is not configured");
}
return directory;
}
@Override
public File getInitialBaseDirectory() {
return repositoryLocationResolver.getInitialBaseDirectory();
}
@Override
public String getVersionInformation() {
return DEFAULT_VERSION_INFORMATION;
@@ -224,40 +167,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
return content;
}
/**
* Returns true if the directory is a repository.
*
* @param directory directory to check
* @return true if the directory is a repository
* @since 1.9
*/
protected boolean isRepository(File directory) {
return new File(directory, DOT.concat(getType().getName())).exists();
private File resolveNativeDirectory(Repository repository) {
return new File(repositoryLocationResolver.getRepositoryDirectory(repository), REPOSITORIES_NATIVE_DIRECTORY);
}
/**
* Check path for existing repositories
*
* @param directory repository target directory
* @throws RuntimeException when the parent directory already is a repository
*/
private void checkPath(File directory, Repository repository) {
if (directory == null) {
return;
}
File repositoryDirectory = getInitialBaseDirectory();
File parent = directory.getParentFile();
while ((parent != null) && !repositoryDirectory.equals(parent)) {
logger.trace("check {} for existing repository", parent);
if (isRepository(parent)) {
throw new InternalRepositoryException(repository, "parent path" + parent + " is a repository");
}
parent = parent.getParentFile();
}
}
}

View File

@@ -1,63 +1,53 @@
package sonia.scm.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.FileSystem;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
/**
*
* A Location Resolver for File based Repository Storage.
*
* WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files.
*
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
* <p>
* <b>WARNING:</b> The Locations provided with this class may not be used from the plugins to store any plugin specific files.
* <p>
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data<br>
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files<br>
* Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations
*
* @author Mohamed Karray
* @since 2.0.0
*/
public final class InitialRepositoryLocationResolver {
public class InitialRepositoryLocationResolver {
private static final String REPOSITORIES_DIRECTORY = "repositories";
public static final String REPOSITORIES_NATIVE_DIRECTORY = "data";
private SCMContextProvider context;
private FileSystem fileSystem;
public static final String DEFAULT_REPOSITORY_PATH = "repositories";
private final SCMContextProvider context;
@Inject
public InitialRepositoryLocationResolver(SCMContextProvider context, FileSystem fileSystem) {
public InitialRepositoryLocationResolver(SCMContextProvider context) {
this.context = context;
this.fileSystem = fileSystem;
}
public static File getNativeDirectory(File repositoriesDirectory, String repositoryId) {
return new File(repositoriesDirectory, repositoryId
.concat(File.separator)
.concat(REPOSITORIES_NATIVE_DIRECTORY));
public InitialRepositoryLocation getRelativeRepositoryPath(Repository repository) {
String relativePath = DEFAULT_REPOSITORY_PATH + File.separator + repository.getId();
return new InitialRepositoryLocation(new File(context.getBaseDirectory(), relativePath), relativePath);
}
public File getBaseDirectory() {
return new File(context.getBaseDirectory(), REPOSITORIES_DIRECTORY);
}
public static class InitialRepositoryLocation {
private final File absolutePath;
private final String relativePath;
File getContextBaseDirectory() {
return context.getBaseDirectory();
}
public InitialRepositoryLocation(File absolutePath, String relativePath) {
this.absolutePath = absolutePath;
this.relativePath = relativePath;
}
public File createDirectory(Repository repository) throws IOException {
File initialRepoFolder = getDirectory(repository);
fileSystem.create(initialRepoFolder);
return initialRepoFolder;
}
public File getAbsolutePath() {
return absolutePath;
}
public File getDirectory(Repository repository) {
return new File(context.getBaseDirectory(), REPOSITORIES_DIRECTORY
.concat(File.separator)
.concat(repository.getId()));
public String getRelativePath() {
return relativePath;
}
}
}

View File

@@ -11,10 +11,7 @@ import java.nio.file.Path;
public interface PathBasedRepositoryDAO extends RepositoryDAO {
/**
* get the current path of the repository
*
* @param repository
* @return the current path of the repository
* Get the current path of the repository. This works for existing repositories only, not for repositories that should be created.
*/
Path getPath(Repository repository) throws RepositoryPathNotFoundException;
Path getPath(Repository repository) ;
}

View File

@@ -48,10 +48,4 @@ public interface RepositoryDirectoryHandler extends RepositoryHandler {
* @return the current directory of the given repository
*/
File getDirectory(Repository repository);
/**
* get the initial directory of all repositories
* @return the initial directory of all repositories
*/
File getInitialBaseDirectory();
}

View File

@@ -50,17 +50,6 @@ public interface RepositoryHandler
extends Handler<Repository>
{
/**
* Returns the resource path for the given {@link Repository}.
* The resource path is part of the {@link Repository} url.
*
*
*
* @param repository given {@link Repository}
* @return resource path of the {@link Repository}
*/
public String createResourcePath(Repository repository);
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -4,18 +4,14 @@ import groovy.lang.Singleton;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import static sonia.scm.repository.InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY;
/**
*
* A Location Resolver for File based Repository Storage.
*
* WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files.
*
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
* <p>
* <b>WARNING:</b> The Locations provided with this class may not be used from the plugins to store any plugin specific files.
* <p>
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data<br>
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files<br>
* Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations
*
* @author Mohamed Karray
@@ -24,10 +20,6 @@ import static sonia.scm.repository.InitialRepositoryLocationResolver.REPOSITORIE
@Singleton
public class RepositoryLocationResolver {
private static final String REPOSITORIES_STORES_DIRECTORY = "stores";
private static final String REPOSITORIES_CONFIG_DIRECTORY = "config";
private static final String GLOBAL_STORE_BASE_DIRECTORY = "var";
private RepositoryDAO repositoryDAO;
private InitialRepositoryLocationResolver initialRepositoryLocationResolver;
@@ -37,59 +29,13 @@ public class RepositoryLocationResolver {
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
}
/**
* Get the current repository directory from the dao or create the initial directory if the repository does not exists
* @param repository
* @return the current repository directory from the dao or the initial directory if the repository does not exists
* @throws IOException
*/
public File getRepositoryDirectory(Repository repository) throws IOException {
public File getRepositoryDirectory(Repository repository){
if (repositoryDAO instanceof PathBasedRepositoryDAO) {
PathBasedRepositoryDAO pathBasedRepositoryDAO = (PathBasedRepositoryDAO) repositoryDAO;
try {
return pathBasedRepositoryDAO.getPath(repository).toFile();
} catch (RepositoryPathNotFoundException e) {
return createInitialDirectory(repository);
}
return pathBasedRepositoryDAO.getPath(repository).toFile();
}
return createInitialDirectory(repository);
return initialRepositoryLocationResolver.getRelativeRepositoryPath(repository).getAbsolutePath();
}
public File getInitialBaseDirectory() {
return initialRepositoryLocationResolver.getBaseDirectory();
}
public File createInitialDirectory(Repository repository) throws IOException {
return initialRepositoryLocationResolver.createDirectory(repository);
}
public File getInitialDirectory(Repository repository) {
return initialRepositoryLocationResolver.getDirectory(repository);
}
public File getNativeDirectory(Repository repository) throws IOException {
return new File (getRepositoryDirectory(repository), REPOSITORIES_NATIVE_DIRECTORY);
}
public File getInitialNativeDirectory(Repository repository) {
return new File (getInitialDirectory(repository), REPOSITORIES_NATIVE_DIRECTORY);
}
/**
* Get the store directory of a specific repository
* @param repository
* @return the store directory of a specific repository
*/
public File getStoresDirectory(Repository repository) throws IOException{
return new File (getRepositoryDirectory(repository), REPOSITORIES_STORES_DIRECTORY);
}
public File getConfigDirectory(Repository repository) throws IOException {
return new File (getRepositoryDirectory(repository), REPOSITORIES_CONFIG_DIRECTORY);
}
public File getGlobalStoreDirectory() {
return new File(initialRepositoryLocationResolver.getContextBaseDirectory(),
GLOBAL_STORE_BASE_DIRECTORY.concat(File.separator));
}
}

View File

@@ -1,119 +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.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import sonia.scm.io.DirectoryFileFilter;
import sonia.scm.util.IOUtil;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
* @since 1.11
*/
public final class RepositoryUtil {
private RepositoryUtil() {}
public static List<File> searchRepositoryDirectories(File directory, String... names) {
List<File> repositories = new ArrayList<>();
searchRepositoryDirectories(repositories, directory, Arrays.asList(names));
return repositories;
}
@SuppressWarnings("squid:S2083") // ignore, because the path is validated at {@link #getRepositoryId(File, File)}
public static String getRepositoryId(RepositoryDirectoryHandler handler, String directoryPath) throws IOException {
return getRepositoryId(handler.getInitialBaseDirectory(), new File(directoryPath));
}
public static String getRepositoryId(RepositoryDirectoryHandler handler, File directory) throws IOException {
return getRepositoryId(handler.getInitialBaseDirectory(), directory);
}
public static String getRepositoryId(File baseDirectory, File directory) throws IOException {
String path = directory.getCanonicalPath();
String basePath = baseDirectory.getCanonicalPath();
Preconditions.checkArgument(
path.startsWith(basePath),
"repository path %s is not in the main repository path %s", path, basePath
);
String id = IOUtil.trimSeperatorChars(path.substring(basePath.length()).replace(InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY, ""));
Preconditions.checkArgument(
!id.contains("\\") && !id.contains("/"),
"got illegal repository directory with separators in id: %s", path
);
return id;
}
private static void searchRepositoryDirectories(List<File> repositories, File directory, List<String> names) {
boolean found = false;
for (String name : names) {
if (new File(directory, name).exists()) {
found = true;
break;
}
}
if (found) {
repositories.add(directory);
} else {
File[] directories = directory.listFiles(DirectoryFileFilter.instance);
if (directories != null) {
for (File d : directories) {
searchRepositoryDirectories(repositories, d, names);
}
}
}
}
}

View File

@@ -74,19 +74,24 @@ public final class HookEventFacade
//~--- methods --------------------------------------------------------------
public HookEventHandler handle(String id) {
return handle(repositoryManagerProvider.get().get(id));
Repository repository = repositoryManagerProvider.get().get(id);
if (repository == null)
{
throw notFound(entity("repository", id));
}
return handle(repository);
}
public HookEventHandler handle(NamespaceAndName namespaceAndName) {
return handle(repositoryManagerProvider.get().get(namespaceAndName));
Repository repository = repositoryManagerProvider.get().get(namespaceAndName);
if (repository == null)
{
throw notFound(entity(namespaceAndName));
}
return handle(repository);
}
public HookEventHandler handle(Repository repository) {
if (repository == null)
{
throw notFound(entity(repository));
}
return new HookEventHandler(repositoryManagerProvider.get(),
hookContextFactory, repository);
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.repository;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContextProvider;
import java.io.File;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class InitialRepositoryLocationResolverTest {
@Mock
private SCMContextProvider context;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void init() throws IOException {
when(context.getBaseDirectory()).thenReturn(temporaryFolder.newFolder());
}
@Test
public void shouldComputeInitialDirectory() {
InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(context);
Repository repository = new Repository();
repository.setId("ABC");
InitialRepositoryLocationResolver.InitialRepositoryLocation directory = resolver.getRelativeRepositoryPath(repository);
assertThat(directory.getAbsolutePath()).isEqualTo(new File(context.getBaseDirectory(), "repositories/ABC"));
assertThat(directory.getRelativePath()).isEqualTo( "repositories/ABC");
}
}

View File

@@ -1,62 +0,0 @@
package sonia.scm.repository;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class RepositoryUtilTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock
private RepositoryDirectoryHandler repositoryHandler;
private File repositoryTypeRoot;
@Before
public void setUpMocks() throws IOException {
repositoryTypeRoot = temporaryFolder.newFolder();
when(repositoryHandler.getInitialBaseDirectory()).thenReturn(repositoryTypeRoot);
}
@Test
public void testGetRepositoryId() throws IOException {
File repository = new File(repositoryTypeRoot, "abc");
String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
assertEquals("abc", id);
}
@Test(expected = IllegalArgumentException.class)
public void testGetRepositoryIdWithInvalidPath() throws IOException {
File repository = new File("/etc/abc");
String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
assertEquals("abc", id);
}
@Test(expected = IllegalArgumentException.class)
public void testGetRepositoryIdWithInvalidPathButSameLength() throws IOException {
File repository = new File(temporaryFolder.newFolder(), "abc");
String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
assertEquals("abc", id);
}
@Test(expected = IllegalArgumentException.class)
public void testGetRepositoryIdWithInvalidId() throws IOException {
File repository = new File(repositoryTypeRoot, "abc/123");
RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
}
}

View File

@@ -21,6 +21,9 @@ public class RepositoryPath implements ModelObject {
@XmlTransient
private Repository repository;
@XmlTransient
private boolean toBeSynchronized;
/**
* Needed from JAXB
*/
@@ -87,4 +90,12 @@ public class RepositoryPath implements ModelObject {
public boolean isValid() {
return StringUtils.isNotEmpty(path);
}
public boolean toBeSynchronized() {
return toBeSynchronized;
}
public void setToBeSynchronized(boolean toBeSynchronized) {
this.toBeSynchronized = toBeSynchronized;
}
}

View File

@@ -37,19 +37,21 @@ package sonia.scm.repository.xml;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.InitialRepositoryLocationResolver.InitialRepositoryLocation;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathBasedRepositoryDAO;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPathNotFoundException;
import sonia.scm.store.JAXBConfigurationStore;
import sonia.scm.store.Store;
import sonia.scm.store.StoreConstants;
import sonia.scm.util.IOUtil;
import sonia.scm.xml.AbstractXmlDAO;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Optional;
@@ -61,19 +63,21 @@ public class XmlRepositoryDAO
extends AbstractXmlDAO<Repository, XmlRepositoryDatabase>
implements PathBasedRepositoryDAO {
/**
* Field description
*/
public static final String STORE_NAME = "repositories";
private InitialRepositoryLocationResolver initialRepositoryLocationResolver;
private final FileSystem fileSystem;
private final SCMContextProvider context;
//~--- constructors ---------------------------------------------------------
@Inject
public XmlRepositoryDAO(InitialRepositoryLocationResolver initialRepositoryLocationResolver, SCMContextProvider context) {
super(new JAXBConfigurationStore<>(XmlRepositoryDatabase.class, new File(context.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME + File.separator + STORE_NAME + StoreConstants.FILE_EXTENSION)));
IOUtil.mkdirs(new File(context.getBaseDirectory(), StoreConstants.CONFIG_DIRECTORY_NAME + File.separator + STORE_NAME + StoreConstants.FILE_EXTENSION));
public XmlRepositoryDAO(InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, SCMContextProvider context) {
super(new JAXBConfigurationStore<>(XmlRepositoryDatabase.class,
new File(context.getBaseDirectory(), Store.CONFIG.getGlobalStoreDirectory()+File.separator+ STORE_NAME + StoreConstants.FILE_EXTENSION)));
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
this.fileSystem = fileSystem;
this.context = context;
}
//~--- methods --------------------------------------------------------------
@@ -95,14 +99,22 @@ public class XmlRepositoryDAO
@Override
public void modify(Repository repository) {
db.remove(repository.getId());
add(repository);
RepositoryPath repositoryPath = findExistingRepositoryPath(repository).orElseThrow(() -> new InternalRepositoryException(repository, "path object for repository not found"));
repositoryPath.setRepository(repository);
repositoryPath.setToBeSynchronized(true);
storeDB();
}
@Override
public void add(Repository repository) {
String path = initialRepositoryLocationResolver.getDirectory(repository).getAbsolutePath();
RepositoryPath repositoryPath = new RepositoryPath(path, repository.getId(), repository.clone());
InitialRepositoryLocation initialLocation = initialRepositoryLocationResolver.getRelativeRepositoryPath(repository);
try {
fileSystem.create(initialLocation.getAbsolutePath());
} catch (IOException e) {
throw new InternalRepositoryException(repository, "could not create directory for repository data: " + initialLocation.getAbsolutePath(), e);
}
RepositoryPath repositoryPath = new RepositoryPath(initialLocation.getRelativePath(), repository.getId(), repository.clone());
repositoryPath.setToBeSynchronized(true);
synchronized (store) {
db.add(repositoryPath);
storeDB();
@@ -134,6 +146,17 @@ public class XmlRepositoryDAO
return repository.clone();
}
@Override
public void delete(Repository repository) {
Path directory = getPath(repository);
super.delete(repository);
try {
fileSystem.destroy(directory.toFile());
} catch (IOException e) {
throw new InternalRepositoryException(repository, "could not delete repository directory", e);
}
}
/**
* Method description
*
@@ -145,15 +168,19 @@ public class XmlRepositoryDAO
}
@Override
public Path getPath(Repository repository) throws RepositoryPathNotFoundException {
Optional<RepositoryPath> repositoryPath = db.getPaths().stream()
.filter(repoPath -> repoPath.getId().equals(repository.getId()))
.findFirst();
if (!repositoryPath.isPresent()) {
throw new RepositoryPathNotFoundException();
} else {
public Path getPath(Repository repository) {
return context
.getBaseDirectory()
.toPath()
.resolve(
findExistingRepositoryPath(repository)
.map(RepositoryPath::getPath)
.orElseThrow(() -> new InternalRepositoryException(repository, "could not find base directory for repository")));
}
return Paths.get(repositoryPath.get().getPath());
}
private Optional<RepositoryPath> findExistingRepositoryPath(Repository repository) {
return db.values().stream()
.filter(repoPath -> repoPath.getId().equals(repository.getId()))
.findAny();
}
}

View File

@@ -98,16 +98,6 @@ public class XmlRepositoryDatabase implements XmlDatabase<RepositoryPath> {
return get(id) != null;
}
public boolean contains(Repository repository)
{
return repositoryPathMap.containsKey(createKey(repository));
}
public void remove(Repository repository)
{
repositoryPathMap.remove(createKey(repository));
}
@Override
public RepositoryPath remove(String id)
{
@@ -129,11 +119,6 @@ public class XmlRepositoryDatabase implements XmlDatabase<RepositoryPath> {
return repositoryPathMap.values();
}
public Collection<RepositoryPath> getPaths() {
return repositoryPathMap.values();
}
public Repository get(NamespaceAndName namespaceAndName) {
RepositoryPath repositoryPath = repositoryPathMap.get(createKey(namespaceAndName));
if (repositoryPath != null) {

View File

@@ -31,10 +31,12 @@
package sonia.scm.repository.xml;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.store.StoreConstants;
import sonia.scm.store.StoreException;
import sonia.scm.util.IOUtil;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@@ -42,6 +44,8 @@ import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -62,14 +66,20 @@ public class XmlRepositoryMapAdapter extends XmlAdapter<XmlRepositoryList, Map<S
// marshall the repo_path/metadata.xml files
for (RepositoryPath repositoryPath : repositoryPaths.getRepositoryPaths()) {
File dir = new File(repositoryPath.getPath());
if (!dir.exists()){
IOUtil.mkdirs(dir);
if (repositoryPath.toBeSynchronized()) {
File baseDirectory = SCMContext.getContext().getBaseDirectory();
Path dir = baseDirectory.toPath().resolve(repositoryPath.getPath());
if (!Files.isDirectory(dir)) {
throw new InternalRepositoryException(repositoryPath.getRepository(), "repository path not found");
}
marshaller.marshal(repositoryPath.getRepository(), getRepositoryMetadataFile(dir.toFile()));
repositoryPath.setToBeSynchronized(false);
}
marshaller.marshal(repositoryPath.getRepository(), getRepositoryMetadataFile(dir));
}
} catch (JAXBException ex) {
throw new StoreException("failed to marshall repository database", ex);
throw new StoreException("failed to marshal repository database", ex);
}
return repositoryPaths;
@@ -81,18 +91,21 @@ public class XmlRepositoryMapAdapter extends XmlAdapter<XmlRepositoryList, Map<S
}
@Override
public Map<String, RepositoryPath> unmarshal(XmlRepositoryList repositories) {
public Map<String, RepositoryPath> unmarshal(XmlRepositoryList repositoryPaths) {
Map<String, RepositoryPath> repositoryPathMap = new LinkedHashMap<>();
try {
JAXBContext context = JAXBContext.newInstance(Repository.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
for (RepositoryPath repositoryPath : repositories) {
Repository repository = (Repository) unmarshaller.unmarshal(getRepositoryMetadataFile(new File(repositoryPath.getPath())));
for (RepositoryPath repositoryPath : repositoryPaths) {
SCMContextProvider contextProvider = SCMContext.getContext();
File baseDirectory = contextProvider.getBaseDirectory();
Repository repository = (Repository) unmarshaller.unmarshal(getRepositoryMetadataFile(baseDirectory.toPath().resolve(repositoryPath.getPath()).toFile()));
repositoryPath.setRepository(repository);
repositoryPathMap.put(XmlRepositoryDatabase.createKey(repository), repositoryPath);
}
} catch (JAXBException ex) {
throw new StoreException("failed to unmarshall object", ex);
throw new StoreException("failed to unmarshal object", ex);
}
return repositoryPathMap;
}

View File

@@ -34,14 +34,12 @@ package sonia.scm.store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
//~--- JDK imports ------------------------------------------------------------
@@ -56,55 +54,54 @@ public abstract class FileBasedStoreFactory {
* the logger for FileBasedStoreFactory
*/
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
private SCMContextProvider contextProvider;
private RepositoryLocationResolver repositoryLocationResolver;
private Store store;
private final String dataDirectoryName;
private File storeDirectory;
private File dataDirectory;
protected FileBasedStoreFactory(RepositoryLocationResolver repositoryLocationResolver, String dataDirectoryName) {
protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
this.contextProvider = contextProvider;
this.repositoryLocationResolver = repositoryLocationResolver;
this.dataDirectoryName = dataDirectoryName;
this.store = store;
}
//~--- get methods ----------------------------------------------------------
/**
* Returns data directory for given name.
*
* @param name name of data directory
*
* @return data directory
*/
protected File getDirectory(String name) {
if (dataDirectory == null) {
dataDirectory = new File(repositoryLocationResolver.getGlobalStoreDirectory(), dataDirectoryName);
LOG.debug("get data directory {}", dataDirectory);
}
File storeDirectory = new File(dataDirectory, name);
IOUtil.mkdirs(storeDirectory);
return storeDirectory;
protected File getStoreLocation(StoreParameters storeParameters) {
return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepository());
}
/**
* Returns data directory for given name.
*
* @param name name of data directory
*
* @return data directory
*/
protected File getDirectory(String name, Repository repository) {
if (dataDirectory == null) {
try {
dataDirectory = new File(repositoryLocationResolver.getStoresDirectory(repository), dataDirectoryName);
} catch (IOException e) {
throw new InternalRepositoryException(repository, MessageFormat.format("Error on getting the store directory {0} of the repository {1}", dataDirectory.getAbsolutePath(), repository.getNamespaceAndName()), e);
protected File getStoreLocation(String name, Class type, Repository repository) {
if (storeDirectory == null) {
if (repository != null) {
LOG.debug("create store with type :{}, name:{} and repository {}", type, name, repository.getNamespaceAndName());
storeDirectory = this.getStoreDirectory(store, repository);
} else {
LOG.debug("create store with type :{} and name:{} ", type, name);
storeDirectory = this.getStoreDirectory(store);
}
LOG.debug("create data directory {}", dataDirectory);
IOUtil.mkdirs(storeDirectory);
}
File storeDirectory = new File(dataDirectory, name);
IOUtil.mkdirs(storeDirectory);
return storeDirectory;
return new File(this.storeDirectory, name);
}
/**
* Get the store directory of a specific repository
* @param store the type of the store
* @param repository the repo
* @return the store directory of a specific repository
*/
private File getStoreDirectory(Store store, Repository repository) {
return new File (repositoryLocationResolver.getRepositoryDirectory(repository), store.getRepositoryStoreDirectory());
}
/**
* Get the global store directory
* @param store the type of the store
* @return the global store directory
*/
private File getStoreDirectory(Store store) {
return new File(contextProvider.getBaseDirectory(), store.getGlobalStoreDirectory());
}
}

View File

@@ -31,14 +31,17 @@
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
import sonia.scm.util.IOUtil;
import java.io.File;
/**
* File based store factory.
@@ -48,8 +51,6 @@ import sonia.scm.security.KeyGenerator;
@Singleton
public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory {
private static final String DIRECTORY_NAME = "blob";
/**
* the logger for FileBlobStoreFactory
*/
@@ -60,21 +61,22 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
/**
* Constructs a new instance.
*
* @param repositoryLocationResolver
* @param repositoryLocationResolver location resolver
* @param keyGenerator key generator
*/
@Inject
public FileBlobStoreFactory(RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(repositoryLocationResolver, DIRECTORY_NAME);
public FileBlobStoreFactory(SCMContextProvider contextProvider ,RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(contextProvider, repositoryLocationResolver, Store.BLOB);
this.keyGenerator = keyGenerator;
}
@Override
@SuppressWarnings("unchecked")
public BlobStore getStore(StoreParameters storeParameters) {
if (storeParameters.getRepository() != null) {
return new FileBlobStore(keyGenerator, getDirectory(storeParameters.getName(), storeParameters.getRepository()));
}
return new FileBlobStore(keyGenerator, getDirectory(storeParameters.getName()));
File storeLocation = getStoreLocation(storeParameters);
IOUtil.mkdirs(storeLocation);
return new FileBlobStore(keyGenerator, storeLocation);
}
}

View File

@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* <p>
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 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.
* 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.
*
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
* <p>
* 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
@@ -24,109 +24,43 @@
* 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.
*
* <p>
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class JAXBConfigurationEntryStoreFactory
implements ConfigurationEntryStoreFactory
{
public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
implements ConfigurationEntryStoreFactory {
/**
* the logger for JAXBConfigurationEntryStoreFactory
*/
private static final Logger logger =
LoggerFactory.getLogger(JAXBConfigurationEntryStoreFactory.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param keyGenerator
* @param context
*/
@Inject
public JAXBConfigurationEntryStoreFactory(KeyGenerator keyGenerator,
SCMContextProvider context)
{
this.keyGenerator = keyGenerator;
directory = new File(context.getBaseDirectory(),
StoreConstants.CONFIG_DIRECTORY_NAME);
IOUtil.mkdirs(directory);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param type
* @param name
*
* @return
*/
private ConfigurationEntryStore getStore(Class type, String name)
{
logger.debug("create new configuration store for type {} with name {}",
type, name);
//J-
return new JAXBConfigurationEntryStore(
new File(directory,name.concat(StoreConstants.FILE_EXTENSION)),
keyGenerator,
type
);
//J+
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private File directory;
/** Field description */
private KeyGenerator keyGenerator;
@Override
public ConfigurationEntryStore getStore(StoreParameters storeParameters) {
if (storeParameters.getRepository() != null){
return getStore(storeParameters.getType(),storeParameters.getName(),storeParameters.getRepository());
}
return getStore(storeParameters.getType(),storeParameters.getName());
@Inject
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(contextProvider, repositoryLocationResolver, Store.CONFIG);
this.keyGenerator = keyGenerator;
}
private ConfigurationEntryStore getStore(Class type, String name, Repository repository) {
return null;
@Override
@SuppressWarnings("unchecked")
public ConfigurationEntryStore getStore(StoreParameters storeParameters) {
return new JAXBConfigurationEntryStore(getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()), keyGenerator, storeParameters.getType());
}
}

View File

@@ -32,15 +32,8 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
import java.io.File;
import java.io.IOException;
/**
* JAXB implementation of {@link StoreFactory}.
@@ -48,14 +41,7 @@ import java.io.IOException;
* @author Sebastian Sdorra
*/
@Singleton
public class JAXBConfigurationStoreFactory implements ConfigurationStoreFactory {
/**
* the logger for JAXBConfigurationStoreFactory
*/
private static final Logger LOG = LoggerFactory.getLogger(JAXBConfigurationStoreFactory.class);
private RepositoryLocationResolver repositoryLocationResolver;
public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory implements ConfigurationStoreFactory {
/**
* Constructs a new instance.
@@ -63,57 +49,13 @@ public class JAXBConfigurationStoreFactory implements ConfigurationStoreFactory
* @param repositoryLocationResolver Resolver to get the repository Directory
*/
@Inject
public JAXBConfigurationStoreFactory(RepositoryLocationResolver repositoryLocationResolver) {
this.repositoryLocationResolver = repositoryLocationResolver;
}
/**
* Get or create the global config directory.
*
* @return the global config directory.
*/
private File getGlobalConfigDirectory() {
File baseDirectory = repositoryLocationResolver.getInitialBaseDirectory();
File configDirectory = new File(baseDirectory, StoreConstants.CONFIG_DIRECTORY_NAME);
return getOrCreateFile(configDirectory);
}
/**
* Get or create the repository specific config directory.
*
* @return the repository specific config directory.
*/
private File getRepositoryConfigDirectory(Repository repository) {
File baseDirectory = null;
try {
baseDirectory = repositoryLocationResolver.getConfigDirectory(repository);
} catch (IOException e) {
e.printStackTrace();
}
File configDirectory = new File(baseDirectory, StoreConstants.CONFIG_DIRECTORY_NAME);
return getOrCreateFile(configDirectory);
}
private File getOrCreateFile(File directory) {
if (!directory.exists()) {
IOUtil.mkdirs(directory);
}
return directory;
}
private JAXBConfigurationStore getStore(Class type, String name, File configDirectory) {
File configFile = new File(configDirectory, name.concat(StoreConstants.FILE_EXTENSION));
LOG.debug("create store for {} at {}", type.getName(), configFile.getPath());
return new JAXBConfigurationStore<>(type, configFile);
public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) {
super(contextProvider, repositoryLocationResolver, Store.CONFIG);
}
@Override
@SuppressWarnings("unchecked")
public JAXBConfigurationStore getStore(StoreParameters storeParameters) {
try {
return getStore(storeParameters.getType(), storeParameters.getName(),repositoryLocationResolver.getRepositoryDirectory(storeParameters.getRepository()));
} catch (IOException e) {
throw new InternalRepositoryException(storeParameters.getRepository(),"Error on getting the store of the repository"+ storeParameters.getRepository().getNamespaceAndName());
}
return new JAXBConfigurationStore(storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()));
}
}

View File

@@ -40,8 +40,12 @@ import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
import sonia.scm.util.IOUtil;
import java.io.File;
/**
*
@@ -49,26 +53,22 @@ import sonia.scm.security.KeyGenerator;
*/
@Singleton
public class JAXBDataStoreFactory extends FileBasedStoreFactory
implements DataStoreFactory
{
implements DataStoreFactory {
private static final Logger logger = LoggerFactory.getLogger(JAXBDataStoreFactory.class);
private static final String DIRECTORY_NAME = "data";
private KeyGenerator keyGenerator;
@Inject
public JAXBDataStoreFactory(RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(repositoryLocationResolver, DIRECTORY_NAME);
public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(contextProvider, repositoryLocationResolver, Store.DATA);
this.keyGenerator = keyGenerator;
}
@Override
@SuppressWarnings("unchecked")
public DataStore getStore(StoreParameters storeParameters) {
logger.debug("create new store for type {} with name {}", storeParameters.getType(), storeParameters.getName());
if (storeParameters.getRepository() != null) {
return new JAXBDataStore(keyGenerator, storeParameters.getType(), getDirectory(storeParameters.getName(), storeParameters.getRepository()));
}
return new JAXBDataStore(keyGenerator, storeParameters.getType(), getDirectory(storeParameters.getName()));
File storeLocation = getStoreLocation(storeParameters);
IOUtil.mkdirs(storeLocation);
return new JAXBDataStore(keyGenerator, storeParameters.getType(), storeLocation);
}
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.store;
import java.io.File;
public enum Store {
CONFIG("config"),
DATA("data"),
BLOB("blob");
private static final String GLOBAL_STORE_BASE_DIRECTORY = "var";
private String directory;
Store(String directory) {
this.directory = directory;
}
/**
* Get the relkative store directory path to be stored in the repository root
* <p>
* The repository store directories are:
* repo_base_dir/config/
* repo_base_dir/blob/
* repo_base_dir/data/
*
* @return the relative store directory path to be stored in the repository root
*/
public String getRepositoryStoreDirectory() {
return directory;
}
/**
* Get the relative store directory path to be stored in the global root
* <p>
* The global store directories are:
* base_dir/config/
* base_dir/var/blob/
* base_dir/var/data/
*
* @return the relative store directory path to be stored in the global root
*/
public String getGlobalStoreDirectory() {
if (this.equals(CONFIG)) {
return directory;
}
return GLOBAL_STORE_BASE_DIRECTORY + File.separator + directory;
}
}

View File

@@ -63,7 +63,7 @@ public abstract class AbstractXmlDAO<I extends ModelObject,
* the logger for XmlGroupDAO
*/
private static final Logger logger =
LoggerFactory.getLogger(XmlGroupDAO.class);
LoggerFactory.getLogger(AbstractXmlDAO.class);
//~--- constructors ---------------------------------------------------------

View File

@@ -0,0 +1,111 @@
package sonia.scm.repository.xml;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.Repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.StoreParameters;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.codehaus.groovy.runtime.InvokerHelper.asList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.xml.XmlRepositoryDAO.STORE_NAME;
@RunWith(MockitoJUnitRunner.class)
@Ignore
public class XmlRepositoryDAOTest {
@Mock
private ConfigurationStoreFactory storeFactory;
@Mock
private ConfigurationStore<XmlRepositoryDatabase> store;
@Mock
private XmlRepositoryDatabase db;
@Mock
private SCMContextProvider context;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final FileSystem fileSystem = new DefaultFileSystem();
@Before
public void init() throws IOException {
StoreParameters storeParameters = new StoreParameters().withType(XmlRepositoryDatabase.class).withName(STORE_NAME).build();
when(storeFactory.getStore(storeParameters)).thenReturn(store);
when(store.get()).thenReturn(db);
when(context.getBaseDirectory()).thenReturn(temporaryFolder.newFolder());
}
@Test
public void addShouldCreateNewRepositoryPathWithRelativePath() {
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(context);
XmlRepositoryDAO dao = new XmlRepositoryDAO(initialRepositoryLocationResolver, fileSystem, context);
dao.add(new Repository("id", "git", "namespace", "name"));
verify(db).add(argThat(repositoryPath -> {
assertThat(repositoryPath.getId()).isEqualTo("id");
assertThat(repositoryPath.getPath()).isEqualTo(InitialRepositoryLocationResolver.DEFAULT_REPOSITORY_PATH + "/id");
return true;
}));
verify(store).set(db);
}
@Test
public void modifyShouldStoreChangedRepository() {
Repository oldRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("/path", "id", oldRepository);
when(db.values()).thenReturn(asList(repositoryPath));
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
Repository newRepository = new Repository("id", "new", null, null);
dao.modify(newRepository);
assertThat(repositoryPath.getRepository()).isSameAs(newRepository);
verify(store).set(db);
}
@Test
public void shouldGetPathInBaseDirectoryForRelativePath() {
Repository existingRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("path", "id", existingRepository);
when(db.values()).thenReturn(asList(repositoryPath));
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
Path path = dao.getPath(existingRepository);
assertThat(path.toString()).isEqualTo(context.getBaseDirectory().getPath() + "/path");
}
@Test
public void shouldGetPathInBaseDirectoryForAbsolutePath() {
Repository existingRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("/tmp/path", "id", existingRepository);
when(db.values()).thenReturn(asList(repositoryPath));
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
Path path = dao.getPath(existingRepository);
assertThat(path.toString()).isEqualTo("/tmp/path");
}
}

View File

@@ -34,8 +34,15 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
import sonia.scm.repository.Repository;
import sonia.scm.security.UUIDKeyGenerator;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
/**
*
* @author Sebastian Sdorra
@@ -52,6 +59,25 @@ public class FileBlobStoreTest extends BlobStoreTestBase
@Override
protected BlobStoreFactory createBlobStoreFactory()
{
return new FileBlobStoreFactory(repositoryLocationResolver, new UUIDKeyGenerator());
return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
}
@Test
@SuppressWarnings("unchecked")
public void shouldStoreAndLoadInRepository() {
BlobStore store = createBlobStoreFactory().getStore(new StoreParameters()
.withType(StoreObject.class)
.withName("test")
.forRepository(new Repository("id", "git", "ns", "n"))
.build());
Blob createdBlob = store.create("abc");
List<Blob> storedBlobs = store.getAll();
assertNotNull(createdBlob);
assertThat(storedBlobs)
.isNotNull()
.hasSize(1)
.usingElementComparatorOnFields("id").containsExactly(createdBlob);
}
}

View File

@@ -144,6 +144,16 @@ public class JAXBConfigurationEntryStoreTest
assertEquals("repository:create", ap.getPermission());
}
@Test
public void shouldStoreAndLoadInRepository() throws IOException
{
repoStore.put("abc", new StoreObject("abc_value"));
StoreObject storeObject = repoStore.get("abc");
assertNotNull(storeObject);
assertEquals("abc_value", storeObject.getValue());
}
/**
* Method description
*
@@ -153,7 +163,7 @@ public class JAXBConfigurationEntryStoreTest
@Override
protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
{
return new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(), contextProvider);
return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
}
/**

View File

@@ -32,6 +32,14 @@
package sonia.scm.store;
import org.junit.Test;
import sonia.scm.repository.Repository;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Unit tests for {@link JAXBConfigurationStore}.
*
@@ -42,6 +50,24 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
@Override
protected ConfigurationStoreFactory createStoreFactory()
{
return new JAXBConfigurationStoreFactory(repositoryLocationResolver);
return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver);
}
@Test
@SuppressWarnings("unchecked")
public void shouldStoreAndLoadInRepository() throws IOException
{
ConfigurationStore<StoreObject> store = createStoreFactory().getStore(new StoreParameters()
.withType(StoreObject.class)
.withName("test")
.forRepository(new Repository("id", "git", "ns", "n"))
.build());
store.set(new StoreObject("value"));
StoreObject storeObject = store.get();
assertNotNull(storeObject);
assertEquals("value", storeObject.getValue());
}
}

View File

@@ -34,9 +34,15 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
import sonia.scm.repository.Repository;
import sonia.scm.security.UUIDKeyGenerator;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
*
* @author Sebastian Sdorra
@@ -52,7 +58,7 @@ public class JAXBDataStoreTest extends DataStoreTestBase {
@Override
protected DataStoreFactory createDataStoreFactory()
{
return new JAXBDataStoreFactory(repositoryLocationResolver, new UUIDKeyGenerator());
return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
}
@Override
@@ -73,4 +79,14 @@ public class JAXBDataStoreTest extends DataStoreTestBase {
.build();
return createDataStoreFactory().getStore(params);
}
@Test
public void shouldStoreAndLoadInRepository() throws IOException
{
repoStore.put("abc", new StoreObject("abc_value"));
StoreObject storeObject = repoStore.get("abc");
assertNotNull(storeObject);
assertEquals("abc_value", storeObject.getValue());
}
}

View File

@@ -136,7 +136,7 @@ public class ScmTransportProtocol extends TransportProtocol
*/
@Override
public Transport open(URIish uri, Repository local, String remoteName)
throws NotSupportedException, TransportException
throws TransportException
{
File localDirectory = local.getDirectory();
File path = local.getFS().resolve(localDirectory, uri.getPath());

View File

@@ -38,6 +38,7 @@ package sonia.scm.repository;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -88,6 +89,8 @@ public class GitRepositoryHandler
GitRepositoryServiceProvider.COMMANDS);
private static final Object LOCK = new Object();
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
private final Scheduler scheduler;
@@ -97,19 +100,13 @@ public class GitRepositoryHandler
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param storeFactory
* @param fileSystem
* @param scheduler
* @param repositoryLocationResolver
*/
@Inject
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, GitWorkdirFactory workdirFactory)
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory,
Scheduler scheduler,
RepositoryLocationResolver repositoryLocationResolver,
GitWorkdirFactory workdirFactory)
{
super(storeFactory, fileSystem, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver);
this.scheduler = scheduler;
this.workdirFactory = workdirFactory;
}
@@ -182,12 +179,23 @@ public class GitRepositoryHandler
return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
}
public GitWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
public String getRepositoryId(StoredConfig gitConfig) {
return gitConfig.getString(GitRepositoryHandler.CONFIG_SECTION_SCMM, null, GitRepositoryHandler.CONFIG_KEY_REPOSITORY_ID);
}
//~--- methods --------------------------------------------------------------
@Override
protected void create(Repository repository, File directory) throws IOException {
try (org.eclipse.jgit.lib.Repository gitRepository = build(directory)) {
gitRepository.create(true);
StoredConfig config = gitRepository.getConfig();
config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId());
config.save();
}
}
@@ -224,22 +232,4 @@ public class GitRepositoryHandler
{
return GitConfig.class;
}
/**
* Method description
*
*
* @param directory
*
* @return
*/
@Override
protected boolean isRepository(File directory)
{
return new File(directory, DIRECTORY_REFS).exists();
}
public GitWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
}

View File

@@ -36,6 +36,7 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.transport.PostReceiveHook;
import org.eclipse.jgit.transport.PreReceiveHook;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -44,11 +45,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryUtil;
import sonia.scm.repository.spi.GitHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@@ -128,14 +127,14 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
try
{
Repository repository = rpack.getRepository();
String id = resolveRepositoryId(repository);
String repositoryId = resolveRepositoryId(repository);
logger.trace("resolved repository to id {}", id);
logger.trace("resolved repository to {}", repositoryId);
GitHookContextProvider context = new GitHookContextProvider(rpack,
receiveCommands);
hookEventFacade.handle(id).fireHookEvent(type, context);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
}
catch (Exception ex)
@@ -187,20 +186,10 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook
*
* @throws IOException
*/
private String resolveRepositoryId(Repository repository) throws IOException
private String resolveRepositoryId(Repository repository)
{
File directory;
if (repository.isBare())
{
directory = repository.getDirectory();
}
else
{
directory = repository.getWorkTree();
}
return RepositoryUtil.getRepositoryId(handler, directory);
StoredConfig gitConfig = repository.getConfig();
return handler.getRepositoryId(gitConfig);
}
//~--- fields ---------------------------------------------------------------

View File

@@ -33,29 +33,19 @@ class GitConfigurationForm extends React.Component<Props, State> {
this.state = { ...props.initialConfiguration };
}
isValid = () => {
return !!this.state.repositoryDirectory;
};
handleChange = (value: any, name: string) => {
this.setState({
[name]: value
}, () => this.props.onConfigurationChange(this.state, this.isValid()));
}, () => this.props.onConfigurationChange(this.state, true));
};
render() {
const { repositoryDirectory, gcExpression, disabled } = this.state;
const { gcExpression, disabled } = this.state;
const { readOnly, t } = this.props;
return (
<>
<InputField name="repositoryDirectory"
label={t("scm-git-plugin.config.directory")}
helpText={t("scm-git-plugin.config.directoryHelpText")}
value={repositoryDirectory}
onChange={this.handleChange}
disabled={readOnly}
/>
<InputField name="gcExpression"
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
import { Title, Configuration } from "@scm-manager/ui-components";
import GitConfigurationForm from "./GitConfigurationForm";
type Props = {
@@ -22,7 +22,7 @@ class GitGlobalConfiguration extends React.Component<Props> {
return (
<div>
<Title title={t("scm-git-plugin.config.title")}/>
<GlobalConfiguration link={link} render={props => <GitConfigurationForm {...props} />}/>
<Configuration link={link} render={props => <GitConfigurationForm {...props} />}/>
</div>
);
}

View File

@@ -8,8 +8,6 @@
"config": {
"link": "Git",
"title": "Git Configuration",
"directory": "Repository Directory",
"directoryHelpText": "Location of the Git repositories.",
"gcExpression": "GC Cron Expression",
"gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.",
"disabled": "Disabled",

View File

@@ -37,26 +37,17 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.schedule.Scheduler;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.StoreFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@@ -71,8 +62,8 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private GitWorkdirFactory gitWorkdirFactory;
RepositoryLocationResolver repositoryLocationResolver ;
private Path repoDir;
RepositoryLocationResolver repositoryLocationResolver;
@Override
protected void checkDirectory(File directory) {
@@ -95,16 +86,10 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
File directory) throws RepositoryPathNotFoundException {
DefaultFileSystem fileSystem = new DefaultFileSystem();
PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class);
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider,fileSystem);
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, initialRepositoryLocationResolver);
File directory) {
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider));
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
fileSystem, scheduler, repositoryLocationResolver, gitWorkdirFactory);
repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
scheduler, repositoryLocationResolver, gitWorkdirFactory);
repositoryHandler.init(contextProvider);
GitConfig config = new GitConfig();
@@ -116,18 +101,17 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
}
@Test
public void getDirectory() {
public void getDirectory() {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
new DefaultFileSystem(), scheduler, repositoryLocationResolver, gitWorkdirFactory);
Repository repository = new Repository("id", "git", "Space", "Name");
scheduler, repositoryLocationResolver, gitWorkdirFactory);
GitConfig config = new GitConfig();
config.setDisabled(false);
config.setGcExpression("gc exp");
repositoryHandler.setConfig(config);
initRepository();
File path = repositoryHandler.getDirectory(repository);
assertEquals(repoDir.toString()+File.separator+InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

View File

@@ -94,12 +94,7 @@ public class HgImportHandler extends AbstactImportHandler
INIConfiguration c = reader.read(hgrc);
INISection web = c.getSection("web");
if (web == null)
{
handler.appendWebSection(c);
}
else
{
if (web != null) {
repository.setDescription(web.getParameter("description"));
String contact = web.getParameter("contact");
@@ -112,16 +107,7 @@ public class HgImportHandler extends AbstactImportHandler
{
logger.warn("contact {} is not a valid mail address", contact);
}
handler.setWebParameter(web);
}
// issue-97
handler.registerMissingHook(c, repositoryName);
INIConfigurationWriter writer = new INIConfigurationWriter();
writer.write(c, hgrc);
}
else
{

View File

@@ -41,12 +41,11 @@ import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException;
import sonia.scm.ContextEntry;
import sonia.scm.SCMContextProvider;
import sonia.scm.installer.HgInstaller;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.io.DirectoryFileFilter;
import sonia.scm.io.ExtendedCommand;
import sonia.scm.io.FileSystem;
import sonia.scm.io.INIConfiguration;
import sonia.scm.io.INIConfigurationReader;
import sonia.scm.io.INIConfigurationWriter;
@@ -56,7 +55,6 @@ import sonia.scm.repository.spi.HgRepositoryServiceProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.util.SystemUtil;
import sonia.scm.util.Util;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@@ -105,22 +103,17 @@ public class HgRepositoryHandler
/** Field description */
public static final String PATH_HGRC =
".hg".concat(File.separator).concat("hgrc");
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
* @param storeFactory
* @param fileSystem
* @param hgContextProvider
* @param repositoryLocationResolver
*/
@Inject
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem,
Provider<HgContext> hgContextProvider, RepositoryLocationResolver repositoryLocationResolver)
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
Provider<HgContext> hgContextProvider,
RepositoryLocationResolver repositoryLocationResolver)
{
super(storeFactory, fileSystem, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver);
this.hgContextProvider = hgContextProvider;
try
@@ -179,7 +172,6 @@ public class HgRepositoryHandler
public void init(SCMContextProvider context)
{
super.init(context);
registerMissingHooks();
writePythonScripts(context);
// fix wrong hg.bat from package installation
@@ -299,100 +291,6 @@ public class HgRepositoryHandler
return version;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param hgrc
*/
void appendHookSection(INIConfiguration hgrc)
{
INISection hooksSection = new INISection("hooks");
setHookParameter(hooksSection);
hgrc.addSection(hooksSection);
}
/**
* Method description
*
*
* @param hgrc
*/
void appendWebSection(INIConfiguration hgrc)
{
INISection webSection = new INISection("web");
setWebParameter(webSection);
hgrc.addSection(webSection);
}
/**
* Method description
*
*
* @param c
* @param repositoryName
*
* @return
*/
boolean registerMissingHook(INIConfiguration c, String repositoryName)
{
INISection hooks = c.getSection("hooks");
if (hooks == null)
{
hooks = new INISection("hooks");
c.addSection(hooks);
}
boolean write = false;
if (appendHook(repositoryName, hooks, "changegroup.scm"))
{
write = true;
}
if (appendHook(repositoryName, hooks, "pretxnchangegroup.scm"))
{
write = true;
}
return write;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param hooksSection
*/
void setHookParameter(INISection hooksSection)
{
hooksSection.setParameter("changegroup.scm", "python:scmhooks.callback");
hooksSection.setParameter("pretxnchangegroup.scm",
"python:scmhooks.callback");
}
/**
* Method description
*
*
* @param webSection
*/
void setWebParameter(INISection webSection)
{
webSection.setParameter("push_ssl", "false");
webSection.setParameter("allow_read", "*");
webSection.setParameter("allow_push", "*");
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
@@ -434,16 +332,25 @@ public class HgRepositoryHandler
File hgrcFile = new File(directory, PATH_HGRC);
INIConfiguration hgrc = new INIConfiguration();
appendWebSection(hgrc);
// register hooks
appendHookSection(hgrc);
INISection iniSection = new INISection(CONFIG_SECTION_SCMM);
iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId());
INIConfiguration iniConfiguration = new INIConfiguration();
iniConfiguration.addSection(iniSection);
hgrc.addSection(iniSection);
INIConfigurationWriter writer = new INIConfigurationWriter();
writer.write(hgrc, hgrcFile);
}
public String getRepositoryId(File directory) {
try {
return new INIConfigurationReader().read(new File(directory, PATH_HGRC)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID);
} catch (IOException e) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("directory", directory.toString()), "could not read scm configuration file", e);
}
}
//~--- get methods ----------------------------------------------------------
/**
@@ -460,37 +367,6 @@ public class HgRepositoryHandler
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param repositoryName
* @param hooks
* @param hookName
*
* @return
*/
private boolean appendHook(String repositoryName, INISection hooks,
String hookName)
{
boolean write = false;
String hook = hooks.getParameter(hookName);
if (Util.isEmpty(hook))
{
if (logger.isInfoEnabled())
{
logger.info("register missing {} hook for respository {}", hookName,
repositoryName);
}
hooks.setParameter(hookName, "python:scmhooks.callback");
write = true;
}
return write;
}
/**
* Method description
*
@@ -512,104 +388,6 @@ public class HgRepositoryHandler
}
}
/**
* Method description
*
*
* @param repositoryDir
*
* @return
*/
private boolean registerMissingHook(File repositoryDir)
{
boolean result = false;
File hgrc = new File(repositoryDir, PATH_HGRC);
if (hgrc.exists())
{
try
{
INIConfigurationReader reader = new INIConfigurationReader();
INIConfiguration c = reader.read(hgrc);
String repositoryName = repositoryDir.getName();
if (registerMissingHook(c, repositoryName))
{
if (logger.isDebugEnabled())
{
logger.debug("rewrite hgrc for repository {}", repositoryName);
}
INIConfigurationWriter writer = new INIConfigurationWriter();
writer.write(c, hgrc);
}
result = true;
}
catch (IOException ex)
{
logger.error("could not register missing hook", ex);
}
}
return result;
}
/**
* Method description
*
*/
private void registerMissingHooks()
{
HgConfig c = getConfig();
if (c != null)
{
File repositoryDirectroy = getInitialBaseDirectory();
if (repositoryDirectroy.exists())
{
File lockFile = new File(repositoryDirectroy, PATH_HOOK);
if (!lockFile.exists())
{
File[] dirs =
repositoryDirectroy.listFiles(DirectoryFileFilter.instance);
boolean success = true;
if (Util.isNotEmpty(dirs))
{
for (File dir : dirs)
{
if (!registerMissingHook(dir))
{
success = false;
}
}
}
if (success)
{
createNewFile(lockFile);
}
}
else if (logger.isDebugEnabled())
{
logger.debug("hooks allready registered");
}
}
else if (logger.isDebugEnabled())
{
logger.debug(
"repository directory does not exists, could not register missing hooks");
}
}
else if (logger.isDebugEnabled())
{
logger.debug("config is not available, could not register missing hooks");
}
}
/**
* Method description
*

View File

@@ -62,11 +62,11 @@ public class HgHookChangesetProvider implements HookChangesetProvider
//~--- constructors ---------------------------------------------------------
public HgHookChangesetProvider(HgRepositoryHandler handler,
String id, HgHookManager hookManager, String startRev,
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
this.handler = handler;
this.id = id;
this.repositoryDirectory = repositoryDirectory;
this.hookManager = hookManager;
this.startRev = startRev;
this.type = type;
@@ -123,10 +123,6 @@ public class HgHookChangesetProvider implements HookChangesetProvider
*/
private Repository open()
{
sonia.scm.repository.Repository repo = new sonia.scm.repository.Repository();
repo.setId(id);
File repositoryDirectory = handler.getDirectory(repo);
// use HG_PENDING only for pre receive hooks
boolean pending = type == RepositoryHookType.PRE_RECEIVE;
@@ -144,7 +140,7 @@ public class HgHookChangesetProvider implements HookChangesetProvider
private HgHookManager hookManager;
/** Field description */
private String id;
private File repositoryDirectory;
/** Field description */
private HookChangesetResponse response;

View File

@@ -44,6 +44,7 @@ import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;
import java.io.File;
import java.util.EnumSet;
import java.util.Set;
@@ -67,16 +68,16 @@ public class HgHookContextProvider extends HookContextProvider
* Constructs a new instance.
*
* @param handler mercurial repository handler
* @param namespaceAndName namespace and name of changed repository
* @param repositoryDirectory the directory of the changed repository
* @param hookManager mercurial hook manager
* @param startRev start revision
* @param type type of hook
*/
public HgHookContextProvider(HgRepositoryHandler handler,
String id, HgHookManager hookManager, String startRev,
RepositoryHookType type)
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
this.hookChangesetProvider = new HgHookChangesetProvider(handler, id, hookManager, startRev, type);
this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type);
}
//~--- get methods ----------------------------------------------------------

View File

@@ -44,12 +44,13 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryUtil;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider;
@@ -63,6 +64,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@@ -113,20 +115,10 @@ public class HgHookCallbackServlet extends HttpServlet
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param hookEventFacade
* @param handler
* @param hookManager
* @param contextProvider
*/
@Inject
public HgHookCallbackServlet(HookEventFacade hookEventFacade,
HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider)
HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider)
{
this.hookEventFacade = hookEventFacade;
this.handler = handler;
@@ -148,7 +140,6 @@ public class HgHookCallbackServlet extends HttpServlet
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String ping = request.getParameter(PARAM_PING);
@@ -179,7 +170,7 @@ public class HgHookCallbackServlet extends HttpServlet
if (m.matches())
{
String id = getRepositoryId(request);
File repositoryPath = getRepositoryPath(request);
String type = m.group(1);
String challenge = request.getParameter(PARAM_CHALLENGE);
@@ -196,7 +187,7 @@ public class HgHookCallbackServlet extends HttpServlet
authenticate(request, credentials);
}
hookCallback(response, id, type, challenge, node);
hookCallback(response, repositoryPath, type, challenge, node);
}
else if (logger.isDebugEnabled())
{
@@ -255,8 +246,7 @@ public class HgHookCallbackServlet extends HttpServlet
}
}
private void fireHook(HttpServletResponse response, String id,
String node, RepositoryHookType type)
private void fireHook(HttpServletResponse response, File repositoryDirectory, String node, RepositoryHookType type)
throws IOException
{
HgHookContextProvider context = null;
@@ -268,10 +258,11 @@ public class HgHookCallbackServlet extends HttpServlet
contextProvider.get().setPending(true);
}
context = new HgHookContextProvider(handler, id, hookManager,
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
node, type);
hookEventFacade.handle(id).fireHookEvent(type, context);
String repositoryId = getRepositoryId(repositoryDirectory);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
printMessages(response, context);
}
@@ -289,7 +280,7 @@ public class HgHookCallbackServlet extends HttpServlet
}
}
private void hookCallback(HttpServletResponse response, String id, String typeName, String challenge, String node) throws IOException {
private void hookCallback(HttpServletResponse response, File repositoryDirectory, String typeName, String challenge, String node) throws IOException {
if (hookManager.isAcceptAble(challenge))
{
RepositoryHookType type = null;
@@ -305,7 +296,7 @@ public class HgHookCallbackServlet extends HttpServlet
if (type != null)
{
fireHook(response, id, node, type);
fireHook(response, repositoryDirectory, node, type);
}
else
{
@@ -450,41 +441,21 @@ public class HgHookCallbackServlet extends HttpServlet
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
private String getRepositoryId(HttpServletRequest request)
@SuppressWarnings("squid:S2083") // we do nothing with the path given, so this should be no issue
private String getRepositoryId(File repositoryPath)
{
String id = null;
return handler.getRepositoryId(repositoryPath);
}
private File getRepositoryPath(HttpServletRequest request) {
String path = request.getParameter(PARAM_REPOSITORYPATH);
if (Util.isNotEmpty(path))
{
/**
* use canonical path to fix symbolic links
* https://bitbucket.org/sdorra/scm-manager/issue/82/symbolic-link-in-hg-repository-path
*/
try
{
id = RepositoryUtil.getRepositoryId(handler, path);
}
catch (IOException ex)
{
logger.error("could not find namespace and name of repository", ex);
}
if (Util.isNotEmpty(path)) {
return new File(path);
}
else if (logger.isWarnEnabled())
else
{
logger.warn("no repository path parameter found");
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("directory", path), "could not find hgrc in directory");
}
return id;
}
//~--- fields ---------------------------------------------------------------

View File

@@ -8,7 +8,6 @@ type Configuration = {
"hgBinary": string,
"pythonBinary": string,
"pythonPath"?: string,
"repositoryDirectory": string,
"encoding": string,
"useOptimizedBytecode": boolean,
"showRevisionInId": boolean,
@@ -39,7 +38,7 @@ class HgConfigurationForm extends React.Component<Props, State> {
updateValidationStatus = () => {
const requiredFields = [
"hgBinary", "pythonBinary", "repositoryDirectory", "encoding"
"hgBinary", "pythonBinary", "encoding"
];
const validationErrors = [];
@@ -99,7 +98,6 @@ class HgConfigurationForm extends React.Component<Props, State> {
{this.inputField("hgBinary")}
{this.inputField("pythonBinary")}
{this.inputField("pythonPath")}
{this.inputField("repositoryDirectory")}
{this.inputField("encoding")}
{this.checkbox("useOptimizedBytecode")}
{this.checkbox("showRevisionInId")}

View File

@@ -1,6 +1,6 @@
//@flow
import React from "react";
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
import { Title, Configuration } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import HgConfigurationForm from "./HgConfigurationForm";
@@ -18,7 +18,7 @@ class HgGlobalConfiguration extends React.Component<Props> {
return (
<div>
<Title title={t("scm-hg-plugin.config.title")}/>
<GlobalConfiguration link={link} render={props => <HgConfigurationForm {...props} />}/>
<Configuration link={link} render={props => <HgConfigurationForm {...props} />}/>
</div>
);
}

View File

@@ -14,8 +14,6 @@
"pythonBinaryHelpText": "Location of Python binary.",
"pythonPath": "Python Module Search Path",
"pythonPathHelpText": "Python Module Search Path (PYTHONPATH).",
"repositoryDirectory": "Repository directory",
"repositoryDirectoryHelpText": "Location of Mercurial repositories.",
"encoding": "Encoding",
"encodingHelpText": "Repository Encoding.",
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",

View File

@@ -31,12 +31,21 @@
import os
from mercurial import demandimport
from mercurial import demandimport, ui as uimod, hg
from mercurial.hgweb import hgweb, wsgicgi
repositoryPath = os.environ['SCM_REPOSITORY_PATH']
demandimport.enable()
application = hgweb(repositoryPath)
u = uimod.ui()
u.setconfig('web', 'push_ssl', 'false')
u.setconfig('web', 'allow_read', '*')
u.setconfig('web', 'allow_push', '*')
u.setconfig('hooks', 'changegroup.scm', 'python:scmhooks.callback')
u.setconfig('hooks', 'pretxnchangegroup.scm', 'python:scmhooks.callback')
r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH'])
application = hgweb(r)
wsgicgi.launch(application)

View File

@@ -38,25 +38,16 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.StoreFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@@ -68,8 +59,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private com.google.inject.Provider<HgContext> provider;
RepositoryLocationResolver repositoryLocationResolver ;
private Path repoDir;
private RepositoryLocationResolver repositoryLocationResolver;
@Override
protected void checkDirectory(File directory) {
@@ -77,27 +67,16 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
assertTrue(hgDirectory.exists());
assertTrue(hgDirectory.isDirectory());
File hgrc = new File(hgDirectory, "hgrc");
assertTrue(hgrc.exists());
assertTrue(hgrc.isFile());
assertTrue(hgrc.length() > 0);
}
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
File directory) throws RepositoryPathNotFoundException {
DefaultFileSystem fileSystem = new DefaultFileSystem();
PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class);
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider,fileSystem));
File directory) {
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider));
HgRepositoryHandler handler = new HgRepositoryHandler(factory,
new DefaultFileSystem(),
new HgContextProvider(), repositoryLocationResolver);
handler.init(contextProvider);
repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
HgTestUtil.checkForSkip(handler);
return handler;
@@ -106,15 +85,15 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory,
new DefaultFileSystem(), provider, repositoryLocationResolver);
provider, repositoryLocationResolver);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");
hgConfig.setPythonBinary("python");
repositoryHandler.setConfig(hgConfig);
Repository repository = new Repository("id", "git", "Space", "Name");
initRepository();
File path = repositoryHandler.getDirectory(repository);
assertEquals(repoDir.toString()+File.separator+InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString() + File.separator + AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

View File

@@ -36,19 +36,18 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Assume;
import sonia.scm.SCMContext;
import sonia.scm.io.FileSystem;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.nio.file.Path;
import javax.servlet.http.HttpServletRequest;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -96,19 +95,17 @@ public final class HgTestUtil
*
* @return
*/
public static HgRepositoryHandler createHandler(File directory) throws RepositoryPathNotFoundException {
public static HgRepositoryHandler createHandler(File directory) {
TempSCMContextProvider context =
(TempSCMContextProvider) SCMContext.getContext();
context.setBaseDirectory(directory);
FileSystem fileSystem = mock(FileSystem.class);
PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class);
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(context,fileSystem));
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(context));
HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), fileSystem,
new HgContextProvider(), repositoryLocationResolver);
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver);
Path repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
handler.init(context);

View File

@@ -1,13 +1,11 @@
package sonia.scm.web;
import org.junit.Test;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import static org.mockito.Matchers.anyInt;
@@ -31,10 +29,6 @@ public class HgHookCallbackServletTest {
String path = "/tmp/hg/12345";
when(request.getParameter(PARAM_REPOSITORYPATH)).thenReturn(path);
File file = new File(path);
when(handler.getInitialBaseDirectory()).thenReturn(file);
servlet.doPost(request, response);
verify(response, never()).sendError(anyInt());

View File

@@ -46,7 +46,11 @@ import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.util.SVNDebugLog;
import sonia.scm.io.FileSystem;
import sonia.scm.ContextEntry;
import sonia.scm.io.INIConfiguration;
import sonia.scm.io.INIConfigurationReader;
import sonia.scm.io.INIConfigurationWriter;
import sonia.scm.io.INISection;
import sonia.scm.logging.SVNKitLogger;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.spi.HookEventFacade;
@@ -56,6 +60,7 @@ import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.Util;
import java.io.File;
import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -82,15 +87,19 @@ public class SvnRepositoryHandler
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME,
TYPE_DISPLAYNAME,
SvnRepositoryServiceProvider.COMMANDS);
private static final String CONFIG_FILE_NAME = "scm-manager.conf";
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
private static final Logger logger =
LoggerFactory.getLogger(SvnRepositoryHandler.class);
@Inject
public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem,
HookEventFacade eventFacade, RepositoryLocationResolver repositoryLocationResolver)
public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory,
HookEventFacade eventFacade,
RepositoryLocationResolver repositoryLocationResolver)
{
super(storeFactory, fileSystem, repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver);
// register logger
SVNDebugLog.setDefaultLog(new SVNKitLogger());
@@ -210,4 +219,21 @@ public class SvnRepositoryHandler
{
return SvnConfig.class;
}
@Override
protected void postCreate(Repository repository, File directory) throws IOException {
INISection iniSection = new INISection(CONFIG_SECTION_SCMM);
iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId());
INIConfiguration iniConfiguration = new INIConfiguration();
iniConfiguration.addSection(iniSection);
new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME));
}
String getRepositoryId(File directory) {
try {
return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID);
} catch (IOException e) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("directory", directory.toString()), "could not read scm configuration file", e);
}
}
}

View File

@@ -70,16 +70,7 @@ public class SvnRepositoryHook implements FSHook
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param hookEventFacade
* @param handler
*/
public SvnRepositoryHook(HookEventFacade hookEventFacade,
SvnRepositoryHandler handler)
public SvnRepositoryHook(HookEventFacade hookEventFacade, SvnRepositoryHandler handler)
{
this.hookEventFacade = hookEventFacade;
this.handler = handler;
@@ -163,10 +154,10 @@ public class SvnRepositoryHook implements FSHook
{
try
{
String id = getRepositoryId(directory);
String repositoryId = getRepositoryId(directory);
//J-
hookEventFacade.handle(id)
hookEventFacade.handle(repositoryId)
.fireHookEvent(
changesetProvider.getType(),
new SvnHookContextProvider(changesetProvider)
@@ -197,18 +188,16 @@ public class SvnRepositoryHook implements FSHook
*
* @throws IOException
*/
private String getRepositoryId(File directory) throws IOException
private String getRepositoryId(File directory)
{
AssertUtil.assertIsNotNull(directory);
return RepositoryUtil.getRepositoryId(handler, directory);
return handler.getRepositoryId(directory);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private SvnRepositoryHandler handler;
/** Field description */
private HookEventFacade hookEventFacade;
private final SvnRepositoryHandler handler;
}

View File

@@ -5,7 +5,6 @@ import { translate } from "react-i18next";
import { InputField, Checkbox, Select } from "@scm-manager/ui-components";
type Configuration = {
repositoryDirectory: string,
compatibility: string,
enabledGZip: boolean,
disabled: boolean,
@@ -31,14 +30,11 @@ class HgConfigurationForm extends React.Component<Props, State> {
this.state = { ...props.initialConfiguration, validationErrors: [] };
}
isValid = () => {
return !!this.state.repositoryDirectory;
};
handleChange = (value: any, name: string) => {
this.setState({
[name]: value
}, () => this.props.onConfigurationChange(this.state, this.isValid()));
}, () => this.props.onConfigurationChange(this.state, true));
};
compatibilityOptions = (values: string[]) => {
@@ -64,16 +60,6 @@ class HgConfigurationForm extends React.Component<Props, State> {
return (
<>
<InputField
name="repositoryDirectory"
label={t("scm-svn-plugin.config.directory")}
helpText={t("scm-svn-plugin.config.directoryHelpText")}
value={this.state.repositoryDirectory}
errorMessage={t("scm-svn-plugin.config.required")}
validationError={!this.state.repositoryDirectory}
onChange={this.handleChange}
disabled={readOnly}
/>
<Select
name="compatibility"
label={t("scm-svn-plugin.config.compatibility")}

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
import { Title, Configuration } from "@scm-manager/ui-components";
import SvnConfigurationForm from "./SvnConfigurationForm";
type Props = {
@@ -18,7 +18,7 @@ class SvnGlobalConfiguration extends React.Component<Props> {
return (
<div>
<Title title={t("scm-svn-plugin.config.title")}/>
<GlobalConfiguration link={link} render={props => <SvnConfigurationForm {...props} />}/>
<Configuration link={link} render={props => <SvnConfigurationForm {...props} />}/>
</div>
);
}

View File

@@ -6,8 +6,6 @@
"config": {
"link": "Subversion",
"title": "Subversion Configuration",
"directory": "Repository Directory",
"directoryHelpText": "Location of Subversion repositories.",
"compatibility": "Version Compatibility",
"compatibilityHelpText": "Specifies with which subversion version repositories are compatible.",
"compatibility-values": {

View File

@@ -36,14 +36,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -69,12 +67,14 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private com.google.inject.Provider<RepositoryManager> repositoryManagerProvider;
@Mock
private RepositoryDAO repositoryDAO;
private HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
private HookEventFacade facade = new HookEventFacade(repositoryManagerProvider, hookContextFactory);
RepositoryLocationResolver repositoryLocationResolver ;
private Path repoDir;
private RepositoryLocationResolver repositoryLocationResolver;
@Override
protected void checkDirectory(File directory) {
@@ -91,18 +91,10 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
File directory) throws RepositoryPathNotFoundException {
File directory) {
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider));
SvnRepositoryHandler handler = new SvnRepositoryHandler(factory, null, repositoryLocationResolver);
DefaultFileSystem fileSystem = new DefaultFileSystem();
PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class);
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, new InitialRepositoryLocationResolver(contextProvider,fileSystem));
SvnRepositoryHandler handler = new SvnRepositoryHandler(factory,
new DefaultFileSystem(), null, repositoryLocationResolver);
repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
handler.init(contextProvider);
SvnConfig config = new SvnConfig();
@@ -117,14 +109,13 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
public void getDirectory() {
when(factory.getStore(any())).thenReturn(store);
SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory,
new DefaultFileSystem(), facade, repositoryLocationResolver);
facade, repositoryLocationResolver);
SvnConfig svnConfig = new SvnConfig();
repositoryHandler.setConfig(svnConfig);
Repository repository = new Repository("id", "svn", "Space", "Name");
initRepository();
File path = repositoryHandler.getDirectory(repository);
assertEquals(repoDir.toString()+File.separator+InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
assertEquals(repoPath.toString()+File.separator+ AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
}

View File

@@ -90,7 +90,7 @@ public class AbstractTestBase
assertTrue(tempDirectory.mkdirs());
contextProvider = MockUtil.getSCMContextProvider(tempDirectory);
fileSystem = new DefaultFileSystem();
InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(contextProvider,fileSystem);
InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(contextProvider);
repositoryLocationResolver = new RepositoryLocationResolver(repositoryDAO, initialRepoLocationResolver);
postSetUp();
}

View File

@@ -37,9 +37,12 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.MockUtil;
import java.io.File;
import java.io.IOException;
import static org.mockito.Mockito.mock;
@@ -61,10 +64,17 @@ public abstract class ManagerTestBase<T extends ModelObject>
protected Manager<T> manager;
protected File temp ;
@Before
public void setUp() throws IOException {
contextProvider = MockUtil.getSCMContextProvider(tempFolder.newFolder());
locationResolver = mock(RepositoryLocationResolver.class);
if (temp == null){
temp = tempFolder.newFolder();
}
contextProvider = MockUtil.getSCMContextProvider(temp);
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider);
RepositoryDAO repoDao = mock(RepositoryDAO.class);
locationResolver = new RepositoryLocationResolver(repoDao ,initialRepositoryLocationResolver);
manager = createManager();
manager.init(contextProvider);
}

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository;
import com.google.common.collect.Sets;
import sonia.scm.AlreadyExistsException;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.xml.bind.annotation.XmlRootElement;
@@ -60,7 +59,7 @@ public class DummyRepositoryHandler
private final Set<String> existingRepoNames = new HashSet<>();
public DummyRepositoryHandler(ConfigurationStoreFactory storeFactory, RepositoryLocationResolver repositoryLocationResolver) {
super(storeFactory, new DefaultFileSystem(), repositoryLocationResolver);
super(storeFactory, repositoryLocationResolver);
}
@Override

View File

@@ -41,20 +41,24 @@ import sonia.scm.util.IOUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
protected PathBasedRepositoryDAO repoDao = mock(PathBasedRepositoryDAO.class);
protected Path repoPath;
protected Repository repository;
protected abstract void checkDirectory(File directory);
protected abstract RepositoryHandler createRepositoryHandler(
@@ -65,27 +69,6 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
createRepository();
}
@Test
public void testCreateResourcePath() {
Repository repository = createRepository();
String path = handler.createResourcePath(repository);
assertNotNull(path);
assertTrue(path.trim().length() > 0);
assertTrue(path.contains(repository.getId()));
}
@Test
public void testDelete() {
Repository repository = createRepository();
handler.delete(repository);
File directory = new File(baseDirectory, repository.getId());
assertFalse(directory.exists());
}
@Override
protected void postSetUp() throws IOException, RepositoryPathNotFoundException {
InMemoryConfigurationStoreFactory storeFactory = new InMemoryConfigurationStoreFactory();
@@ -101,18 +84,22 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
}
}
private Repository createRepository() {
Repository repository = RepositoryTestData.createHeartOfGold();
private void createRepository() {
File nativeRepoDirectory = initRepository();
handler.create(repository);
File directory = new File(new File(baseDirectory, repository.getId()), InitialRepositoryLocationResolver.REPOSITORIES_NATIVE_DIRECTORY);
assertTrue(nativeRepoDirectory.exists());
assertTrue(nativeRepoDirectory.isDirectory());
checkDirectory(nativeRepoDirectory);
}
assertTrue(directory.exists());
assertTrue(directory.isDirectory());
checkDirectory(directory);
return repository;
protected File initRepository() {
repository = RepositoryTestData.createHeartOfGold();
File repoDirectory = new File(baseDirectory, repository.getId());
repoPath = repoDirectory.toPath();
when(repoDao.getPath(repository)).thenReturn(repoPath);
return new File(repoDirectory, AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY);
}
protected File baseDirectory;

View File

@@ -53,7 +53,7 @@ public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestB
protected ConfigurationEntryStore getDataStore(Class type) {
StoreParameters params = new StoreParameters()
.withType(type)
.withName("test")
.withName(storeName)
.build();
return this.createConfigurationStoreFactory().getStore(params);
}
@@ -62,7 +62,7 @@ public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestB
protected ConfigurationEntryStore getDataStore(Class type, Repository repository) {
StoreParameters params = new StoreParameters()
.withType(type)
.withName("test")
.withName(repoStoreName)
.forRepository(repository)
.build();
return this.createConfigurationStoreFactory().getStore(params);

View File

@@ -58,9 +58,11 @@ import java.util.Map;
public abstract class KeyValueStoreTestBase extends AbstractTestBase
{
private Repository repository = RepositoryTestData.createHeartOfGold();
private DataStore<StoreObject> store;
private DataStore<StoreObject> repoStore;
protected Repository repository = RepositoryTestData.createHeartOfGold();
protected DataStore<StoreObject> store;
protected DataStore<StoreObject> repoStore;
protected String repoStoreName = "testRepoStore";
protected String storeName = "testStore";
/**
* Method description

View File

@@ -18,6 +18,7 @@
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",
"fetch-mock": "^7.2.5",
"flow-bin": "^0.79.1",
"flow-typed": "^2.5.1",
"jest": "^23.5.0",

View File

@@ -1,12 +1,13 @@
// @flow
import React from "react";
import { mount, shallow } from "enzyme";
import {mount, shallow} from "enzyme";
import "./tests/enzyme";
import "./tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import Paginator from "./Paginator";
describe("paginator rendering tests", () => {
// TODO: Fix tests
xdescribe("paginator rendering tests", () => {
const options = new ReactRouterEnzymeContext();
@@ -18,7 +19,8 @@ describe("paginator rendering tests", () => {
const collection = {
page: 10,
pageTotal: 20,
_links: {}
_links: {},
_embedded: {}
};
const paginator = shallow(
@@ -40,7 +42,8 @@ describe("paginator rendering tests", () => {
first: dummyLink,
next: dummyLink,
last: dummyLink
}
},
_embedded: {}
};
const paginator = shallow(
@@ -79,7 +82,8 @@ describe("paginator rendering tests", () => {
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
},
_embedded: {}
};
const paginator = shallow(
@@ -121,7 +125,8 @@ describe("paginator rendering tests", () => {
_links: {
first: dummyLink,
prev: dummyLink
}
},
_embedded: {}
};
const paginator = shallow(
@@ -160,7 +165,8 @@ describe("paginator rendering tests", () => {
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
},
_embedded: {}
};
const paginator = shallow(
@@ -204,7 +210,8 @@ describe("paginator rendering tests", () => {
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
},
_embedded: {}
};
const paginator = shallow(
@@ -256,7 +263,8 @@ describe("paginator rendering tests", () => {
},
next: dummyLink,
last: dummyLink
}
},
_embedded: {}
};
let urlToOpen;

View File

@@ -1,8 +1,8 @@
// @flow
import { contextPath } from "./urls";
import {contextPath} from "./urls";
export const NOT_FOUND_ERROR = Error("not found");
export const UNAUTHORIZED_ERROR = Error("unauthorized");
export const NOT_FOUND_ERROR_MESSAGE = "not found";
export const UNAUTHORIZED_ERROR_MESSAGE = "unauthorized";
const fetchOptions: RequestOptions = {
credentials: "same-origin",
@@ -13,17 +13,30 @@ const fetchOptions: RequestOptions = {
function handleStatusCode(response: Response) {
if (!response.ok) {
if (response.status === 401) {
throw UNAUTHORIZED_ERROR;
switch (response.status) {
case 401:
return throwErrorWithMessage(response, UNAUTHORIZED_ERROR_MESSAGE);
case 404:
return throwErrorWithMessage(response, NOT_FOUND_ERROR_MESSAGE);
default:
return throwErrorWithMessage(response, "server returned status code " + response.status);
}
if (response.status === 404) {
throw NOT_FOUND_ERROR;
}
throw new Error("server returned status code " + response.status);
}
return response;
}
function throwErrorWithMessage(response: Response, message: string) {
return response.json().then(
json => {
throw Error(json.message);
},
() => {
throw Error(message);
}
);
}
export function createUrl(url: string) {
if (url.includes("://")) {
return url;

View File

@@ -1,15 +1,65 @@
// @flow
import { createUrl } from "./apiclient";
import {apiClient, createUrl} from "./apiclient";
import fetchMock from "fetch-mock";
describe("create url", () => {
it("should not change absolute urls", () => {
expect(createUrl("https://www.scm-manager.org")).toBe(
"https://www.scm-manager.org"
);
describe("apiClient", () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should add prefix for api", () => {
expect(createUrl("/users")).toBe("/api/v2/users");
expect(createUrl("users")).toBe("/api/v2/users");
describe("create url", () => {
it("should not change absolute urls", () => {
expect(createUrl("https://www.scm-manager.org")).toBe(
"https://www.scm-manager.org"
);
});
it("should add prefix for api", () => {
expect(createUrl("/users")).toBe("/api/v2/users");
expect(createUrl("users")).toBe("/api/v2/users");
});
});
describe("error handling", () => {
const error = {
message: "Error!!"
};
it("should append default error message for 401 if none provided", () => {
fetchMock.mock("api/v2/foo", 401);
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("unauthorized");
});
});
it("should append error message for 401 if provided", () => {
fetchMock.mock("api/v2/foo", {"status": 401, body: error});
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("Error!!");
});
});
it("should append default error message for 401 if none provided", () => {
fetchMock.mock("api/v2/foo", 404);
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("not found");
});
});
it("should append error message for 404 if provided", () => {
fetchMock.mock("api/v2/foo", {"status": 404, body: error});
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("Error!!");
});
});
});
});

View File

@@ -11,8 +11,8 @@ import {
type RenderProps = {
readOnly: boolean,
initialConfiguration: Configuration,
onConfigurationChange: (Configuration, boolean) => void
initialConfiguration: ConfigurationType,
onConfigurationChange: (ConfigurationType, boolean) => void
};
type Props = {
@@ -23,7 +23,7 @@ type Props = {
t: (string) => string
};
type Configuration = {
type ConfigurationType = {
_links: Links
} & Object;
@@ -33,8 +33,8 @@ type State = {
modifying: boolean,
contentType?: string,
configuration?: Configuration,
modifiedConfiguration?: Configuration,
configuration?: ConfigurationType,
modifiedConfiguration?: ConfigurationType,
valid: boolean
};
@@ -42,7 +42,7 @@ type State = {
* GlobalConfiguration uses the render prop pattern to encapsulate the logic for
* synchronizing the configuration with the backend.
*/
class GlobalConfiguration extends React.Component<Props, State> {
class Configuration extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -84,7 +84,7 @@ class GlobalConfiguration extends React.Component<Props, State> {
});
};
loadConfig = (configuration: Configuration) => {
loadConfig = (configuration: ConfigurationType) => {
this.setState({
configuration,
fetching: false,
@@ -107,7 +107,7 @@ class GlobalConfiguration extends React.Component<Props, State> {
return !modificationUrl;
};
configurationChanged = (configuration: Configuration, valid: boolean) => {
configurationChanged = (configuration: ConfigurationType, valid: boolean) => {
this.setState({
modifiedConfiguration: configuration,
valid
@@ -159,4 +159,4 @@ class GlobalConfiguration extends React.Component<Props, State> {
}
export default translate("config")(GlobalConfiguration);
export default translate("config")(Configuration);

View File

@@ -9,6 +9,16 @@ class ConfigurationBinder {
i18nNamespace: string = "plugins";
navLink(to: string, labelI18nKey: string, t: any){
return <NavLink to={to} label={t(labelI18nKey)} />;
}
route(path: string, Component: any){
return <Route path={path}
render={() => Component}
exact/>;
}
bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) {
// create predicate based on the link name of the index resource
@@ -19,25 +29,48 @@ class ConfigurationBinder {
// create NavigationLink with translated label
const ConfigNavLink = translate(this.i18nNamespace)(({t}) => {
return <NavLink to={"/config" + to} label={t(labelI18nKey)} />;
return this.navLink("/config" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("config.navigation", ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links }) => {
const link = links[linkName].href;
return <Route path={url + to}
render={() => <ConfigurationComponent link={link}/>}
exact/>;
return this.route(url + to, <ConfigurationComponent link={link}/>);
};
// bind config route to extension point
binder.bind("config.route", ConfigRoute, configPredicate);
}
bindRepository(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.navigation", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({ url, repository }) => {
return this.route(url + to, <RepositoryComponent repository={repository}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -1,3 +1,3 @@
// @flow
export { default as ConfigurationBinder } from "./ConfigurationBinder";
export { default as GlobalConfiguration } from "./GlobalConfiguration";
export { default as Configuration } from "./Configuration";

View File

@@ -0,0 +1,112 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import InputField from "./InputField";
type State = {
password: string,
confirmedPassword: string,
passwordValid: boolean,
passwordConfirmationFailed: boolean
};
type Props = {
passwordChanged: string => void,
passwordValidator?: string => boolean,
// Context props
t: string => string
};
class PasswordConfirmation extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
password: "",
confirmedPassword: "",
passwordValid: true,
passwordConfirmationFailed: false
};
}
componentDidMount() {
this.setState({
password: "",
confirmedPassword: "",
passwordValid: true,
passwordConfirmationFailed: false
});
}
render() {
const { t } = this.props;
return (
<>
<InputField
label={t("password.newPassword")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={!this.state.passwordValid}
errorMessage={t("password.passwordInvalid")}
helpText={t("password.passwordHelpText")}
/>
<InputField
label={t("password.confirmPassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.confirmedPassword : ""}
validationError={this.state.passwordConfirmationFailed}
errorMessage={t("password.passwordConfirmFailed")}
helpText={t("password.passwordConfirmHelpText")}
/>
</>
);
}
validatePassword = password => {
const { passwordValidator } = this.props;
if (passwordValidator) {
return passwordValidator(password);
}
return password.length >= 6 && password.length < 32;
};
handlePasswordValidationChange = (confirmedPassword: string) => {
const passwordConfirmed = this.state.password === confirmedPassword;
this.setState(
{
confirmedPassword,
passwordConfirmationFailed: !passwordConfirmed
},
this.propagateChange
);
};
handlePasswordChange = (password: string) => {
const passwordConfirmationFailed =
password !== this.state.confirmedPassword;
this.setState(
{
passwordValid: this.validatePassword(password),
passwordConfirmationFailed,
password: password
},
this.propagateChange
);
};
propagateChange = () => {
if (
this.state.password &&
this.state.passwordValid &&
!this.state.passwordConfirmationFailed
) {
this.props.passwordChanged(this.state.password);
}
};
}
export default translate("commons")(PasswordConfirmation);

View File

@@ -5,5 +5,6 @@ export { default as Checkbox } from "./Checkbox.js";
export { default as InputField } from "./InputField.js";
export { default as Select } from "./Select.js";
export { default as Textarea } from "./Textarea.js";
export { default as PasswordConfirmation } from "./PasswordConfirmation.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";

View File

@@ -22,7 +22,7 @@ export { default as HelpIcon } from "./HelpIcon";
export { default as Tooltip } from "./Tooltip";
export { getPageFromMatch } from "./urls";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
export { apiClient, NOT_FOUND_ERROR_MESSAGE, UNAUTHORIZED_ERROR_MESSAGE } from "./apiclient.js";
export * from "./buttons";
export * from "./config";

View File

@@ -2995,6 +2995,15 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
fetch-mock@^7.2.5:
version "7.2.5"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.2.5.tgz#4682f51b9fa74d790e10a471066cb22f3ff84d48"
dependencies:
babel-polyfill "^6.26.0"
glob-to-regexp "^0.4.0"
path-to-regexp "^2.2.1"
whatwg-url "^6.5.0"
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -3341,6 +3350,10 @@ glob-stream@^3.1.5:
through2 "^0.6.1"
unique-stream "^1.0.0"
glob-to-regexp@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz#49bd677b1671022bd10921c3788f23cdebf9c7e6"
glob-watcher@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b"
@@ -5982,6 +5995,10 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -7814,7 +7831,7 @@ whatwg-mimetype@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171"
whatwg-url@^6.4.1:
whatwg-url@^6.4.1, whatwg-url@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
dependencies:

View File

@@ -1,7 +1,10 @@
// @flow
import type { Links } from "./hal";
export type Me = {
name: string,
displayName: string,
mail: string
mail: string,
_links: Links
};

View File

@@ -1,6 +1,6 @@
// @flow
import type { Collection, Links } from "./hal";
import type { Links } from "./hal";
// TODO ?? check ?? links
export type SubRepository = {
@@ -20,6 +20,6 @@ export type File = {
subRepository?: SubRepository, // TODO
_links: Links,
_embedded: {
children: File[]
children: ?File[]
}
};

View File

@@ -40,12 +40,29 @@
"previous": "Previous"
},
"profile": {
"navigation-label": "Navigation",
"actions-label": "Actions",
"username": "Username",
"displayName": "Display Name",
"mail": "E-Mail",
"information": "Information",
"change-password": "Change password",
"error-title": "Error",
"error-subtitle": "Cannot display profile"
"error-subtitle": "Cannot display profile",
"error": "Error",
"error-message": "'me' is undefined"
},
"password": {
"label": "Password",
"newPassword": "New password",
"passwordHelpText": "Plain text password of the user.",
"passwordConfirmHelpText": "Repeat the password for confirmation.",
"currentPassword": "Current password",
"currentPasswordHelpText": "The password currently in use",
"confirmPassword": "Confirm password",
"passwordInvalid": "Password has to be between 6 and 32 characters",
"passwordConfirmFailed": "Passwords have to be identical",
"submit": "Submit",
"changedSuccessfully": "Pasword successfully changed"
}
}

View File

@@ -50,10 +50,7 @@
"validation": {
"mail-invalid": "This email is invalid",
"name-invalid": "This name is invalid",
"displayname-invalid": "This displayname is invalid",
"password-invalid": "Password has to be between 6 and 32 characters",
"passwordValidation-invalid": "Passwords have to be identical",
"validatePassword": "Confirm password"
"displayname-invalid": "This displayname is invalid"
},
"password": {
"set-password-successful": "Password successfully set"
@@ -62,8 +59,6 @@
"usernameHelpText": "Unique name of the user.",
"displayNameHelpText": "Display name of the user.",
"mailHelpText": "Email address of the user.",
"passwordHelpText": "Plain text password of the user.",
"passwordConfirmHelpText": "Repeat the password for validation.",
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
"activeHelpText": "Activate or deactive the user."
}

View File

@@ -0,0 +1,141 @@
// @flow
import React from "react";
import {
ErrorNotification,
InputField,
Notification,
PasswordConfirmation,
SubmitButton
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Me } from "@scm-manager/ui-types";
import { changePassword } from "../modules/changePassword";
type Props = {
me: Me,
t: string => string
};
type State = {
oldPassword: string,
password: string,
loading: boolean,
error?: Error,
passwordChanged: boolean
};
class ChangeUserPassword extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
oldPassword: "",
password: "",
loading: false,
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: "",
passwordChanged: false
};
}
setLoadingState = () => {
this.setState({
...this.state,
loading: true
});
};
setErrorState = (error: Error) => {
this.setState({
...this.state,
error: error,
loading: false
});
};
setSuccessfulState = () => {
this.setState({
...this.state,
loading: false,
passwordChanged: true,
oldPassword: "",
password: ""
});
};
submit = (event: Event) => {
event.preventDefault();
if (this.state.password) {
const { oldPassword, password } = this.state;
this.setLoadingState();
changePassword(this.props.me._links.password.href, oldPassword, password)
.then(result => {
if (result.error) {
this.setErrorState(result.error);
} else {
this.setSuccessfulState();
}
})
.catch(err => {
this.setErrorState(err);
});
}
};
render() {
const { t } = this.props;
const { loading, passwordChanged, error } = this.state;
let message = null;
if (passwordChanged) {
message = (
<Notification
type={"success"}
children={t("password.changedSuccessfully")}
onClose={() => this.onClose()}
/>
);
} else if (error) {
message = <ErrorNotification error={error} />;
}
return (
<form onSubmit={this.submit}>
{message}
<InputField
label={t("password.currentPassword")}
type="password"
onChange={oldPassword =>
this.setState({ ...this.state, oldPassword })
}
value={this.state.oldPassword ? this.state.oldPassword : ""}
helpText={t("password.currentPasswordHelpText")}
/>
<PasswordConfirmation
passwordChanged={this.passwordChanged}
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<SubmitButton
disabled={!this.state.password}
loading={loading}
label={t("password.submit")}
/>
</form>
);
}
passwordChanged = (password: string) => {
this.setState({ ...this.state, password });
};
onClose = () => {
this.setState({
...this.state,
passwordChanged: false
});
};
}
export default translate("commons")(ChangeUserPassword);

View File

@@ -19,6 +19,7 @@ import SingleGroup from "../groups/containers/SingleGroup";
import AddGroup from "../groups/containers/AddGroup";
import Config from "../config/containers/Config";
import ChangeUserPassword from "./ChangeUserPassword";
import Profile from "./Profile";
type Props = {
@@ -79,6 +80,7 @@ class Main extends React.Component<Props> {
path="/user/:name"
component={SingleUser}
/>
<ProtectedRoute
exact
path="/groups"
@@ -107,7 +109,6 @@ class Main extends React.Component<Props> {
authenticated={authenticated}
/>
<ProtectedRoute
exact
path="/me"
component={Profile}
authenticated={authenticated}

View File

@@ -2,82 +2,82 @@
import React from "react";
import {
Page,
Navigation,
Section,
MailLink
} from "../../../scm-ui-components/packages/ui-components/src/index";
import { NavLink } from "react-router-dom";
import { Route, withRouter } from "react-router-dom";
import { getMe } from "../modules/auth";
import { compose } from "redux";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { Me } from "../../../scm-ui-components/packages/ui-types/src/index";
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
import { ErrorPage } from "@scm-manager/ui-components";
import type { Me } from "@scm-manager/ui-types";
import {
ErrorPage,
Page,
Navigation,
Section,
NavLink
} from "@scm-manager/ui-components";
import ChangeUserPassword from "./ChangeUserPassword";
import ProfileInfo from "./ProfileInfo";
type Props = {
me: Me,
// Context props
t: string => string
t: string => string,
match: any
};
type State = {};
class Profile extends React.Component<Props, State> {
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 2);
}
return url;
};
matchedUrl = () => {
return this.stripEndingSlash(this.props.match.url);
};
render() {
const url = this.matchedUrl();
const { me, t } = this.props;
if (me) {
if (!me) {
return (
<ErrorPage
title={t("profile.error-title")}
subtitle={t("profile.error-subtitle")}
error={{ name: "Error", message: "'me' is undefined" }}
error={{
name: t("profile.error"),
message: t("profile.error-message")
}}
/>
);
}
return (
<Page title={me.displayName}>
<div className="columns">
<AvatarWrapper>
<div>
<figure className="media-left">
<p className="image is-64x64">
{
// TODO: add avatar
}
</p>
</figure>
</div>
</AvatarWrapper>
<div className="column is-two-quarters">
<table className="table">
<tbody>
<tr>
<td>{t("profile.username")}</td>
<td>{me.name}</td>
</tr>
<tr>
<td>{t("profile.displayName")}</td>
<td>{me.displayName}</td>
</tr>
<tr>
<td>{t("profile.mail")}</td>
<td>
<MailLink address={me.mail} />
</td>
</tr>
</tbody>
</table>
<div className="column is-three-quarters">
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
<Route
path={`${url}/password`}
render={() => <ChangeUserPassword me={me} />}
/>
</div>
<div className="column is-one-quarter">
<div className="column">
<Navigation>
<Section label={t("profile.actions-label")} />
<NavLink to={"me/password"}>
{t("profile.change-password")}
</NavLink>
<Section label={t("profile.navigation-label")}>
<NavLink to={`${url}`} label={t("profile.information")} />
</Section>
<Section label={t("profile.actions-label")}>
<NavLink
to={`${url}/password`}
label={t("profile.change-password")}
/>
</Section>
</Navigation>
</div>
</div>
@@ -94,5 +94,6 @@ const mapStateToProps = state => {
export default compose(
translate("commons"),
connect(mapStateToProps)
connect(mapStateToProps),
withRouter
)(Profile);

View File

@@ -0,0 +1,56 @@
// @flow
import React from "react";
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
import type { Me } from "@scm-manager/ui-types";
import { MailLink } from "@scm-manager/ui-components";
import { compose } from "redux";
import { translate } from "react-i18next";
type Props = {
me: Me,
// Context props
t: string => string
};
type State = {};
class ProfileInfo extends React.Component<Props, State> {
render() {
const { me, t } = this.props;
return (
<>
<AvatarWrapper>
<div>
<figure className="media-left">
<p className="image is-64x64">
{
// TODO: add avatar
}
</p>
</figure>
</div>
</AvatarWrapper>
<table className="table">
<tbody>
<tr>
<td>{t("profile.username")}</td>
<td>{me.name}</td>
</tr>
<tr>
<td>{t("profile.displayName")}</td>
<td>{me.displayName}</td>
</tr>
<tr>
<td>{t("profile.mail")}</td>
<td>
<MailLink address={me.mail} />
</td>
</tr>
</tbody>
</table>
</>
);
}
}
export default compose(translate("commons"))(ProfileInfo);

View File

@@ -15,11 +15,14 @@ i18n
.init({
fallbackLng: "en",
// try to load only "en" and not "en_US"
load: "languageOnly",
// have a common namespace used around the full app
ns: ["commons"],
defaultNS: "commons",
debug: true,
debug: false,
interpolation: {
escapeValue: false // not needed for react!!

View File

@@ -2,7 +2,10 @@
import type { Me } from "@scm-manager/ui-types";
import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
import {
apiClient,
UNAUTHORIZED_ERROR_MESSAGE
} from "@scm-manager/ui-components";
import { isPending } from "./pending";
import { getFailure } from "./failure";
import {
@@ -136,10 +139,12 @@ const callFetchMe = (link: string): Promise<Me> => {
return response.json();
})
.then(json => {
const { name, displayName, mail, _links } = json;
return {
name: json.name,
displayName: json.displayName,
mail: json.mail
name,
displayName,
mail,
_links
};
});
};
@@ -185,7 +190,7 @@ export const fetchMe = (link: string) => {
dispatch(fetchMeSuccess(me));
})
.catch((error: Error) => {
if (error === UNAUTHORIZED_ERROR) {
if (error.message === UNAUTHORIZED_ERROR_MESSAGE) {
dispatch(fetchMeUnauthenticated());
} else {
dispatch(fetchMeFailure(error));

View File

@@ -0,0 +1,16 @@
// @flow
import { apiClient } from "@scm-manager/ui-components";
export const CONTENT_TYPE_PASSWORD_CHANGE =
"application/vnd.scmm-passwordChange+json;v=2";
export function changePassword(
url: string,
oldPassword: string,
newPassword: string
) {
return apiClient
.put(url, { oldPassword, newPassword }, CONTENT_TYPE_PASSWORD_CHANGE)
.then(response => {
return response;
});
}

View File

@@ -0,0 +1,25 @@
import fetchMock from "fetch-mock";
import { changePassword, CONTENT_TYPE_PASSWORD_CHANGE } from "./changePassword";
describe("change password", () => {
const CHANGE_PASSWORD_URL = "/me/password";
const oldPassword = "old";
const newPassword = "new";
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should update password", done => {
fetchMock.put("/api/v2" + CHANGE_PASSWORD_URL, 204, {
headers: { "content-type": CONTENT_TYPE_PASSWORD_CHANGE }
});
changePassword(CHANGE_PASSWORD_URL, oldPassword, newPassword).then(
content => {
done();
}
);
});
});

View File

@@ -11,6 +11,9 @@ describe("RepositoryNavLink", () => {
it("should render nothing, if the sources link is missing", () => {
const repository = {
namespace: "Namespace",
name: "Repo",
type: "GIT",
_links: {}
};
@@ -20,6 +23,7 @@ describe("RepositoryNavLink", () => {
linkName="sources"
to="/sources"
label="Sources"
activeOnlyWhenExact={true}
/>,
options.get()
);
@@ -28,6 +32,9 @@ describe("RepositoryNavLink", () => {
it("should render the navLink", () => {
const repository = {
namespace: "Namespace",
name: "Repo",
type: "GIT",
_links: {
sources: {
href: "/sources"
@@ -41,6 +48,7 @@ describe("RepositoryNavLink", () => {
linkName="sources"
to="/sources"
label="Sources"
activeOnlyWhenExact={true}
/>,
options.get()
);

View File

@@ -35,7 +35,7 @@ import PermissionsNavLink from "../components/PermissionsNavLink";
import Sources from "../sources/containers/Sources";
import RepositoryNavLink from "../components/RepositoryNavLink";
import { getRepositoriesLink } from "../../modules/indexResource";
import {ExtensionPoint} from '@scm-manager/ui-extensions';
import {ExtensionPoint} from "@scm-manager/ui-extensions";
type Props = {
namespace: string,

View File

@@ -96,7 +96,7 @@ class FileTree extends React.Component<Props> {
});
}
if (tree._embedded) {
if (tree._embedded && tree._embedded.children) {
files.push(...tree._embedded.children.sort(compareFiles));
}

View File

@@ -8,7 +8,13 @@ describe("create link tests", () => {
return {
name: "dir",
path: path,
directory: true
directory: true,
length: 1,
revision: "1a",
_links: {},
_embedded: {
children: []
}
};
}

View File

@@ -91,7 +91,7 @@ export default function reducer(
state: any = {},
action: Action = { type: "UNKNOWN" }
): any {
if (action.type === FETCH_SOURCES_SUCCESS) {
if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) {
return {
[action.itemId]: action.payload,
...state

View File

@@ -33,7 +33,13 @@ const repository: Repository = {
};
const collection = {
name: "src",
path: "src",
directory: true,
description: "foo",
length: 176,
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
subRepository: undefined,
_links: {
self: {
href:
@@ -41,20 +47,24 @@ const collection = {
}
},
_embedded: {
files: [
children: [
{
name: "src",
path: "src",
directory: true,
description: null,
description: "",
length: 176,
lastModified: null,
subRepository: null,
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
lastModified: "",
subRepository: undefined,
_links: {
self: {
href:
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src"
}
},
_embedded: {
children: []
}
},
{
@@ -63,8 +73,9 @@ const collection = {
directory: false,
description: "bump version",
length: 780,
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
lastModified: "2017-07-31T11:17:19Z",
subRepository: null,
subRepository: undefined,
_links: {
self: {
href:
@@ -74,6 +85,9 @@ const collection = {
href:
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/history/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
}
},
_embedded: {
children: []
}
}
]
@@ -92,7 +106,9 @@ const noDirectory: File = {
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src"
}
},
_embedded: collection
_embedded: {
children: []
}
};
describe("sources fetch", () => {
@@ -116,7 +132,7 @@ describe("sources fetch", () => {
];
const store = mockStore({});
return store.dispatch(fetchSources(repository)).then(() => {
return store.dispatch(fetchSources(repository, "", "")).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
@@ -145,7 +161,7 @@ describe("sources fetch", () => {
});
const store = mockStore({});
return store.dispatch(fetchSources(repository)).then(() => {
return store.dispatch(fetchSources(repository, "", "")).then(() => {
const actions = store.getActions();
expect(actions[0].type).toBe(FETCH_SOURCES_PENDING);
expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE);
@@ -166,7 +182,7 @@ describe("reducer tests", () => {
"scm/core/_/": collection
};
expect(
reducer({}, fetchSourcesSuccess(repository, null, null, collection))
reducer({}, fetchSourcesSuccess(repository, "", "", collection))
).toEqual(expectedState);
});
@@ -207,7 +223,7 @@ describe("selector tests", () => {
});
it("should return null", () => {
expect(getSources({}, repository)).toBeFalsy();
expect(getSources({}, repository, "", "")).toBeFalsy();
});
it("should return the source collection without revision and path", () => {
@@ -216,7 +232,7 @@ describe("selector tests", () => {
"scm/core/_/": collection
}
};
expect(getSources(state, repository)).toBe(collection);
expect(getSources(state, repository, "", "")).toBe(collection);
});
it("should return the source collection with revision and path", () => {
@@ -234,11 +250,11 @@ describe("selector tests", () => {
[FETCH_SOURCES + "/scm/core/_/"]: true
}
};
expect(isFetchSourcesPending(state, repository)).toEqual(true);
expect(isFetchSourcesPending(state, repository, "", "")).toEqual(true);
});
it("should return false, when fetch sources is not pending", () => {
expect(isFetchSourcesPending({}, repository)).toEqual(false);
expect(isFetchSourcesPending({}, repository, "", "")).toEqual(false);
});
const error = new Error("incredible error from hell");
@@ -249,10 +265,10 @@ describe("selector tests", () => {
[FETCH_SOURCES + "/scm/core/_/"]: error
}
};
expect(getFetchSourcesFailure(state, repository)).toEqual(error);
expect(getFetchSourcesFailure(state, repository, "", "")).toEqual(error);
});
it("should return undefined when fetch sources did not fail", () => {
expect(getFetchSourcesFailure({}, repository)).toBe(undefined);
expect(getFetchSourcesFailure({}, repository, "", "")).toBe(undefined);
});
});

View File

@@ -2,14 +2,13 @@
import React from "react";
import type { User } from "@scm-manager/ui-types";
import {
InputField,
SubmitButton,
Notification,
ErrorNotification
ErrorNotification,
PasswordConfirmation
} from "@scm-manager/ui-components";
import * as userValidator from "./userValidation";
import { translate } from "react-i18next";
import { updatePassword } from "./updatePassword";
import { setPassword } from "./setPassword";
type Props = {
user: User,
@@ -19,9 +18,6 @@ type Props = {
type State = {
password: string,
loading: boolean,
passwordConfirmationError: boolean,
validatePasswordError: boolean,
validatePassword: string,
error?: Error,
passwordChanged: boolean
};
@@ -40,12 +36,6 @@ class SetUserPassword extends React.Component<Props, State> {
};
}
passwordIsValid = () => {
return !(
this.state.validatePasswordError || this.state.passwordConfirmationError
);
};
setLoadingState = () => {
this.setState({
...this.state,
@@ -66,20 +56,17 @@ class SetUserPassword extends React.Component<Props, State> {
...this.state,
loading: false,
passwordChanged: true,
password: "",
validatePassword: "",
validatePasswordError: false,
passwordConfirmationError: false
password: ""
});
};
submit = (event: Event) => {
event.preventDefault();
if (this.passwordIsValid()) {
if (this.state.password) {
const { user } = this.props;
const { password } = this.state;
this.setLoadingState();
updatePassword(user._links.password.href, password)
setPassword(user._links.password.href, password)
.then(result => {
if (result.error) {
this.setErrorState(result.error);
@@ -112,26 +99,12 @@ class SetUserPassword extends React.Component<Props, State> {
return (
<form onSubmit={this.submit}>
{message}
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
<PasswordConfirmation
passwordChanged={this.passwordChanged}
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<SubmitButton
disabled={!this.passwordIsValid()}
disabled={!this.state.password}
loading={loading}
label={t("user-form.submit")}
/>
@@ -139,31 +112,8 @@ class SetUserPassword extends React.Component<Props, State> {
);
}
handlePasswordChange = (password: string) => {
const validatePasswordError = !this.checkPasswords(
password,
this.state.validatePassword
);
this.setState({
validatePasswordError: !userValidator.isPasswordValid(password),
passwordConfirmationError: validatePasswordError,
password: password
});
};
handlePasswordValidationChange = (validatePassword: string) => {
const passwordConfirmed = this.checkPasswords(
this.state.password,
validatePassword
);
this.setState({
validatePassword,
passwordConfirmationError: !passwordConfirmed
});
};
checkPasswords = (password1: string, password2: string) => {
return password1 === password2;
passwordChanged = (password: string) => {
this.setState({ ...this.state, password });
};
onClose = () => {

View File

@@ -5,6 +5,7 @@ import type { User } from "@scm-manager/ui-types";
import {
Checkbox,
InputField,
PasswordConfirmation,
SubmitButton,
validation as validator
} from "@scm-manager/ui-components";
@@ -21,10 +22,7 @@ type State = {
user: User,
mailValidationError: boolean,
nameValidationError: boolean,
displayNameValidationError: boolean,
passwordConfirmationError: boolean,
validatePasswordError: boolean,
validatePassword: string
displayNameValidationError: boolean
};
class UserForm extends React.Component<Props, State> {
@@ -38,15 +36,12 @@ class UserForm extends React.Component<Props, State> {
mail: "",
password: "",
admin: false,
active: false,
active: true,
_links: {}
},
mailValidationError: false,
displayNameValidationError: false,
nameValidationError: false,
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: ""
nameValidationError: false
};
}
@@ -66,14 +61,15 @@ class UserForm extends React.Component<Props, State> {
isValid = () => {
const user = this.state.user;
const passwordValid = this.props.user ? !this.isFalsy(user.password) : true;
return !(
this.state.validatePasswordError ||
this.state.nameValidationError ||
this.state.mailValidationError ||
this.state.passwordConfirmationError ||
this.state.displayNameValidationError ||
this.isFalsy(user.name) ||
this.isFalsy(user.displayName)
this.isFalsy(user.displayName) ||
this.isFalsy(user.mail) ||
passwordValid
);
};
@@ -89,7 +85,7 @@ class UserForm extends React.Component<Props, State> {
const user = this.state.user;
let nameField = null;
let passwordFields = null;
let passwordChangeField = null;
if (!this.props.user) {
nameField = (
<InputField
@@ -101,27 +97,9 @@ class UserForm extends React.Component<Props, State> {
helpText={t("help.usernameHelpText")}
/>
);
passwordFields = (
<>
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
</>
passwordChangeField = (
<PasswordConfirmation passwordChanged={this.handlePasswordChange} />
);
}
return (
@@ -143,7 +121,7 @@ class UserForm extends React.Component<Props, State> {
errorMessage={t("validation.mail-invalid")}
helpText={t("help.mailHelpText")}
/>
{passwordFields}
{passwordChangeField}
<Checkbox
label={t("user.admin")}
onChange={this.handleAdminChange}
@@ -189,32 +167,11 @@ class UserForm extends React.Component<Props, State> {
};
handlePasswordChange = (password: string) => {
const validatePasswordError = !this.checkPasswords(
password,
this.state.validatePassword
);
this.setState({
validatePasswordError: !userValidator.isPasswordValid(password),
passwordConfirmationError: validatePasswordError,
user: { ...this.state.user, password }
});
};
handlePasswordValidationChange = (validatePassword: string) => {
const validatePasswordError = this.checkPasswords(
this.state.user.password,
validatePassword
);
this.setState({
validatePassword,
passwordConfirmationError: !validatePasswordError
});
};
checkPasswords = (password1: string, password2: string) => {
return password1 === password2;
};
handleAdminChange = (admin: boolean) => {
this.setState({ user: { ...this.state.user, admin } });
};

View File

@@ -1,15 +1,13 @@
//@flow
import { apiClient } from "@scm-manager/ui-components";
const CONTENT_TYPE_PASSWORD_OVERWRITE =
export const CONTENT_TYPE_PASSWORD_OVERWRITE =
"application/vnd.scmm-passwordOverwrite+json;v=2";
export function updatePassword(url: string, password: string) {
export function setPassword(url: string, password: string) {
return apiClient
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
.then(response => {
return response;
})
.catch(err => {
return { error: err };
});
}

View File

@@ -0,0 +1,25 @@
//@flow
import fetchMock from "fetch-mock";
import { CONTENT_TYPE_PASSWORD_OVERWRITE, setPassword } from "./setPassword";
describe("password change", () => {
const SET_PASSWORD_URL = "/users/testuser/password";
const newPassword = "testpw123";
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should set password", done => {
fetchMock.put("/api/v2" + SET_PASSWORD_URL, 204, {
headers: {
"content-type": CONTENT_TYPE_PASSWORD_OVERWRITE
}
});
setPassword(SET_PASSWORD_URL, newPassword).then(content => {
done();
});
});
});

View File

@@ -1,23 +0,0 @@
//@flow
import fetchMock from "fetch-mock";
import { updatePassword } from "./updatePassword";
describe("get content type", () => {
const PASSWORD_URL = "/users/testuser/password";
const password = "testpw123";
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should update password", done => {
fetchMock.put("/api/v2" + PASSWORD_URL, 204);
updatePassword(PASSWORD_URL, password).then(content => {
done();
});
});
});

View File

@@ -52,7 +52,7 @@ public final class DebugService
private final Multimap<NamespaceAndName,DebugHookData> receivedHooks = LinkedListMultimap.create();
/**
* Stores {@link DebugHookData} for the given repository.
* Store {@link DebugHookData} for the given repository.
*/
void put(NamespaceAndName namespaceAndName, DebugHookData hookData)
{

View File

@@ -39,7 +39,6 @@ import com.google.inject.Singleton;
import org.apache.shiro.concurrent.SubjectAwareExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ConfigurationException;
import sonia.scm.HandlerEventType;
import sonia.scm.ManagerDaoAdapter;
@@ -138,17 +137,18 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
return managerDaoAdapter.create(
repository,
RepositoryPermissions::create,
newRepository -> fireEvent(HandlerEventType.BEFORE_CREATE, newRepository),
newRepository -> {
fireEvent(HandlerEventType.CREATE, newRepository);
if (initRepository) {
try {
getHandler(newRepository).create(newRepository);
} catch (AlreadyExistsException e) {
throw new InternalRepositoryException(repository, "directory for repository does already exist", e);
} catch (InternalRepositoryException e) {
delete(repository);
throw e;
}
}
fireEvent(HandlerEventType.BEFORE_CREATE, newRepository);
},
newRepository -> fireEvent(HandlerEventType.CREATE, newRepository),
newRepository -> repositoryDAO.contains(newRepository.getNamespaceAndName())
);
}

View File

@@ -39,9 +39,11 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.util.ThreadContext;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import sonia.scm.AlreadyExistsException;
@@ -49,6 +51,7 @@ import sonia.scm.HandlerEventType;
import sonia.scm.Manager;
import sonia.scm.ManagerTestBase;
import sonia.scm.NotFoundException;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.io.DefaultFileSystem;
@@ -62,6 +65,7 @@ import sonia.scm.security.KeyGenerator;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -106,10 +110,18 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private ScmConfiguration configuration;
private String mockedNamespace = "default_namespace";
@Before
public void initContext() {
((TempSCMContextProvider)SCMContext.getContext()).setBaseDirectory(temp);
}
@Test
public void testCreate() {
Repository heartOfGold = createTestRepository();
@@ -422,10 +434,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
private DefaultRepositoryManager createRepositoryManager(boolean archiveEnabled, KeyGenerator keyGenerator) {
DefaultFileSystem fileSystem = new DefaultFileSystem();
Set<RepositoryHandler> handlerSet = new HashSet<>();
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider, fileSystem);
XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(initialRepositoryLocationResolver, contextProvider);
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider);
XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(initialRepositoryLocationResolver, fileSystem, contextProvider);
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(repositoryDAO, initialRepositoryLocationResolver);
ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(repositoryLocationResolver);
ConfigurationStoreFactory factory = new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver);
handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver));
handlerSet.add(new DummyRepositoryHandler(factory, repositoryLocationResolver) {
@Override

View File

@@ -70,8 +70,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
public void createSecuritySystem()
{
JAXBConfigurationEntryStoreFactory factory =
new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(),
contextProvider);
new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() );
securitySystem = new DefaultSecuritySystem(factory);

View File

@@ -3,7 +3,7 @@ package sonia.scm.security;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.authc.AuthenticationException;
import org.junit.Ignore;
import org.apache.shiro.util.ThreadContext;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -30,6 +30,10 @@ public class SecurityRequestFilterTest {
@InjectMocks
private SecurityRequestFilter securityRequestFilter;
{
ThreadContext.unbindSubject();
}
@Test
public void shouldAllowUnauthenticatedAccessForAnnotatedMethod() throws NoSuchMethodException {
when(resourceInfo.getResourceMethod()).thenReturn(SecurityTestClass.class.getMethod("anonymousAccessAllowed"));

View File

@@ -45,6 +45,9 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import sonia.scm.NotFoundException;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.store.JAXBConfigurationStoreFactory;
import sonia.scm.user.xml.XmlUserDAO;
@@ -72,7 +75,7 @@ public class DefaultUserManagerTest extends UserManagerTestBase
public ShiroRule shiro = new ShiroRule();
private UserDAO userDAO = mock(UserDAO.class);
private UserDAO userDAO ;
private User trillian;
/**
@@ -182,6 +185,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
//~--- methods --------------------------------------------------------------
private XmlUserDAO createXmlUserDAO() {
return new XmlUserDAO(new JAXBConfigurationStoreFactory(locationResolver));
return new XmlUserDAO(new JAXBConfigurationStoreFactory(contextProvider, locationResolver));
}
}