mirror of
				https://github.com/scm-manager/scm-manager.git
				synced 2025-10-31 18:46:07 +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 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 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); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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,11 +83,12 @@ 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 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 && ( | ||||
|     const repositoryFlags = []; | ||||
|     if (repository.archived) { | ||||
|       repositoryFlags.push( | ||||
|         <Tooltip message={t("archive.tooltip")}> | ||||
|         <ArchiveTag className="is-size-6">{t("repository.archived")}</ArchiveTag> | ||||
|           <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,7 +108,6 @@ public class DefaultRepositoryManagerPerfTest { | ||||
|     Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler); | ||||
|     NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); | ||||
|     repositoryManager = new DefaultRepositoryManager( | ||||
|       configuration,  | ||||
|       contextProvider, | ||||
|       keyGenerator,  | ||||
|       repositoryDAO, | ||||
|   | ||||
| @@ -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)); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,6 @@ 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