mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 02:55:56 +01:00
Repository export read-only lock (#1519)
* Lock repository for read-only access only while exporting * Create read-only check api Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
@@ -18,6 +18,7 @@ Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository N
|
||||
Ein archiviertes Repository kann nicht mehr verändert werden.
|
||||
|
||||
In dem Bereich "Repository exportieren" kann das Repository in unterschiedlichen Formaten exportiert werden.
|
||||
Während des laufenden Exports kann auf das Repository nur lesend zugriffen werden.
|
||||
Das Ausgabeformat des Repository kann über die angebotenen Optionen verändert werden:
|
||||
* `Standard`: Werden keine Optionen ausgewählt, wird das Repository im Standard Format exportiert.
|
||||
Git und Mercurial werden dabei als `Tar Archiv` exportiert und Subversion nutzt das `Dump` Format.
|
||||
|
||||
@@ -16,6 +16,7 @@ strategy in the global SCM-Manager config is set to `custom` you may even rename
|
||||
repository is marked as archived, it can no longer be modified.
|
||||
|
||||
In the "Export repository" section the repository can be exported in different formats.
|
||||
During the export the repository cannot be modified!
|
||||
The output format of the repository can be changed via the offered options:
|
||||
* `Standard`: If no options are selected, the repository will be exported in the standard format.
|
||||
Git and Mercurial are exported as `Tar archive` and Subversion uses the `Dump` format.
|
||||
|
||||
2
gradle/changelog/repository_export_lock.yaml
Normal file
2
gradle/changelog/repository_export_lock.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Lock repository to "read-only" access during export ([#1519](https://github.com/scm-manager/scm-manager/pull/1519))
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RepositoryExportingCheck}. This tracks the exporting status of repositories.
|
||||
*/
|
||||
public final class DefaultRepositoryExportingCheck implements RepositoryExportingCheck {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultRepositoryExportingCheck.class);
|
||||
private static final Map<String, AtomicInteger> EXPORTING_REPOSITORIES = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public static boolean isRepositoryExporting(String repositoryId) {
|
||||
return getLockCount(repositoryId).get() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExporting(String repositoryId) {
|
||||
return isRepositoryExporting(repositoryId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T withExportingLock(Repository repository, Supplier<T> callback) {
|
||||
try {
|
||||
getLockCount(repository.getId()).incrementAndGet();
|
||||
return callback.get();
|
||||
} finally {
|
||||
int lockCount = getLockCount(repository.getId()).decrementAndGet();
|
||||
if (lockCount <= 0) {
|
||||
LOG.warn("Got negative export lock count {} for repository {}", lockCount, repository);
|
||||
EXPORTING_REPOSITORIES.remove(repository.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AtomicInteger getLockCount(String repositoryId) {
|
||||
return EXPORTING_REPOSITORIES.computeIfAbsent(repositoryId, r -> new AtomicInteger(0));
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,11 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.github.legman.Subscribe;
|
||||
import sonia.scm.EagerSingleton;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
@Extension
|
||||
@EagerSingleton
|
||||
/**
|
||||
* Default implementation of {@link RepositoryArchivedCheck}. This tracks the archive status of repositories by using
|
||||
* {@link RepositoryModificationEvent}s. The initial set of archived repositories is read by
|
||||
|
||||
@@ -188,7 +188,9 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getNamespace() { return namespace; }
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
public NamespaceAndName getNamespaceAndName() {
|
||||
@@ -267,7 +269,9 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
public void setNamespace(String namespace) { this.namespace = namespace; }
|
||||
public void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
|
||||
@@ -27,7 +27,7 @@ package sonia.scm.repository;
|
||||
/**
|
||||
* Implementations of this class can be used to check whether a repository is archived.
|
||||
*
|
||||
* @since 1.12.0
|
||||
* @since 2.12.0
|
||||
*/
|
||||
public interface RepositoryArchivedCheck {
|
||||
|
||||
|
||||
@@ -22,10 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
@@ -34,7 +33,7 @@ public class RepositoryArchivedException extends ExceptionWithContext {
|
||||
|
||||
public static final String CODE = "3hSIlptme1";
|
||||
|
||||
protected RepositoryArchivedException(Repository repository) {
|
||||
public RepositoryArchivedException(Repository repository) {
|
||||
super(entity(repository).build(), format("Repository %s is marked as archived and must not be modified", repository));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Implementations of this class can be used to check whether a repository is currently being exported.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public interface RepositoryExportingCheck {
|
||||
|
||||
/**
|
||||
* Checks whether the repository with the given id is currently (that is, at this moment) being exported or not.
|
||||
* @param repositoryId The id of the repository to check.
|
||||
* @return <code>true</code> when the repository with the given id is currently being exported, <code>false</code>
|
||||
* otherwise.
|
||||
*/
|
||||
boolean isExporting(String repositoryId);
|
||||
|
||||
/**
|
||||
* Checks whether the given repository is currently (that is, at this moment) being exported or not. This checks the
|
||||
* status on behalf of the id of the repository, not by the exporting flag provided by the repository itself.
|
||||
* @param repository The repository to check.
|
||||
* @return <code>true</code> when the given repository is currently being exported, <code>false</code> otherwise.
|
||||
*/
|
||||
default boolean isExporting(Repository repository) {
|
||||
return isExporting(repository.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given repository is marked as being exported during the execution of the given callback.
|
||||
* @param repository The repository that will be marked as being exported.
|
||||
* @param callback This callback will be executed.
|
||||
* @param <T> The return type of the callback.
|
||||
* @return The result of the callback.
|
||||
*/
|
||||
<T> T withExportingLock(Repository repository, Supplier<T> callback);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
public class RepositoryExportingException extends ExceptionWithContext {
|
||||
|
||||
public static final String CODE = "1mSNlpe1V1";
|
||||
|
||||
public RepositoryExportingException(Repository repository) {
|
||||
super(entity(repository).build(), format("Repository %s is currently being exported and must not be modified", repository));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static sonia.scm.repository.DefaultRepositoryExportingCheck.isRepositoryExporting;
|
||||
import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.isRepositoryArchived;
|
||||
|
||||
/**
|
||||
@@ -64,11 +65,14 @@ public class RepositoryPermissionGuard implements PermissionGuard<Repository> {
|
||||
if (isRepositoryArchived(id)) {
|
||||
throw new AuthorizationException("repository is archived");
|
||||
}
|
||||
if (isRepositoryExporting(id)) {
|
||||
throw new AuthorizationException("repository is exporting");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate) {
|
||||
return !isRepositoryArchived(id) && delegate.getAsBoolean();
|
||||
return !isRepositoryArchived(id) && !isRepositoryExporting(id) && delegate.getAsBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Checks, whether a repository has to be considered read only. Currently, this includes {@link RepositoryArchivedCheck}
|
||||
* and {@link RepositoryExportingCheck}.
|
||||
*
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public final class RepositoryReadOnlyChecker {
|
||||
|
||||
private final RepositoryArchivedCheck archivedCheck;
|
||||
private final RepositoryExportingCheck exportingCheck;
|
||||
|
||||
@Inject
|
||||
public RepositoryReadOnlyChecker(RepositoryArchivedCheck archivedCheck, RepositoryExportingCheck exportingCheck) {
|
||||
this.archivedCheck = archivedCheck;
|
||||
this.exportingCheck = exportingCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the repository is read only.
|
||||
* @param repository The repository to check.
|
||||
* @return <code>true</code> if any check locks the repository to read only access.
|
||||
*/
|
||||
public boolean isReadOnly(Repository repository) {
|
||||
return isReadOnly(repository.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the repository for the given id is read only.
|
||||
* @param repositoryId The id of the given repository to check.
|
||||
* @return <code>true</code> if any check locks the repository to read only access.
|
||||
*/
|
||||
public boolean isReadOnly(String repositoryId) {
|
||||
return archivedCheck.isArchived(repositoryId) || exportingCheck.isExporting(repositoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the repository may be modified.
|
||||
*
|
||||
* @throws RepositoryArchivedException if the repository is archived
|
||||
* @throws RepositoryExportingException if the repository is currently being exported
|
||||
*/
|
||||
public static void checkReadOnly(Repository repository) {
|
||||
if (isArchived(repository)) {
|
||||
throw new RepositoryArchivedException(repository);
|
||||
}
|
||||
if (isExporting(repository)) {
|
||||
throw new RepositoryExportingException(repository);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isExporting(Repository repository) {
|
||||
return DefaultRepositoryExportingCheck.isRepositoryExporting(repository.getId());
|
||||
}
|
||||
|
||||
private static boolean isArchived(Repository repository) {
|
||||
return repository.isArchived() || EventDrivenRepositoryArchiveCheck.isRepositoryArchived(repository.getId());
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,9 @@ import com.google.common.io.ByteSink;
|
||||
import com.google.common.io.Files;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.spi.BundleCommand;
|
||||
import sonia.scm.repository.spi.BundleCommandRequest;
|
||||
|
||||
@@ -63,12 +65,13 @@ public final class BundleCommandBuilder {
|
||||
|
||||
/**
|
||||
* Constructs a new {@link BundleCommandBuilder}.
|
||||
*
|
||||
* @param bundleCommand bundle command implementation
|
||||
* @param bundleCommand bundle command implementation
|
||||
* @param repositoryExportingCheck
|
||||
* @param repository repository
|
||||
*/
|
||||
BundleCommandBuilder(BundleCommand bundleCommand, Repository repository) {
|
||||
BundleCommandBuilder(BundleCommand bundleCommand, RepositoryExportingCheck repositoryExportingCheck, Repository repository) {
|
||||
this.bundleCommand = bundleCommand;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@@ -79,9 +82,8 @@ public final class BundleCommandBuilder {
|
||||
*
|
||||
* @param outputFile output file
|
||||
* @return bundle response
|
||||
* @throws IOException
|
||||
*/
|
||||
public BundleResponse bundle(File outputFile) throws IOException {
|
||||
public BundleResponse bundle(File outputFile) {
|
||||
checkArgument((outputFile != null) && !outputFile.exists(),
|
||||
"file is null or exists already");
|
||||
|
||||
@@ -91,7 +93,7 @@ public final class BundleCommandBuilder {
|
||||
logger.info("create bundle at {} for repository {}", outputFile,
|
||||
repository.getId());
|
||||
|
||||
return bundleCommand.bundle(request);
|
||||
return bundleWithExportingLock(request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,16 +101,14 @@ public final class BundleCommandBuilder {
|
||||
*
|
||||
* @param outputStream output stream
|
||||
* @return bundle response
|
||||
* @throws IOException
|
||||
*/
|
||||
public BundleResponse bundle(OutputStream outputStream)
|
||||
throws IOException {
|
||||
public BundleResponse bundle(OutputStream outputStream) {
|
||||
checkNotNull(outputStream, "output stream is required");
|
||||
|
||||
logger.info("bundle {} to output stream", repository);
|
||||
|
||||
return bundleCommand.bundle(
|
||||
new BundleCommandRequest(asByteSink(outputStream)));
|
||||
BundleCommandRequest request = new BundleCommandRequest(asByteSink(outputStream));
|
||||
return bundleWithExportingLock(request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,14 +116,23 @@ public final class BundleCommandBuilder {
|
||||
*
|
||||
* @param sink byte sink
|
||||
* @return bundle response
|
||||
* @throws IOException
|
||||
*/
|
||||
public BundleResponse bundle(ByteSink sink)
|
||||
throws IOException {
|
||||
public BundleResponse bundle(ByteSink sink) {
|
||||
checkNotNull(sink, "byte sink is required");
|
||||
logger.info("bundle {} to byte sink", sink);
|
||||
|
||||
return bundleCommand.bundle(new BundleCommandRequest(sink));
|
||||
BundleCommandRequest request = new BundleCommandRequest(sink);
|
||||
return bundleWithExportingLock(request);
|
||||
}
|
||||
|
||||
private BundleResponse bundleWithExportingLock(BundleCommandRequest request) {
|
||||
return repositoryExportingCheck.withExportingLock(repository, () -> {
|
||||
try {
|
||||
return bundleCommand.bundle(request);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "Exception during bundle; does not necessarily indicate a problem with the repository", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,4 +171,6 @@ public final class BundleCommandBuilder {
|
||||
* repository
|
||||
*/
|
||||
private final Repository repository;
|
||||
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
import sonia.scm.repository.work.WorkdirProvider;
|
||||
import sonia.scm.security.Authentications;
|
||||
@@ -93,15 +95,18 @@ public final class RepositoryService implements Closeable {
|
||||
|
||||
@Nullable
|
||||
private final EMail eMail;
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryService}. This constructor should only
|
||||
* be called from the {@link RepositoryServiceFactory}.
|
||||
* @param cacheManager cache manager
|
||||
* @param provider implementation for {@link RepositoryServiceProvider}
|
||||
* @param repository the repository
|
||||
* @param workdirProvider provider for workdirs
|
||||
* @param eMail utility to compute email addresses if missing
|
||||
*
|
||||
* @param cacheManager cache manager
|
||||
* @param provider implementation for {@link RepositoryServiceProvider}
|
||||
* @param repository the repository
|
||||
* @param workdirProvider provider for workdirs
|
||||
* @param eMail utility to compute email addresses if missing
|
||||
* @param repositoryExportingCheck
|
||||
*/
|
||||
RepositoryService(CacheManager cacheManager,
|
||||
RepositoryServiceProvider provider,
|
||||
@@ -109,7 +114,7 @@ public final class RepositoryService implements Closeable {
|
||||
PreProcessorUtil preProcessorUtil,
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
|
||||
WorkdirProvider workdirProvider,
|
||||
@Nullable EMail eMail) {
|
||||
@Nullable EMail eMail, RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this.cacheManager = cacheManager;
|
||||
this.provider = provider;
|
||||
this.repository = repository;
|
||||
@@ -117,6 +122,7 @@ public final class RepositoryService implements Closeable {
|
||||
this.protocolProviders = protocolProviders;
|
||||
this.workdirProvider = workdirProvider;
|
||||
this.eMail = eMail;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +188,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BranchCommandBuilder getBranchCommand() {
|
||||
verifyNotArchived();
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
RepositoryPermissions.push(getRepository()).check();
|
||||
LOG.debug("create branch command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
@@ -217,7 +223,7 @@ public final class RepositoryService implements Closeable {
|
||||
LOG.debug("create bundle command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BundleCommandBuilder(provider.getBundleCommand(), repository);
|
||||
return new BundleCommandBuilder(provider.getBundleCommand(), repositoryExportingCheck, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,7 +311,7 @@ public final class RepositoryService implements Closeable {
|
||||
*/
|
||||
public ModificationsCommandBuilder getModificationsCommand() {
|
||||
LOG.debug("create modifications command for repository {}", repository);
|
||||
return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
|
||||
return new ModificationsCommandBuilder(provider.getModificationsCommand(), repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,7 +339,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.31
|
||||
*/
|
||||
public PullCommandBuilder getPullCommand() {
|
||||
verifyNotArchived();
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create pull command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
@@ -383,12 +389,11 @@ public final class RepositoryService implements Closeable {
|
||||
* The tag command allows the management of repository tags.
|
||||
*
|
||||
* @return instance of {@link TagCommandBuilder}
|
||||
*
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public TagCommandBuilder getTagCommand() {
|
||||
verifyNotArchived();
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
return new TagCommandBuilder(provider.getTagCommand());
|
||||
}
|
||||
|
||||
@@ -418,7 +423,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public MergeCommandBuilder getMergeCommand() {
|
||||
verifyNotArchived();
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create merge command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
@@ -440,7 +445,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public ModifyCommandBuilder getModifyCommand() {
|
||||
verifyNotArchived();
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create modify command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
@@ -489,12 +494,6 @@ public final class RepositoryService implements Closeable {
|
||||
.filter(protocol -> !Authentications.isAuthenticatedSubjectAnonymous() || protocol.isAnonymousEnabled());
|
||||
}
|
||||
|
||||
private void verifyNotArchived() {
|
||||
if (getRepository().isArchived()) {
|
||||
throw new RepositoryArchivedException(getRepository());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"})
|
||||
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
|
||||
return protocolProvider.get(repository);
|
||||
@@ -507,6 +506,6 @@ public final class RepositoryService implements Closeable {
|
||||
// no idea how to fix this, without cast
|
||||
.map(p -> (T) p)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType())));
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(), getRepository().getType())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +43,14 @@ import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.ClearRepositoryCacheEvent;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryCacheKeyPredicate;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
@@ -123,6 +125,7 @@ public final class RepositoryServiceFactory {
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"})
|
||||
private final Set<ScmProtocolProvider> protocolProviders;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
@Nullable
|
||||
private final EMail eMail;
|
||||
@@ -141,7 +144,7 @@ public final class RepositoryServiceFactory {
|
||||
* @param protocolProviders providers for repository protocols
|
||||
* @param workdirProvider provider for working directories
|
||||
*
|
||||
* @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail)} instead
|
||||
* @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail, RepositoryExportingCheck)} instead
|
||||
* @since 1.21
|
||||
*/
|
||||
@Deprecated
|
||||
@@ -152,7 +155,8 @@ public final class RepositoryServiceFactory {
|
||||
WorkdirProvider workdirProvider) {
|
||||
this(
|
||||
cacheManager, repositoryManager, resolvers,
|
||||
preProcessorUtil, protocolProviders, workdirProvider, null, ScmEventBus.getInstance()
|
||||
preProcessorUtil, protocolProviders, workdirProvider, null, ScmEventBus.getInstance(),
|
||||
new DefaultRepositoryExportingCheck()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,11 +178,12 @@ public final class RepositoryServiceFactory {
|
||||
public RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager,
|
||||
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
|
||||
WorkdirProvider workdirProvider, EMail eMail) {
|
||||
WorkdirProvider workdirProvider, EMail eMail,
|
||||
RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this(
|
||||
cacheManager, repositoryManager, resolvers,
|
||||
preProcessorUtil, protocolProviders, workdirProvider,
|
||||
eMail, ScmEventBus.getInstance()
|
||||
eMail, ScmEventBus.getInstance(), repositoryExportingCheck
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,7 +192,8 @@ public final class RepositoryServiceFactory {
|
||||
RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager,
|
||||
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"}) Set<ScmProtocolProvider> protocolProviders,
|
||||
WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus) {
|
||||
WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus,
|
||||
RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this.cacheManager = cacheManager;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.resolvers = resolvers;
|
||||
@@ -195,6 +201,7 @@ public final class RepositoryServiceFactory {
|
||||
this.protocolProviders = protocolProviders;
|
||||
this.workdirProvider = workdirProvider;
|
||||
this.eMail = eMail;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
|
||||
eventBus.register(new CacheClearHook(cacheManager));
|
||||
}
|
||||
@@ -284,7 +291,7 @@ public final class RepositoryServiceFactory {
|
||||
}
|
||||
|
||||
service = new RepositoryService(cacheManager, provider, repository,
|
||||
preProcessorUtil, protocolProviders, workdirProvider, eMail);
|
||||
preProcessorUtil, protocolProviders, workdirProvider, eMail, repositoryExportingCheck);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class DefaultRepositoryExportingCheckTest {
|
||||
|
||||
private static final Repository EXPORTING_REPOSITORY = new Repository("exporting_hog", "git", "hitchhiker", "hog");
|
||||
|
||||
private final DefaultRepositoryExportingCheck check = new DefaultRepositoryExportingCheck();
|
||||
|
||||
@Test
|
||||
void shouldBeReadOnlyIfBeingExported() {
|
||||
check.withExportingLock(EXPORTING_REPOSITORY, () -> {
|
||||
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
||||
assertThat(readOnly).isTrue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeReadOnlyIfBeingExportedMultipleTimes() {
|
||||
check.withExportingLock(EXPORTING_REPOSITORY, () -> {
|
||||
check.withExportingLock(EXPORTING_REPOSITORY, () -> {
|
||||
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
||||
assertThat(readOnly).isTrue();
|
||||
return null;
|
||||
});
|
||||
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
||||
assertThat(readOnly).isTrue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeReadOnlyIfNotBeingExported() {
|
||||
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
||||
assertThat(readOnly).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,10 @@ import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.setAsArchiv
|
||||
class EventDrivenRepositoryArchiveCheckTest {
|
||||
|
||||
private static final Repository NORMAL_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog");
|
||||
private static final Repository ARCHIVED_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog");
|
||||
private static final Repository ARCHIVED_REPOSITORY = new Repository("archived_hog", "git", "hitchhiker", "hog");
|
||||
static {
|
||||
ARCHIVED_REPOSITORY.setArchived(true);
|
||||
EventDrivenRepositoryArchiveCheck.setAsArchived(ARCHIVED_REPOSITORY.getId());
|
||||
}
|
||||
|
||||
EventDrivenRepositoryArchiveCheck check = new EventDrivenRepositoryArchiveCheck();
|
||||
@@ -53,7 +54,7 @@ class EventDrivenRepositoryArchiveCheckTest {
|
||||
@Test
|
||||
void shouldBeArchivedAfterFlagHasBeenSet() {
|
||||
check.updateListener(new RepositoryModificationEvent(HandlerEventType.MODIFY, ARCHIVED_REPOSITORY, NORMAL_REPOSITORY));
|
||||
assertThat(check.isArchived("hog")).isTrue();
|
||||
assertThat(check.isArchived("archived_hog")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,6 +71,18 @@ class EventDrivenRepositoryArchiveCheckTest {
|
||||
|
||||
new EventDrivenRepositoryArchiveCheckInitializer(repositoryDAO).init(null);
|
||||
|
||||
assertThat(check.isArchived("hog")).isTrue();
|
||||
assertThat(check.isArchived("archived_hog")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeReadOnly() {
|
||||
boolean readOnly = check.isArchived(ARCHIVED_REPOSITORY);
|
||||
assertThat(readOnly).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeReadOnly() {
|
||||
boolean readOnly = check.isArchived(NORMAL_REPOSITORY);
|
||||
assertThat(readOnly).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,16 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.api.extension.InvocationInterceptor;
|
||||
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@@ -58,7 +62,7 @@ class RepositoryPermissionGuardTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setReadOnlyVerbs() {
|
||||
RepositoryPermissionGuard.setReadOnlyVerbs(asList("read"));
|
||||
RepositoryPermissionGuard.setReadOnlyVerbs(singletonList("read"));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -142,5 +146,47 @@ class RepositoryPermissionGuardTest {
|
||||
verify(checkDelegate).run();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ExtendWith(WrapInExportCheck.class)
|
||||
class WithExportingRepository {
|
||||
|
||||
@Test
|
||||
void shouldInterceptPermissionCheck() {
|
||||
assertThat(readInterceptor.isPermitted(subject, "1", permittedDelegate)).isFalse();
|
||||
|
||||
verify(permittedDelegate, never()).getAsBoolean();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInterceptCheckRequest() {
|
||||
assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowConcretePermissionExceptionOverArchiveException() {
|
||||
doThrow(new AuthorizationException()).when(checkDelegate).run();
|
||||
|
||||
assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate));
|
||||
|
||||
verify(checkDelegate).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrapInExportCheck implements InvocationInterceptor {
|
||||
|
||||
public void interceptTestMethod(Invocation<Void> invocation,
|
||||
ReflectiveInvocationContext<Method> invocationContext,
|
||||
ExtensionContext extensionContext) {
|
||||
new DefaultRepositoryExportingCheck().withExportingLock(new Repository("1", "git", "space", "X"), () -> {
|
||||
try {
|
||||
invocation.proceed();
|
||||
return null;
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RepositoryReadOnlyCheckerTest {
|
||||
|
||||
private final Repository repository = new Repository("1", "git","hitchhiker", "HeartOfGold");
|
||||
|
||||
private boolean archived = false;
|
||||
private boolean exporting = false;
|
||||
|
||||
private final RepositoryArchivedCheck archivedCheck = repositoryId -> archived;
|
||||
private final RepositoryExportingCheck exportingCheck = new RepositoryExportingCheck() {
|
||||
@Override
|
||||
public boolean isExporting(String repositoryId) {
|
||||
return exporting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T withExportingLock(Repository repository, Supplier<T> callback) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private final RepositoryReadOnlyChecker checker = new RepositoryReadOnlyChecker(archivedCheck, exportingCheck);
|
||||
|
||||
@Test
|
||||
void shouldReturnFalseIfAllChecksFalse() {
|
||||
boolean readOnly = checker.isReadOnly(repository);
|
||||
|
||||
assertThat(readOnly).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrueIfArchivedIsTrue() {
|
||||
archived = true;
|
||||
|
||||
boolean readOnly = checker.isReadOnly(repository);
|
||||
|
||||
assertThat(readOnly).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrueIfExportingIsTrue() {
|
||||
exporting = true;
|
||||
|
||||
boolean readOnly = checker.isReadOnly(repository);
|
||||
|
||||
assertThat(readOnly).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import sonia.scm.NotFoundException;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -94,7 +95,8 @@ class RepositoryServiceFactoryTest {
|
||||
return new RepositoryServiceFactory(
|
||||
cacheManager, repositoryManager, builder.build(),
|
||||
preProcessorUtil, ImmutableSet.of(), workdirProvider,
|
||||
new EMail(new ScmConfiguration()), eventBus
|
||||
new EMail(new ScmConfiguration()), eventBus,
|
||||
new DefaultRepositoryExportingCheck()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryArchivedException;
|
||||
import sonia.scm.repository.RepositoryExportingException;
|
||||
import sonia.scm.repository.spi.HttpScmProtocol;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
import sonia.scm.user.EMail;
|
||||
@@ -76,7 +79,7 @@ class RepositoryServiceTest {
|
||||
@Test
|
||||
void shouldReturnMatchingProtocolsFromProvider() {
|
||||
when(subject.getPrincipal()).thenReturn("Hitchhiker");
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||
Stream<ScmProtocol> supportedProtocols = repositoryService.getSupportedProtocols();
|
||||
|
||||
assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
|
||||
@@ -85,7 +88,7 @@ class RepositoryServiceTest {
|
||||
@Test
|
||||
void shouldFilterOutNonAnonymousEnabledProtocolsForAnonymousUser() {
|
||||
when(subject.getPrincipal()).thenReturn(SCMContext.USER_ANONYMOUS);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Stream.of(new DummyScmProtocolProvider(), new DummyScmProtocolProvider(false)).collect(Collectors.toSet()), null, eMail);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Stream.of(new DummyScmProtocolProvider(), new DummyScmProtocolProvider(false)).collect(Collectors.toSet()), null, eMail, null);
|
||||
Stream<ScmProtocol> supportedProtocols = repositoryService.getSupportedProtocols();
|
||||
|
||||
assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
|
||||
@@ -94,7 +97,7 @@ class RepositoryServiceTest {
|
||||
@Test
|
||||
void shouldFindKnownProtocol() {
|
||||
when(subject.getPrincipal()).thenReturn("Hitchhiker");
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||
|
||||
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
|
||||
|
||||
@@ -104,7 +107,7 @@ class RepositoryServiceTest {
|
||||
@Test
|
||||
void shouldFailForUnknownProtocol() {
|
||||
when(subject.getPrincipal()).thenReturn("Hitchhiker");
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class));
|
||||
}
|
||||
@@ -112,14 +115,29 @@ class RepositoryServiceTest {
|
||||
@Test
|
||||
void shouldFailForArchivedRepository() {
|
||||
repository.setArchived(true);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail);
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand());
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getBranchCommand());
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getPullCommand());
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getTagCommand());
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getMergeCommand());
|
||||
assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand());
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getBranchCommand);
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getPullCommand);
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getTagCommand);
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getMergeCommand);
|
||||
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForExportingRepository() {
|
||||
new DefaultRepositoryExportingCheck().withExportingLock(repository, () -> {
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getBranchCommand);
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getPullCommand);
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getTagCommand);
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getMergeCommand);
|
||||
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static class DummyHttpProtocol extends HttpScmProtocol {
|
||||
@@ -141,7 +159,7 @@ class RepositoryServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DummyScmProtocolProvider implements ScmProtocolProvider {
|
||||
private static class DummyScmProtocolProvider implements ScmProtocolProvider<ScmProtocol> {
|
||||
|
||||
private final boolean anonymousEnabled;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.store.StoreReadOnlyException;
|
||||
|
||||
@@ -54,14 +55,16 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
|
||||
private final PathBasedRepositoryLocationResolver repositoryLocationResolver;
|
||||
private final FileSystem fileSystem;
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
private final Map<String, Repository> byId;
|
||||
private final Map<NamespaceAndName, Repository> byNamespaceAndName;
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) {
|
||||
public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem, RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this.repositoryLocationResolver = repositoryLocationResolver;
|
||||
this.fileSystem = fileSystem;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
|
||||
this.byId = new ConcurrentHashMap<>();
|
||||
this.byNamespaceAndName = new ConcurrentHashMap<>();
|
||||
@@ -140,7 +143,7 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
@Override
|
||||
public void modify(Repository repository) {
|
||||
Repository clone = repository.clone();
|
||||
if (clone.isArchived() && byId.get(clone.getId()).isArchived()) {
|
||||
if (mustNotModifyRepository(clone)) {
|
||||
throw new StoreReadOnlyException(repository);
|
||||
}
|
||||
|
||||
@@ -160,9 +163,14 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
metadataStore.write(repositoryPath, clone);
|
||||
}
|
||||
|
||||
private boolean mustNotModifyRepository(Repository clone) {
|
||||
return clone.isArchived() && byId.get(clone.getId()).isArchived()
|
||||
|| repositoryExportingCheck.isExporting(clone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Repository repository) {
|
||||
if (repository.isArchived()) {
|
||||
if (repository.isArchived() || repositoryExportingCheck.isExporting(repository)) {
|
||||
throw new StoreReadOnlyException(repository);
|
||||
}
|
||||
Path path;
|
||||
|
||||
@@ -29,8 +29,8 @@ package sonia.scm.store;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.File;
|
||||
@@ -52,13 +52,13 @@ public abstract class FileBasedStoreFactory {
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final RepositoryLocationResolver repositoryLocationResolver;
|
||||
private final Store store;
|
||||
private final RepositoryArchivedCheck archivedCheck;
|
||||
private final RepositoryReadOnlyChecker readOnlyChecker;
|
||||
|
||||
protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store, RepositoryArchivedCheck archivedCheck) {
|
||||
protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.repositoryLocationResolver = repositoryLocationResolver;
|
||||
this.store = store;
|
||||
this.archivedCheck = archivedCheck;
|
||||
this.readOnlyChecker = readOnlyChecker;
|
||||
}
|
||||
|
||||
protected File getStoreLocation(StoreParameters storeParameters) {
|
||||
@@ -83,12 +83,13 @@ public abstract class FileBasedStoreFactory {
|
||||
}
|
||||
|
||||
protected boolean mustBeReadOnly(StoreParameters storeParameters) {
|
||||
return storeParameters.getRepositoryId() != null && archivedCheck.isArchived(storeParameters.getRepositoryId());
|
||||
return storeParameters.getRepositoryId() != null && readOnlyChecker.isReadOnly(storeParameters.getRepositoryId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store directory of a specific repository
|
||||
* @param store the type of the store
|
||||
*
|
||||
* @param store the type of the store
|
||||
* @param repositoryId the id of the repossitory
|
||||
* @return the store directory of a specific repository
|
||||
*/
|
||||
@@ -98,6 +99,7 @@ public abstract class FileBasedStoreFactory {
|
||||
|
||||
/**
|
||||
* Get the global store directory
|
||||
*
|
||||
* @param store the type of the store
|
||||
* @return the global store directory
|
||||
*/
|
||||
|
||||
@@ -28,11 +28,9 @@ 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.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
@@ -46,11 +44,6 @@ import java.io.File;
|
||||
@Singleton
|
||||
public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory {
|
||||
|
||||
/**
|
||||
* the logger for FileBlobStoreFactory
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileBlobStoreFactory.class);
|
||||
|
||||
private final KeyGenerator keyGenerator;
|
||||
|
||||
/**
|
||||
@@ -60,8 +53,8 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
|
||||
* @param keyGenerator key generator
|
||||
*/
|
||||
@Inject
|
||||
public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.BLOB, archivedCheck);
|
||||
public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.BLOB, readOnlyChecker);
|
||||
this.keyGenerator = keyGenerator;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ package sonia.scm.store;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -46,8 +46,8 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
|
||||
private KeyGenerator keyGenerator;
|
||||
|
||||
@Inject
|
||||
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
|
||||
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker);
|
||||
this.keyGenerator = keyGenerator;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ package sonia.scm.store;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
|
||||
/**
|
||||
* JAXB implementation of {@link ConfigurationStoreFactory}.
|
||||
@@ -44,8 +44,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
|
||||
* @param repositoryLocationResolver Resolver to get the repository Directory
|
||||
*/
|
||||
@Inject
|
||||
public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryArchivedCheck archivedCheck) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck);
|
||||
public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,8 +29,8 @@ package sonia.scm.store;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
@@ -47,8 +47,8 @@ public class JAXBDataStoreFactory extends FileBasedStoreFactory
|
||||
private final KeyGenerator keyGenerator;
|
||||
|
||||
@Inject
|
||||
public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.DATA, archivedCheck);
|
||||
public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
super(contextProvider, repositoryLocationResolver, Store.DATA, readOnlyChecker);
|
||||
this.keyGenerator = keyGenerator;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -54,6 +55,8 @@ class XmlRepositoryDAOSynchronizationTest {
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider provider;
|
||||
@Mock
|
||||
private RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
private FileSystem fileSystem;
|
||||
private PathBasedRepositoryLocationResolver resolver;
|
||||
@@ -75,7 +78,7 @@ class XmlRepositoryDAOSynchronizationTest {
|
||||
provider, new InitialRepositoryLocationResolver(), fileSystem
|
||||
);
|
||||
|
||||
repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem);
|
||||
repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +91,7 @@ class XmlRepositoryDAOSynchronizationTest {
|
||||
}
|
||||
|
||||
private void assertCreated() {
|
||||
XmlRepositoryDAO assertionDao = new XmlRepositoryDAO(resolver, fileSystem);
|
||||
XmlRepositoryDAO assertionDao = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
|
||||
assertThat(assertionDao.getAll()).hasSize(CREATION_COUNT);
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ class XmlRepositoryDAOSynchronizationTest {
|
||||
void shouldCreateALotOfRepositoriesInParallel() throws InterruptedException {
|
||||
ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
|
||||
final XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem);
|
||||
final XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
|
||||
for (int i=0; i<CREATION_COUNT; i++) {
|
||||
executors.submit(create(repositoryDAO, i));
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.store.StoreReadOnlyException;
|
||||
@@ -72,8 +73,10 @@ class XmlRepositoryDAOTest {
|
||||
@Mock
|
||||
private PathBasedRepositoryLocationResolver locationResolver;
|
||||
private Consumer<BiConsumer<String, Path>> triggeredOnForAllLocations = none -> {};
|
||||
@Mock
|
||||
private RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
private FileSystem fileSystem = new DefaultFileSystem();
|
||||
private final FileSystem fileSystem = new DefaultFileSystem();
|
||||
|
||||
private XmlRepositoryDAO dao;
|
||||
|
||||
@@ -120,7 +123,7 @@ class XmlRepositoryDAOTest {
|
||||
|
||||
@BeforeEach
|
||||
void createDAO() {
|
||||
dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -245,6 +248,15 @@ class XmlRepositoryDAOTest {
|
||||
assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotModifyExportingRepository() {
|
||||
when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true);
|
||||
dao.add(REPOSITORY);
|
||||
|
||||
Repository heartOfGold = createRepository("42");
|
||||
assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveRepository() {
|
||||
dao.add(REPOSITORY);
|
||||
@@ -268,6 +280,15 @@ class XmlRepositoryDAOTest {
|
||||
assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRemoveExportingRepository() {
|
||||
when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true);
|
||||
dao.add(REPOSITORY);
|
||||
assertThat(dao.contains("42")).isTrue();
|
||||
|
||||
assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenameTheRepository() {
|
||||
dao.add(REPOSITORY);
|
||||
@@ -317,8 +338,9 @@ class XmlRepositoryDAOTest {
|
||||
dao.add(REPOSITORY);
|
||||
|
||||
String content = getXmlFileContent(REPOSITORY.getId());
|
||||
assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
|
||||
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
|
||||
assertThat(content)
|
||||
.containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>")
|
||||
.containsSubsequence("vogons", "<verb>delete</verb>");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -372,7 +394,7 @@ class XmlRepositoryDAOTest {
|
||||
mockExistingPath();
|
||||
|
||||
// when
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
|
||||
|
||||
// then
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
@@ -383,7 +405,7 @@ class XmlRepositoryDAOTest {
|
||||
// given
|
||||
mockExistingPath();
|
||||
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
|
||||
|
||||
// when
|
||||
dao.refresh();
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.AbstractTestBase;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.security.UUIDKeyGenerator;
|
||||
|
||||
@@ -51,8 +51,8 @@ import static org.mockito.Mockito.when;
|
||||
class FileBlobStoreTest extends AbstractTestBase
|
||||
{
|
||||
|
||||
private Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
private RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||
private final Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
|
||||
private BlobStore store;
|
||||
|
||||
@BeforeEach
|
||||
@@ -191,7 +191,7 @@ class FileBlobStoreTest extends AbstractTestBase
|
||||
@BeforeEach
|
||||
void setRepositoryArchived() {
|
||||
store.create("1"); // store for test must not be empty
|
||||
when(archivedCheck.isArchived(repository.getId())).thenReturn(true);
|
||||
when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true);
|
||||
createBlobStore();
|
||||
}
|
||||
|
||||
@@ -227,6 +227,6 @@ class FileBlobStoreTest extends AbstractTestBase
|
||||
|
||||
protected BlobStoreFactory createBlobStoreFactory()
|
||||
{
|
||||
return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck);
|
||||
return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), readOnlyChecker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ package sonia.scm.store;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -41,17 +41,16 @@ import static org.mockito.Mockito.when;
|
||||
*/
|
||||
public class JAXBConfigurationStoreTest extends StoreTestBase {
|
||||
|
||||
private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
|
||||
|
||||
@Override
|
||||
protected ConfigurationStoreFactory createStoreFactory()
|
||||
{
|
||||
return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, archivedCheck);
|
||||
return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldStoreAndLoadInRepository()
|
||||
{
|
||||
Repository repository = new Repository("id", "git", "ns", "n");
|
||||
@@ -70,17 +69,17 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
|
||||
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldNotWriteArchivedRepository()
|
||||
{
|
||||
Repository repository = new Repository("id", "git", "ns", "n");
|
||||
when(archivedCheck.isArchived("id")).thenReturn(true);
|
||||
when(readOnlyChecker.isReadOnly("id")).thenReturn(true);
|
||||
ConfigurationStore<StoreObject> store = createStoreFactory()
|
||||
.withType(StoreObject.class)
|
||||
.withName("test")
|
||||
.forRepository(repository)
|
||||
.build();
|
||||
|
||||
assertThrows(RuntimeException.class, () -> store.set(new StoreObject("value")));
|
||||
StoreObject storeObject = new StoreObject("value");
|
||||
assertThrows(RuntimeException.class, () -> store.set(storeObject));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ package sonia.scm.store;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.security.UUIDKeyGenerator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -42,12 +42,12 @@ import static org.mockito.Mockito.when;
|
||||
*/
|
||||
public class JAXBDataStoreTest extends DataStoreTestBase {
|
||||
|
||||
private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
|
||||
|
||||
@Override
|
||||
protected DataStoreFactory createDataStoreFactory()
|
||||
{
|
||||
return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck);
|
||||
return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), readOnlyChecker);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,7 +80,7 @@ public class JAXBDataStoreTest extends DataStoreTestBase {
|
||||
@Test(expected = StoreReadOnlyException.class)
|
||||
public void shouldNotStoreForReadOnlyRepository()
|
||||
{
|
||||
when(archivedCheck.isArchived(repository.getId())).thenReturn(true);
|
||||
when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true);
|
||||
getDataStore(StoreObject.class, repository).put("abc", new StoreObject("abc_value"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryReadOnlyChecker;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
import sonia.scm.update.RepositoryV1PropertyReader;
|
||||
@@ -111,8 +111,8 @@ class XmlV1PropertyDAOTest {
|
||||
Files.createDirectories(configPath);
|
||||
Path propFile = configPath.resolve("repository-properties-v1.xml");
|
||||
Files.write(propFile, PROPERTIES.getBytes());
|
||||
RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class);
|
||||
XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), archivedCheck));
|
||||
RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
|
||||
XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), readOnlyChecker));
|
||||
|
||||
dao.getProperties(new RepositoryV1PropertyReader())
|
||||
.forEachEntry((key, prop) -> {
|
||||
|
||||
@@ -48475,7 +48475,7 @@ exports[`Storyshots RepositoryEntry Archived 1`] = `
|
||||
</strong>
|
||||
|
||||
<span
|
||||
className="RepositoryEntry__ArchiveTag-sc-6jys82-0 lgBbpU"
|
||||
className="RepositoryEntry__RepositoryTag-sc-6jys82-0 cFBCjw"
|
||||
title="archive.tooltip"
|
||||
>
|
||||
repository.archived
|
||||
@@ -49023,6 +49023,314 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots RepositoryEntry Exporting 1`] = `
|
||||
<div
|
||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||
>
|
||||
<a
|
||||
className="overlay-column"
|
||||
href="/repo/hitchhiker/heartOfGold"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<article
|
||||
className="CardColumn__NoEventWrapper-sc-1w6lsih-0 eUWboI media"
|
||||
>
|
||||
<figure
|
||||
className="CardColumn__AvatarWrapper-sc-1w6lsih-1 lhzEPm media-left"
|
||||
>
|
||||
<p
|
||||
className="image is-64x64"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</p>
|
||||
</figure>
|
||||
<div
|
||||
className="CardColumn__FlexFullHeight-sc-1w6lsih-2 hWRPir media-content text-box is-flex"
|
||||
>
|
||||
<div
|
||||
className="is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__ContentLeft-sc-1w6lsih-4 iRVRBC content"
|
||||
>
|
||||
<p
|
||||
className="shorten-text is-marginless"
|
||||
>
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
<span
|
||||
className="RepositoryEntry__RepositoryTag-sc-6jys82-0 cFBCjw"
|
||||
title="exporting.tooltip"
|
||||
>
|
||||
repository.exporting
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
>
|
||||
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__FooterWrapper-sc-1w6lsih-3 hzknmV level is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 bnJfDV level-left is-hidden-mobile"
|
||||
>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 kdhCxo level-right is-block is-mobile is-marginless shorten-text"
|
||||
>
|
||||
<small
|
||||
className="level-item"
|
||||
>
|
||||
<time
|
||||
className="DateElement-sc-1schp8c-0 gkptML"
|
||||
title="2020-03-23 09:26:01"
|
||||
>
|
||||
3 days ago
|
||||
</time>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots RepositoryEntry MultiRepositoryTags 1`] = `
|
||||
<div
|
||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||
>
|
||||
<a
|
||||
className="overlay-column"
|
||||
href="/repo/hitchhiker/heartOfGold"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<article
|
||||
className="CardColumn__NoEventWrapper-sc-1w6lsih-0 eUWboI media"
|
||||
>
|
||||
<figure
|
||||
className="CardColumn__AvatarWrapper-sc-1w6lsih-1 lhzEPm media-left"
|
||||
>
|
||||
<p
|
||||
className="image is-64x64"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</p>
|
||||
</figure>
|
||||
<div
|
||||
className="CardColumn__FlexFullHeight-sc-1w6lsih-2 hWRPir media-content text-box is-flex"
|
||||
>
|
||||
<div
|
||||
className="is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__ContentLeft-sc-1w6lsih-4 iRVRBC content"
|
||||
>
|
||||
<p
|
||||
className="shorten-text is-marginless"
|
||||
>
|
||||
<strong>
|
||||
heartOfGold
|
||||
</strong>
|
||||
|
||||
<span
|
||||
className="RepositoryEntry__RepositoryTag-sc-6jys82-0 cFBCjw"
|
||||
title="archive.tooltip"
|
||||
>
|
||||
repository.archived
|
||||
</span>
|
||||
<span
|
||||
className="RepositoryEntry__RepositoryTag-sc-6jys82-0 cFBCjw"
|
||||
title="exporting.tooltip"
|
||||
>
|
||||
repository.exporting
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="shorten-text"
|
||||
>
|
||||
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__FooterWrapper-sc-1w6lsih-3 hzknmV level is-flex"
|
||||
>
|
||||
<div
|
||||
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 bnJfDV level-left is-hidden-mobile"
|
||||
>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 kdhCxo level-right is-block is-mobile is-marginless shorten-text"
|
||||
>
|
||||
<small
|
||||
className="level-item"
|
||||
>
|
||||
<time
|
||||
className="DateElement-sc-1schp8c-0 gkptML"
|
||||
title="2020-03-23 09:26:01"
|
||||
>
|
||||
3 days ago
|
||||
</time>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
|
||||
<div
|
||||
className="RepositoryEntrystories__Spacing-toppdg-0 iIzVNZ box box-link-shadow"
|
||||
|
||||
@@ -75,6 +75,8 @@ const QuickLink = (
|
||||
);
|
||||
|
||||
const archivedRepository = { ...repository, archived: true };
|
||||
const exportingRepository = { ...repository, exporting: true };
|
||||
const archivedExportingRepository = { ...repository, archived: true, exporting: true };
|
||||
|
||||
storiesOf("RepositoryEntry", module)
|
||||
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||
@@ -101,4 +103,14 @@ storiesOf("RepositoryEntry", module)
|
||||
const binder = new Binder("title");
|
||||
bindAvatar(binder, Git);
|
||||
return withBinder(binder, archivedRepository);
|
||||
})
|
||||
.add("Exporting", () => {
|
||||
const binder = new Binder("title");
|
||||
bindAvatar(binder, Git);
|
||||
return withBinder(binder, exportingRepository);
|
||||
})
|
||||
.add("MultiRepositoryTags", () => {
|
||||
const binder = new Binder("title");
|
||||
bindAvatar(binder, Git);
|
||||
return withBinder(binder, archivedExportingRepository);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ type Props = WithTranslation & {
|
||||
baseDate?: DateProp;
|
||||
};
|
||||
|
||||
const ArchiveTag = styled.span`
|
||||
const RepositoryTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #9a9a9a;
|
||||
padding: 0.25rem;
|
||||
@@ -145,13 +145,19 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
|
||||
createTitle = () => {
|
||||
const { repository, t } = this.props;
|
||||
const archivedFlag = repository.archived && (
|
||||
<ArchiveTag title={t("archive.tooltip")}>{t("repository.archived")}</ArchiveTag>
|
||||
);
|
||||
const repositoryFlags = [];
|
||||
if (repository.archived) {
|
||||
repositoryFlags.push(<RepositoryTag title={t("archive.tooltip")}>{t("repository.archived")}</RepositoryTag>);
|
||||
}
|
||||
|
||||
if (repository.exporting) {
|
||||
repositoryFlags.push(<RepositoryTag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryTag>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
|
||||
<strong>{repository.name}</strong> {archivedFlag}
|
||||
<strong>{repository.name}</strong> {repositoryFlags.map(flag => flag)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export type Repository = {
|
||||
creationDate?: string;
|
||||
lastModified?: string;
|
||||
archived?: boolean;
|
||||
exporting?: boolean;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"description": "Beschreibung",
|
||||
"creationDate": "Erstellt",
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"archived": "archiviert"
|
||||
"archived": "archiviert",
|
||||
"exporting": "Wird exportiert"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||
@@ -252,6 +253,7 @@
|
||||
},
|
||||
"export": {
|
||||
"subtitle": "Repository exportieren",
|
||||
"notification": "Achtung: Während eines laufenden Exports kann auf das Repository nur lesend zugegriffen werden.",
|
||||
"compressed": {
|
||||
"label": "Komprimieren",
|
||||
"helpText": "Export Datei vor dem Download komprimieren. Reduziert die Downloadgröße."
|
||||
@@ -399,6 +401,9 @@
|
||||
"archive": {
|
||||
"tooltip": "Nur lesender Zugriff möglich. Das Archiv kann nicht verändert werden."
|
||||
},
|
||||
"exporting": {
|
||||
"tooltip": "Nur lesender Zugriff möglich. Das Repository wird derzeit exportiert."
|
||||
},
|
||||
"diff": {
|
||||
"jumpToSource": "Zur Quelldatei springen",
|
||||
"jumpToTarget": "Zur vorherigen Version der Datei springen",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"description": "Description",
|
||||
"creationDate": "Creation Date",
|
||||
"lastModified": "Last Modified",
|
||||
"archived": "archived"
|
||||
"archived": "archived",
|
||||
"exporting": "exporting"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
@@ -252,6 +253,7 @@
|
||||
},
|
||||
"export": {
|
||||
"subtitle": "Repository Export",
|
||||
"notification": "Attention: During the export the repository cannot be modified.",
|
||||
"compressed": {
|
||||
"label": "Compress",
|
||||
"helpText": "Compress the export dump size to reduce the download size."
|
||||
@@ -399,6 +401,9 @@
|
||||
"archive": {
|
||||
"tooltip": "Read only. The archive cannot be changed."
|
||||
},
|
||||
"exporting": {
|
||||
"tooltip": "Read only. The repository is currently being exported."
|
||||
},
|
||||
"diff": {
|
||||
"changes": {
|
||||
"add": "added",
|
||||
|
||||
@@ -56,6 +56,9 @@ const ExportRepository: FC<Props> = ({ repository }) => {
|
||||
<>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("export.subtitle")} />
|
||||
<Notification type="inherit">
|
||||
{t("export.notification")}
|
||||
</Notification>
|
||||
<>
|
||||
<Checkbox
|
||||
checked={fullExport || compressed}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
StateMenuContextProvider,
|
||||
SubNavigation,
|
||||
Tooltip,
|
||||
urls,
|
||||
urls
|
||||
} from "@scm-manager/ui-components";
|
||||
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
@@ -75,7 +75,7 @@ type Props = RouteComponentProps &
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => void;
|
||||
};
|
||||
|
||||
const ArchiveTag = styled.span`
|
||||
const RepositoryTag = styled.span`
|
||||
margin-left: 0.2rem;
|
||||
background-color: #9a9a9a;
|
||||
padding: 0.4rem;
|
||||
@@ -153,7 +153,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
const extensionProps = {
|
||||
repository,
|
||||
url,
|
||||
indexLinks,
|
||||
indexLinks
|
||||
};
|
||||
|
||||
const redirectUrlFactory = binder.getExtension("repository.redirect", this.props);
|
||||
@@ -164,16 +164,16 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
redirectedUrl = url + "/info";
|
||||
}
|
||||
|
||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => {
|
||||
const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => {
|
||||
const baseUrl = `${url}/code/sources`;
|
||||
const sourceLink = file.newPath && {
|
||||
url: `${baseUrl}/${changeset.id}/${file.newPath}/`,
|
||||
label: t("diff.jumpToSource"),
|
||||
label: t("diff.jumpToSource")
|
||||
};
|
||||
const targetLink = file.oldPath &&
|
||||
changeset._embedded?.parents?.length === 1 && {
|
||||
url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`,
|
||||
label: t("diff.jumpToTarget"),
|
||||
label: t("diff.jumpToTarget")
|
||||
};
|
||||
|
||||
const links = [];
|
||||
@@ -199,11 +199,22 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return links ? links.map(({ url, label }) => <JumpToFileButton tooltip={label} link={url} />) : null;
|
||||
};
|
||||
|
||||
const archivedFlag = repository.archived && (
|
||||
<Tooltip message={t("archive.tooltip")}>
|
||||
<ArchiveTag className="is-size-6">{t("repository.archived")}</ArchiveTag>
|
||||
</Tooltip>
|
||||
);
|
||||
const repositoryFlags = [];
|
||||
if (repository.archived) {
|
||||
repositoryFlags.push(
|
||||
<Tooltip message={t("archive.tooltip")}>
|
||||
<RepositoryTag className="is-size-6">{t("repository.archived")}</RepositoryTag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (repository.exporting) {
|
||||
repositoryFlags.push(
|
||||
<Tooltip message={t("exporting.tooltip")}>
|
||||
<RepositoryTag className="is-size-6">{t("repository.exporting")}</RepositoryTag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const titleComponent = (
|
||||
<>
|
||||
@@ -222,7 +233,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
afterTitle={
|
||||
<>
|
||||
<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />
|
||||
{archivedFlag}
|
||||
{repositoryFlags.map(flag => flag)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -360,7 +371,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
loading,
|
||||
error,
|
||||
repoLink,
|
||||
indexLinks,
|
||||
indexLinks
|
||||
};
|
||||
};
|
||||
|
||||
@@ -368,7 +379,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchRepoByName: (link: string, namespace: string, name: string) => {
|
||||
dispatch(fetchRepoByName(link, namespace, name));
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository
|
||||
@NotEmpty
|
||||
private String type;
|
||||
private boolean archived;
|
||||
private boolean exporting;
|
||||
|
||||
RepositoryDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
|
||||
@@ -161,12 +161,7 @@ public class RepositoryExportResource {
|
||||
@PathParam("name") String name
|
||||
) {
|
||||
Repository repository = getVerifiedRepository(namespace, name);
|
||||
StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os);
|
||||
|
||||
return Response
|
||||
.ok(output, "application/x-gzip")
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz"))
|
||||
.build();
|
||||
return exportFullRepository(repository);
|
||||
}
|
||||
|
||||
private Repository getVerifiedRepository(String namespace, String name) {
|
||||
@@ -186,6 +181,15 @@ public class RepositoryExportResource {
|
||||
return repository;
|
||||
}
|
||||
|
||||
private Response exportFullRepository(Repository repository) {
|
||||
StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os);
|
||||
|
||||
return Response
|
||||
.ok(output, "application/x-gzip")
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Response exportRepository(Repository repository, boolean compressed) {
|
||||
StreamingOutput output;
|
||||
String fileExtension;
|
||||
|
||||
@@ -27,9 +27,12 @@ package sonia.scm.api.v2.resources;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
@@ -70,6 +73,11 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
@Override
|
||||
public abstract RepositoryDto map(Repository modelObject);
|
||||
|
||||
@AfterMapping
|
||||
void setExporting(Repository repository, @MappingTarget RepositoryDto repositoryDto) {
|
||||
repositoryDto.setExporting(DefaultRepositoryExportingCheck.isRepositoryExporting(repository.getId()));
|
||||
}
|
||||
|
||||
@ObjectFactory
|
||||
RepositoryDto createDto(Repository repository) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(repository.getNamespace(), repository.getName()));
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -54,25 +55,36 @@ public class FullScmRepositoryExporter {
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final TarArchiveRepositoryStoreExporter storeExporter;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryExporter(EnvironmentInformationXmlGenerator environmentGenerator,
|
||||
RepositoryMetadataXmlGenerator metadataGenerator,
|
||||
RepositoryServiceFactory serviceFactory,
|
||||
TarArchiveRepositoryStoreExporter storeExporter, WorkdirProvider workdirProvider) {
|
||||
TarArchiveRepositoryStoreExporter storeExporter,
|
||||
WorkdirProvider workdirProvider,
|
||||
RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this.environmentGenerator = environmentGenerator;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.storeExporter = storeExporter;
|
||||
this.workdirProvider = workdirProvider;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
}
|
||||
|
||||
public void export(Repository repository, OutputStream outputStream) {
|
||||
repositoryExportingCheck.withExportingLock(repository, () -> {
|
||||
exportInLock(repository, outputStream);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void exportInLock(Repository repository, OutputStream outputStream) {
|
||||
try (
|
||||
RepositoryService service = serviceFactory.create(repository);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(outputStream);
|
||||
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos);
|
||||
TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos);
|
||||
TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)
|
||||
) {
|
||||
writeEnvironmentData(taos);
|
||||
writeMetadata(repository, taos);
|
||||
|
||||
@@ -36,8 +36,10 @@ import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.lifecycle.DefaultRestarter;
|
||||
import sonia.scm.lifecycle.Restarter;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.MetadataStore;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
@@ -100,6 +102,7 @@ public class BootstrapModule extends AbstractModule {
|
||||
|
||||
// bind core
|
||||
bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class);
|
||||
bind(RepositoryExportingCheck.class, DefaultRepositoryExportingCheck.class);
|
||||
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
|
||||
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
|
||||
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
|
||||
|
||||
@@ -39,7 +39,6 @@ import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.security.AuthorizationChangedEvent;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
@@ -79,7 +78,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
private static final String THREAD_NAME = "Hook-%s";
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(DefaultRepositoryManager.class);
|
||||
private final ScmConfiguration configuration;
|
||||
private final ExecutorService executorService;
|
||||
private final Map<String, RepositoryHandler> handlerMap;
|
||||
private final KeyGenerator keyGenerator;
|
||||
@@ -89,11 +87,9 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
private final ManagerDaoAdapter<Repository> managerDaoAdapter;
|
||||
|
||||
@Inject
|
||||
public DefaultRepositoryManager(ScmConfiguration configuration,
|
||||
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
public DefaultRepositoryManager(SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider) {
|
||||
this.configuration = configuration;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.namespaceStrategyProvider = namespaceStrategyProvider;
|
||||
|
||||
@@ -326,6 +326,10 @@
|
||||
"4hSNNTBiu1": {
|
||||
"displayName": "Falscher Repository Typ",
|
||||
"description": "Der gegebene Typ entspricht nicht dem Typen des Repositories."
|
||||
},
|
||||
"1mSNlpe1V1": {
|
||||
"displayName": "Repository wird exportiert",
|
||||
"description": "Das Repository wird momentan exportiert und darf nicht modifiziert werden."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -326,6 +326,10 @@
|
||||
"4hSNNTBiu1": {
|
||||
"displayName": "Wrong repository type",
|
||||
"description": "The given type does not match the type of the repository."
|
||||
},
|
||||
"1mSNlpe1V1": {
|
||||
"displayName": "Repository is being exported",
|
||||
"description": "The repository is being exported and therefore must not be modified."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -88,6 +88,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.stream.Stream.of;
|
||||
@@ -104,23 +105,24 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.RETURNS_SELF;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static org.mockito.MockitoAnnotations.openMocks;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
password = "secret",
|
||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||
)
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
private static final String REALM = "AdminRealm";
|
||||
@@ -160,6 +162,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
private Repository repositoryMarkedAsExported;
|
||||
|
||||
@InjectMocks
|
||||
private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper;
|
||||
@@ -168,7 +171,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
initMocks(this);
|
||||
openMocks(this);
|
||||
super.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||
super.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||
super.manager = repositoryManager;
|
||||
@@ -316,7 +319,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NO_CONTENT, response.getStatus());
|
||||
verify(repositoryManager).modify(anyObject());
|
||||
verify(repositoryManager).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -336,7 +339,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
assertEquals(SC_CONFLICT, response.getStatus());
|
||||
assertThat(response.getContentAsString()).contains("space/repo");
|
||||
verify(repositoryManager, never()).modify(anyObject());
|
||||
verify(repositoryManager, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -355,7 +358,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_BAD_REQUEST, response.getStatus());
|
||||
verify(repositoryManager, never()).modify(anyObject());
|
||||
verify(repositoryManager, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -368,7 +371,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NO_CONTENT, response.getStatus());
|
||||
verify(repositoryManager).delete(anyObject());
|
||||
verify(repositoryManager).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -826,7 +829,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
/**
|
||||
* This method is a slightly adapted copy of Lin Zaho's gist at https://gist.github.com/lin-zhao/9985191
|
||||
*/
|
||||
private MockHttpRequest multipartRequest(MockHttpRequest request, Map<String, InputStream> files, RepositoryDto repository) throws IOException {
|
||||
private void multipartRequest(MockHttpRequest request, Map<String, InputStream> files, RepositoryDto repository) throws IOException {
|
||||
String boundary = UUID.randomUUID().toString();
|
||||
request.contentType("multipart/form-data; boundary=" + boundary);
|
||||
|
||||
@@ -864,6 +867,5 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
formWriter.flush();
|
||||
}
|
||||
request.setInputStream(new ByteArrayInputStream(buffer.toByteArray()));
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.BundleCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
@@ -47,6 +48,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -70,6 +72,8 @@ class FullScmRepositoryExporterTest {
|
||||
private TarArchiveRepositoryStoreExporter storeExporter;
|
||||
@Mock
|
||||
private WorkdirProvider workdirProvider;
|
||||
@Mock
|
||||
private RepositoryExportingCheck repositoryExportingCheck;
|
||||
|
||||
@InjectMocks
|
||||
private FullScmRepositoryExporter exporter;
|
||||
@@ -81,6 +85,7 @@ class FullScmRepositoryExporterTest {
|
||||
when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService);
|
||||
when(environmentGenerator.generate()).thenReturn(new byte[0]);
|
||||
when(metadataGenerator.generate(REPOSITORY)).thenReturn(new byte[0]);
|
||||
when(repositoryExportingCheck.withExportingLock(any(), any())).thenAnswer(invocation -> invocation.getArgument(1, Supplier.class).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,6 +101,7 @@ class FullScmRepositoryExporterTest {
|
||||
verify(environmentGenerator, times(1)).generate();
|
||||
verify(metadataGenerator, times(1)).generate(REPOSITORY);
|
||||
verify(bundleCommandBuilder, times(1)).bundle(any(OutputStream.class));
|
||||
verify(repositoryExportingCheck).withExportingLock(eq(REPOSITORY), any());
|
||||
workDirsCreated.forEach(wd -> assertThat(wd).doesNotExist());
|
||||
}
|
||||
|
||||
|
||||
@@ -108,8 +108,7 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
|
||||
NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class);
|
||||
repositoryManager = new DefaultRepositoryManager(
|
||||
configuration,
|
||||
contextProvider,
|
||||
contextProvider,
|
||||
keyGenerator,
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
|
||||
@@ -65,7 +65,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasProperty;
|
||||
@@ -109,7 +108,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
|
||||
private RepositoryDAO repositoryDAO;
|
||||
|
||||
{
|
||||
static {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@@ -121,8 +120,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
|
||||
private NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class);
|
||||
|
||||
private ScmConfiguration configuration;
|
||||
|
||||
private String mockedNamespace = "default_namespace";
|
||||
|
||||
@Before
|
||||
@@ -552,11 +549,9 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
handlerSet.add(createRepositoryHandler("hg", "Mercurial"));
|
||||
handlerSet.add(createRepositoryHandler("svn", "SVN"));
|
||||
|
||||
this.configuration = new ScmConfiguration();
|
||||
|
||||
when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace);
|
||||
|
||||
return new DefaultRepositoryManager(configuration, contextProvider,
|
||||
return new DefaultRepositoryManager(contextProvider,
|
||||
keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStore;
|
||||
import sonia.scm.store.InMemoryConfigurationEntryStoreFactory;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user