diff --git a/CHANGELOG.md b/CHANGELOG.md index b3871922f1..4bb817d66e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Detect renamed files in git and hg diffs ([#1157](https://github.com/scm-manager/scm-manager/pull/1157)) - ClassLoader and Adapter parameters to typed store apis ([#1111](https://github.com/scm-manager/scm-manager/pull/1111)) - Native packaging for Debian, Red Hat, Windows, Unix, Docker and Kubernetes ([#1165](https://github.com/scm-manager/scm-manager/pull/1165)) +- Cache for working directories ([#1166](https://github.com/scm-manager/scm-manager/pull/1166)) ### Fixed - Correctly resolve Links in markdown files ([#1152](https://github.com/scm-manager/scm-manager/pull/1152)) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 57f70dcdc3..1035ccf009 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import com.google.common.base.Preconditions; @@ -34,7 +34,7 @@ import sonia.scm.repository.Person; import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.util.AuthorUtil; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.util.IOUtil; import java.io.File; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index a1978df90d..43f443b4bb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import org.slf4j.Logger; @@ -33,7 +33,7 @@ import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.WorkdirProvider; import java.io.Closeable; import java.io.IOException; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 15bc338ff0..60b11162d1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -53,7 +53,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.security.ScmSecurityException; import java.util.Set; diff --git a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java deleted file mode 100644 index 5aed4fad77..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Repository; - -import java.io.File; -import java.io.IOException; - -public abstract class SimpleWorkdirFactory implements WorkdirFactory { - - private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class); - - private final WorkdirProvider workdirProvider; - - public SimpleWorkdirFactory(WorkdirProvider workdirProvider) { - this.workdirProvider = workdirProvider; - } - - @Override - public WorkingCopy createWorkingCopy(C context, String initialBranch) { - try { - File directory = workdirProvider.createNewWorkdir(); - ParentAndClone parentAndClone = cloneRepository(context, directory, initialBranch); - return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::closeWorkdir, this::closeCentral, directory); - } catch (IOException e) { - throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e); - } - } - - protected abstract Repository getScmRepository(C context); - - @SuppressWarnings("squid:S00112") - // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeCentral - protected abstract void closeRepository(R repository) throws Exception; - @SuppressWarnings("squid:S00112") - // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeWorkdir - protected abstract void closeWorkdirInternal(W workdir) throws Exception; - - protected abstract ParentAndClone cloneRepository(C context, File target, String initialBranch) throws IOException; - - private void closeCentral(R repository) { - try { - closeRepository(repository); - } catch (Exception e) { - logger.warn("could not close temporary repository clone", e); - } - } - - private void closeWorkdir(W repository) { - try { - closeWorkdirInternal(repository); - } catch (Exception e) { - logger.warn("could not close temporary repository clone", e); - } - } - - protected static class ParentAndClone { - private final R parent; - private final W clone; - - public ParentAndClone(R parent, W clone) { - this.parent = parent; - this.clone = clone; - } - - public R getParent() { - return parent; - } - - public W getClone() { - return clone; - } - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/work/NoneCachingWorkingCopyPool.java b/scm-core/src/main/java/sonia/scm/repository/work/NoneCachingWorkingCopyPool.java new file mode 100644 index 0000000000..c7bea0fd80 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/work/NoneCachingWorkingCopyPool.java @@ -0,0 +1,61 @@ +/* + * 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.work; + +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.File; + +/** + * This is the default implementation for the {@link WorkingCopyPool}. For each requested {@link WorkingCopy} with + * {@link #getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext)}, a new directory is requested from the + * {@link WorkdirProvider}. This directory is deleted immediately when the context is closed with + * {@link #contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext, File)}. + */ +public class NoneCachingWorkingCopyPool implements WorkingCopyPool { + + private final WorkdirProvider workdirProvider; + + @Inject + public NoneCachingWorkingCopyPool(WorkdirProvider workdirProvider) { + this.workdirProvider = workdirProvider; + } + + @Override + public WorkingCopy getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext context) { + return context.initialize(workdirProvider.createNewWorkdir()); + } + + @Override + public void contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext, File workdir) { + IOUtil.deleteSilently(workdir); + } + + @Override + public void shutdown() { + // no caches, nothing to clean up :-) + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPool.java b/scm-core/src/main/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPool.java new file mode 100644 index 0000000000..1cb38a72f0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPool.java @@ -0,0 +1,125 @@ +/* + * 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.work; + +import com.google.common.base.Stopwatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class is a simple implementation of the {@link WorkingCopyPool} to demonstrate, + * how caching can work in the simplest way. For the first time a {@link WorkingCopy} is + * requested for a repository with {@link #getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext)}, + * this implementation fetches a new directory from the {@link WorkdirProvider}. + * On {@link #contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext, File)}, + * the directory is not deleted, but put into a map with the repository id as key. + * When a working copy is requested with {@link #getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext)} + * for a repository with such an existing directory, it is taken from the map, reclaimed and + * returned as {@link WorkingCopy}. + * If for one repository a working copy is requested, while another is in use already, + * a second directory is requested from the {@link WorkdirProvider} for the second one. + * If a context is closed with {@link #contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext, File)} + * and there already is a directory stored in the map for the repository, + * the directory from the closed context simply is deleted. + *
+ * In general, this implementation should speed up things a bit, but one has to take into + * account, that there is no monitoring of diskspace. So you have to make sure, that + * there is enough space for a clone of each repository in the working dir. + *
+ * Possible enhancements: + *
    + *
  • Monitoring of times
  • + *
  • Allow multiple cached directories for busy repositories (possibly taking initial branches into account)
  • + *
  • Measure allocated disk space and set a limit
  • + *
  • Remove directories not used for a longer time
  • + *
  • Wait for a cached directory on parallel requests
  • + *
+ */ +public class SimpleCachingWorkingCopyPool implements WorkingCopyPool { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleCachingWorkingCopyPool.class); + + private final Map workdirs = new ConcurrentHashMap<>(); + + private final WorkdirProvider workdirProvider; + + @Inject + public SimpleCachingWorkingCopyPool(WorkdirProvider workdirProvider) { + this.workdirProvider = workdirProvider; + } + + @Override + public WorkingCopy getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext) { + String id = workingCopyContext.getScmRepository().getId(); + File existingWorkdir = workdirs.remove(id); + if (existingWorkdir != null) { + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + WorkingCopy reclaimed = workingCopyContext.reclaim(existingWorkdir); + LOG.debug("reclaimed workdir for {} in path {} in {}", workingCopyContext.getScmRepository().getNamespaceAndName(), existingWorkdir, stopwatch.stop()); + return reclaimed; + } catch (SimpleWorkingCopyFactory.ReclaimFailedException e) { + LOG.debug("failed to reclaim workdir for {} in path {} in {}", workingCopyContext.getScmRepository().getNamespaceAndName(), existingWorkdir, stopwatch.stop(), e); + deleteWorkdir(existingWorkdir); + } + } + return createNewWorkingCopy(workingCopyContext); + } + + private WorkingCopy createNewWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext) { + Stopwatch stopwatch = Stopwatch.createStarted(); + File newWorkdir = workdirProvider.createNewWorkdir(); + WorkingCopy parentAndClone = workingCopyContext.initialize(newWorkdir); + LOG.debug("initialized new workdir for {} in path {} in {}", workingCopyContext.getScmRepository().getNamespaceAndName(), newWorkdir, stopwatch.stop()); + return parentAndClone; + } + + @Override + public void contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext, File workdir) { + String id = workingCopyContext.getScmRepository().getId(); + File putResult = workdirs.putIfAbsent(id, workdir); + if (putResult != null && putResult != workdir) { + deleteWorkdir(workdir); + } + } + + @Override + public void shutdown() { + workdirs.values().parallelStream().forEach(this::deleteWorkdir); + workdirs.clear(); + } + + private void deleteWorkdir(File workdir) { + if (workdir.exists()) { + IOUtil.deleteSilently(workdir); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/work/SimpleWorkingCopyFactory.java b/scm-core/src/main/java/sonia/scm/repository/work/SimpleWorkingCopyFactory.java new file mode 100644 index 0000000000..0905ce1081 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/work/SimpleWorkingCopyFactory.java @@ -0,0 +1,224 @@ +/* + * 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.work; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryProvider; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.File; + +/** + * This class is responsible to govern the creation, the reuse and the destruction + * of working copies. For every repository type there has to be an implementation + * of this class to provide the repository specific logic to create, initialize, + * reclaim and clean up working copies. To do this, the following methods have to be + * implemented: + * + *
+ *
{@link #initialize(C, File, String)}
+ *
Creates a new clone of the repository for the given context in the given + * directory with the given branch checked out (if branches are supported).
+ *
{@link #reclaim(C, File, String)}
+ *
Reclaim the working directory with a already checked out clone of the + * repository given in the context, so that the directory is not modified in + * respect to the repository and the given branch is checked out (if branches + * are supported).
+ *
{@link #closeWorkingCopy(W)}
+ *
Closes resources allocated for the working copy, so that the directory can + * be put to the cache. Will be called at the end of the operation.
+ *
{@link #closeRepository(R)}
+ *
Closes resources allocated for the central repository.
+ *
+ *
+ * The general process looks like this: + *
+ * + * @param Type of central repository location + * @param Type of working copy for repository + * @param Type of repository context + */ +/* +http://www.plantuml.com/plantuml/uml/jLF1JiCm3BtdAtAkr7r0aQf9XN42JGE9SvHumyA9goHbAr-FnZgKDbCPGXnZl_ViFDlB49MFdINnm0QtVSFMAcVA-WbjIt2FyONz6xfTmss_KZgoxsKbjGSL8Kc96NnPooJOi8jmY4LHdKJccKbipKpL3bAOs7dkMldiUnbPUj2aq8e9fwppwjKPgoYUUJ9qMaC8suv4pXYf5CL5H2sdxQQz0WNuhhLLIE5cy54ws5yKF6I2cnD_fP30t1qqj17PNVwoGR_s_8u6_E3r8-o7X9W0odfgzLKseiE8Yl03_iSoP_8svbQpabVlP3rQ-35niLXCxo59LuQFhvzGcZYCR9azgW4-WxY2diJ_gBI1bWCUtx-xJtqQR7FKo6UNmvL-XLlqy2Kdbk1CP-aJ +@startuml +ModifyCommand->SimpleGitWorkingCopyFactory : createWorkingCopy +SimpleGitWorkingCopyFactory-> WorkingCopyContext**:create +SimpleGitWorkingCopyFactory->WorkingCopyPool:getWorkingCopy +group Try to reclaim +WorkingCopyPool->WorkingCopyContext:reclaim +alt reclaim successful +WorkingCopyContext->WorkingCopy** +WorkingCopyContext->> WorkingCopyPool:WorkingCopy +else reclaim fails; create new +WorkingCopyContext->x WorkingCopyPool:ReclaimFailedException +WorkingCopyPool->WorkdirProvider:createNewWorkdir +WorkdirProvider->>WorkingCopyPool +WorkingCopyPool->WorkingCopyContext:initialize +WorkingCopyContext->WorkingCopy** +WorkingCopyContext->> WorkingCopyPool:WorkingCopy +end +WorkingCopyPool->>SimpleGitWorkingCopyFactory: WorkingCopy +SimpleGitWorkingCopyFactory->>ModifyCommand: WorkingCopy +... +ModifyCommand->WorkingCopy:doWork +... +ModifyCommand->WorkingCopy:close +WorkingCopy->SimpleGitWorkingCopyFactory:close +SimpleGitWorkingCopyFactory->SimpleGitWorkingCopyFactory:closeWorkingCopy +SimpleGitWorkingCopyFactory->SimpleGitWorkingCopyFactory:closeRepository +SimpleGitWorkingCopyFactory->WorkingCopyPool:contextClosed +WorkingCopyPool->WorkingCopyPool:cacheDirectory +@enduml +*/ +public abstract class SimpleWorkingCopyFactory implements WorkingCopyFactory, ServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleWorkingCopyFactory.class); + + private final WorkingCopyPool workingCopyPool; + + public SimpleWorkingCopyFactory(WorkingCopyPool workingCopyPool) { + this.workingCopyPool = workingCopyPool; + } + + @Override + public WorkingCopy createWorkingCopy(C repositoryContext, String initialBranch) { + WorkingCopyContext workingCopyContext = createWorkingCopyContext(repositoryContext, initialBranch); + return workingCopyPool.getWorkingCopy(workingCopyContext); + } + + private WorkingCopyContext createWorkingCopyContext(C repositoryContext, String initialBranch) { + return new WorkingCopyContext( + initialBranch, + repositoryContext + ); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + workingCopyPool.shutdown(); + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + // nothing to do + } + + protected abstract ParentAndClone initialize(C context, File target, String initialBranch); + + protected abstract ParentAndClone reclaim(C context, File target, String initialBranch) throws ReclaimFailedException; + + @SuppressWarnings("squid:S00112") + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeCentral + protected abstract void closeRepository(R repository) throws Exception; + + @SuppressWarnings("squid:S00112") + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeWorkingCopy + protected abstract void closeWorkingCopy(W workingCopy) throws Exception; + + public static class ReclaimFailedException extends Exception { + public ReclaimFailedException(String message) { + super(message); + } + + public ReclaimFailedException(Throwable cause) { + super(cause); + } + + public ReclaimFailedException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ParentAndClone { + private final R parent; + private final W clone; + private final File directory; + + public ParentAndClone(R parent, W clone, File directory) { + this.parent = parent; + this.clone = clone; + this.directory = directory; + } + + R getParent() { + return parent; + } + + W getClone() { + return clone; + } + + File getDirectory() { + return directory; + } + } + + public class WorkingCopyContext { + private final String requestedBranch; + private final C repositoryContext; + + public WorkingCopyContext(String requestedBranch, C repositoryContext) { + this.requestedBranch = requestedBranch; + this.repositoryContext = repositoryContext; + } + + public Repository getScmRepository() { + return repositoryContext.get(); + } + + public WorkingCopy reclaim(File workdir) throws SimpleWorkingCopyFactory.ReclaimFailedException { + return createWorkingCopyFromParentAndClone(SimpleWorkingCopyFactory.this.reclaim(repositoryContext, workdir, requestedBranch)); + } + + public WorkingCopy initialize(File workdir) { + return createWorkingCopyFromParentAndClone(SimpleWorkingCopyFactory.this.initialize(repositoryContext, workdir, requestedBranch)); + } + + public WorkingCopy createWorkingCopyFromParentAndClone(ParentAndClone parentAndClone) { + return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), () -> close(parentAndClone), parentAndClone.getDirectory()); + } + + private void close(ParentAndClone parentAndClone) { + try { + closeWorkingCopy(parentAndClone.getClone()); + } catch (Exception e) { + LOG.warn("could not close clone for {} in directory {}", getScmRepository(), parentAndClone.getDirectory(), e); + } + try { + closeRepository(parentAndClone.getParent()); + } catch (Exception e) { + LOG.warn("could not close central repository for {}", getScmRepository(), e); + } + try { + workingCopyPool.contextClosed(this, parentAndClone.getDirectory()); + } catch (Exception e) { + LOG.warn("could not close context for {} with directory {}", getScmRepository(), parentAndClone.getDirectory(), e); + } + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java b/scm-core/src/main/java/sonia/scm/repository/work/WorkdirCreationException.java similarity index 72% rename from scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java rename to scm-core/src/main/java/sonia/scm/repository/work/WorkdirCreationException.java index 3015387c6a..df3bb93ff1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java +++ b/scm-core/src/main/java/sonia/scm/repository/work/WorkdirCreationException.java @@ -21,25 +21,22 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.util; -import java.util.function.Consumer; +package sonia.scm.repository.work; -public class CloseableWrapper implements AutoCloseable { +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; - private final T wrapped; - private final Consumer cleanup; +public class WorkdirCreationException extends ExceptionWithContext { - public CloseableWrapper(T wrapped, Consumer cleanup) { - this.wrapped = wrapped; - this.cleanup = cleanup; + public static final String CODE = "3tS0mjSoo1"; + + public WorkdirCreationException(String path, Exception cause) { + super(ContextEntry.ContextBuilder.entity("Path", path).build(), "Could not create directory " + path, cause); } - public T get() { return wrapped; } - @Override - public void close() { - cleanup.accept(wrapped); + public String getCode() { + return CODE; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java b/scm-core/src/main/java/sonia/scm/repository/work/WorkdirProvider.java similarity index 80% rename from scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java rename to scm-core/src/main/java/sonia/scm/repository/work/WorkdirProvider.java index ea5b10ee0c..ae6a5a6d5e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/work/WorkdirProvider.java @@ -21,8 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.util; + +package sonia.scm.repository.work; import java.io.File; import java.io.IOException; @@ -30,24 +30,24 @@ import java.nio.file.Files; public class WorkdirProvider { - private final File poolDirectory; + private final File rootDirectory; public WorkdirProvider() { this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work")); } - public WorkdirProvider(File poolDirectory) { - this.poolDirectory = poolDirectory; - if (!poolDirectory.exists() && !poolDirectory.mkdirs()) { - throw new IllegalStateException("could not create pool directory " + poolDirectory); + public WorkdirProvider(File rootDirectory) { + this.rootDirectory = rootDirectory; + if (!rootDirectory.exists() && !rootDirectory.mkdirs()) { + throw new IllegalStateException("could not create pool directory " + rootDirectory); } } public File createNewWorkdir() { try { - return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile(); + return Files.createTempDirectory(rootDirectory.toPath(),"workdir").toFile(); } catch (IOException e) { - throw new RuntimeException("could not create temporary workdir", e); + throw new WorkdirCreationException(rootDirectory.toString(), e); } } } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopy.java similarity index 67% rename from scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java rename to scm-core/src/main/java/sonia/scm/repository/work/WorkingCopy.java index bc1465ca46..009fc4f109 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java +++ b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopy.java @@ -21,33 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.util; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package sonia.scm.repository.work; + import sonia.scm.util.IOUtil; import java.io.File; import java.io.IOException; -import java.util.function.Consumer; -public class WorkingCopy implements AutoCloseable { - - private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class); +public final class WorkingCopy implements AutoCloseable { private final File directory; private final W workingRepository; private final R centralRepository; - private final Consumer cleanupWorkdir; - private final Consumer cleanupCentral; + private final Runnable close; - public WorkingCopy(W workingRepository, R centralRepository, Consumer cleanupWorkdir, Consumer cleanupCentral, File directory) { + public WorkingCopy(W workingRepository, R centralRepository, Runnable close, File directory) { this.directory = directory; this.workingRepository = workingRepository; this.centralRepository = centralRepository; - this.cleanupCentral = cleanupCentral; - this.cleanupWorkdir = cleanupWorkdir; + this.close = close; } public W getWorkingRepository() { @@ -64,12 +57,10 @@ public class WorkingCopy implements AutoCloseable { @Override public void close() { - try { - cleanupWorkdir.accept(workingRepository); - cleanupCentral.accept(centralRepository); - IOUtil.delete(directory); - } catch (IOException e) { - LOG.warn("could not delete temporary workdir '{}'", directory, e); - } + close.run(); + } + + void delete() throws IOException { + IOUtil.delete(directory); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyFactory.java similarity index 87% rename from scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java rename to scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyFactory.java index e3c72bc3ae..37ea57a11a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyFactory.java @@ -21,9 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.util; -public interface WorkdirFactory { - WorkingCopy createWorkingCopy(C context, String initialBranch); +package sonia.scm.repository.work; + +public interface WorkingCopyFactory { + WorkingCopy createWorkingCopy(C repositoryContext, String initialBranch); } diff --git a/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyPool.java b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyPool.java new file mode 100644 index 0000000000..472363ceb7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/work/WorkingCopyPool.java @@ -0,0 +1,35 @@ +/* + * 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.work; + +import java.io.File; + +public interface WorkingCopyPool { + WorkingCopy getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext context); + + void contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext, File workdir); + + void shutdown(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/work/doc-files/SimpleWorkingCopyFactory_Sequence.png b/scm-core/src/main/java/sonia/scm/repository/work/doc-files/SimpleWorkingCopyFactory_Sequence.png new file mode 100644 index 0000000000..6e9306bddd Binary files /dev/null and b/scm-core/src/main/java/sonia/scm/repository/work/doc-files/SimpleWorkingCopyFactory_Sequence.png differ diff --git a/scm-core/src/main/java/sonia/scm/util/IOUtil.java b/scm-core/src/main/java/sonia/scm/util/IOUtil.java index f1e63dd018..037f8e07ff 100644 --- a/scm-core/src/main/java/sonia/scm/util/IOUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/IOUtil.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- @@ -358,6 +358,15 @@ public final class IOUtil delete(file, false); } + public static void deleteSilently(File file) + { + try { + delete(file, true); + } catch (IOException e) { + // silent delete throws no exception + } + } + /** * Method description * diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index a374a2272c..5583261e52 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -37,7 +37,7 @@ import org.mockito.stubbing.Answer; import sonia.scm.repository.Person; import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.spi.ModifyCommandRequest; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.WorkdirProvider; import java.io.ByteArrayInputStream; import java.io.File; diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java index d908321bb6..492d23dec8 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java @@ -45,11 +45,13 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.WorkdirProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class RepositoryServiceFactoryTest { diff --git a/scm-core/src/test/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPoolTest.java b/scm-core/src/test/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPoolTest.java new file mode 100644 index 0000000000..cc45936bb7 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/work/SimpleCachingWorkingCopyPoolTest.java @@ -0,0 +1,115 @@ +/* + * 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.work; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.Repository; + +import java.io.File; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class}) +class SimpleCachingWorkingCopyPoolTest { + + private static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); + + @Mock + WorkdirProvider workdirProvider; + @InjectMocks + SimpleCachingWorkingCopyPool simpleCachingWorkingCopyPool; + + @Mock + SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext; + + @BeforeEach + void initContext() throws SimpleWorkingCopyFactory.ReclaimFailedException { + + lenient().when(workingCopyContext.initialize(any())) + .thenAnswer(invocationOnMock -> new WorkingCopy<>(null, null, () -> {}, invocationOnMock.getArgument(0, File.class))); + lenient().when(workingCopyContext.reclaim(any())) + .thenAnswer(invocationOnMock -> new WorkingCopy<>(null, null, () -> {}, invocationOnMock.getArgument(0, File.class))); + } + + @Test + void shouldCreateNewWorkdirForTheFirstRequest(@TempDir Path temp) { + when(workingCopyContext.getScmRepository()).thenReturn(REPOSITORY); + when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); + + WorkingCopy workdir = simpleCachingWorkingCopyPool.getWorkingCopy(workingCopyContext); + + verify(workingCopyContext).initialize(temp.toFile()); + } + + @Test + void shouldCreateWorkdirOnlyOnceForTheSameRepository(@TempDir Path temp) throws SimpleWorkingCopyFactory.ReclaimFailedException { + when(workingCopyContext.getScmRepository()).thenReturn(REPOSITORY); + when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); + + WorkingCopy firstWorkdir = simpleCachingWorkingCopyPool.getWorkingCopy(workingCopyContext); + simpleCachingWorkingCopyPool.contextClosed(workingCopyContext, firstWorkdir.getDirectory()); + WorkingCopy secondWorkdir = simpleCachingWorkingCopyPool.getWorkingCopy(workingCopyContext); + + verify(workingCopyContext).initialize(temp.toFile()); + verify(workingCopyContext).reclaim(temp.toFile()); + assertThat(secondWorkdir.getDirectory()).isEqualTo(temp.toFile()); + } + + @Test + void shouldCacheOnlyOneWorkdirForRepository(@TempDir Path temp) throws SimpleWorkingCopyFactory.ReclaimFailedException { + when(workingCopyContext.getScmRepository()).thenReturn(REPOSITORY); + File firstDirectory = temp.resolve("first").toFile(); + firstDirectory.mkdirs(); + File secondDirectory = temp.resolve("second").toFile(); + secondDirectory.mkdirs(); + when(workdirProvider.createNewWorkdir()).thenReturn( + firstDirectory, + secondDirectory); + + WorkingCopy firstWorkdir = simpleCachingWorkingCopyPool.getWorkingCopy(workingCopyContext); + WorkingCopy secondWorkdir = simpleCachingWorkingCopyPool.getWorkingCopy(workingCopyContext); + simpleCachingWorkingCopyPool.contextClosed(workingCopyContext, firstWorkdir.getDirectory()); + simpleCachingWorkingCopyPool.contextClosed(workingCopyContext, secondWorkdir.getDirectory()); + + verify(workingCopyContext, never()).reclaim(any()); + verify(workingCopyContext).initialize(firstDirectory); + verify(workingCopyContext).initialize(secondDirectory); + assertThat(firstWorkdir.getDirectory()).isNotEqualTo(secondWorkdir.getDirectory()); + assertThat(firstWorkdir.getDirectory()).exists(); + assertThat(secondWorkdir.getDirectory()).doesNotExist(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/work/SimpleWorkingCopyFactoryTest.java similarity index 57% rename from scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java rename to scm-core/src/test/java/sonia/scm/repository/work/SimpleWorkingCopyFactoryTest.java index 1e36307288..1ab22972f9 100644 --- a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/work/SimpleWorkingCopyFactoryTest.java @@ -21,15 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.util; +package sonia.scm.repository.work; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryProvider; +import sonia.scm.util.IOUtil; import java.io.Closeable; import java.io.File; @@ -39,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -public class SimpleWorkdirFactoryTest { +public class SimpleWorkingCopyFactoryTest { private static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); @@ -48,33 +49,54 @@ public class SimpleWorkdirFactoryTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - private SimpleWorkdirFactory simpleWorkdirFactory; + private SimpleWorkingCopyFactory simpleWorkingCopyFactory; private String initialBranchForLastCloneCall; + private boolean workdirIsCached = false; + private File workdir; + @Before public void initFactory() throws IOException { WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); - simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) { + WorkingCopyPool configurableTestWorkingCopyPool = new WorkingCopyPool() { @Override - protected Repository getScmRepository(Context context) { - return REPOSITORY; + public WorkingCopy getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext context) { + workdir = workdirProvider.createNewWorkdir(); + return context.initialize(workdir); } + @Override + public void contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext createWorkdirContext, File workdir) { + if (!workdirIsCached) { + IOUtil.deleteSilently(workdir); + } + } + + @Override + public void shutdown() { + } + }; + simpleWorkingCopyFactory = new SimpleWorkingCopyFactory(configurableTestWorkingCopyPool) { @Override protected void closeRepository(Closeable repository) throws IOException { repository.close(); } @Override - protected void closeWorkdirInternal(Closeable workdir) throws Exception { - workdir.close(); + protected ParentAndClone initialize(Context context, File target, String initialBranch) { + initialBranchForLastCloneCall = initialBranch; + return new ParentAndClone<>(parent, clone, target); } @Override - protected ParentAndClone cloneRepository(Context context, File target, String initialBranch) { - initialBranchForLastCloneCall = initialBranch; - return new ParentAndClone<>(parent, clone); + protected ParentAndClone reclaim(Context context, File target, String initialBranch) throws ReclaimFailedException { + throw new UnsupportedOperationException(); + } + + @Override + protected void closeWorkingCopy(Closeable workingCopy) throws Exception { + workingCopy.close(); } }; } @@ -82,7 +104,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCreateParentAndClone() { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) { + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) { assertThat(workingCopy.getCentralRepository()).isSameAs(parent); assertThat(workingCopy.getWorkingRepository()).isSameAs(clone); } @@ -91,7 +113,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCloseParent() throws IOException { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {} verify(parent).close(); } @@ -99,18 +121,40 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCloseClone() throws IOException { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {} verify(clone).close(); } + @Test + public void shouldDeleteWorkdirIfNotCached() { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {} + + assertThat(workdir).doesNotExist(); + } + + @Test + public void shouldNotDeleteWorkdirIfCached() { + Context context = new Context(); + workdirIsCached = true; + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {} + + assertThat(workdir).exists(); + } + @Test public void shouldPropagateInitialBranch() { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) { + try (WorkingCopy workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, "some")) { assertThat(initialBranchForLastCloneCall).isEqualTo("some"); } } - private static class Context {} + private static class Context implements RepositoryProvider { + @Override + public Repository get() { + return REPOSITORY; + } + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index d977cb242f..2760f59794 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -83,7 +83,7 @@ public class GitRepositoryHandler private final Scheduler scheduler; - private final GitWorkdirFactory workdirFactory; + private final GitWorkingCopyFactory workingCopyFactory; private Task task; @@ -93,12 +93,12 @@ public class GitRepositoryHandler public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, - GitWorkdirFactory workdirFactory, + GitWorkingCopyFactory workingCopyFactory, PluginLoader pluginLoader) { super(storeFactory, repositoryLocationResolver, pluginLoader); this.scheduler = scheduler; - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } //~--- get methods ---------------------------------------------------------- @@ -169,8 +169,8 @@ public class GitRepositoryHandler return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION); } - public GitWorkdirFactory getWorkdirFactory() { - return workdirFactory; + public GitWorkingCopyFactory getWorkingCopyFactory() { + return workingCopyFactory; } public String getRepositoryId(StoredConfig gitConfig) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkingCopyFactory.java similarity index 89% rename from scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java rename to scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkingCopyFactory.java index bb9f40132d..8b127d8fde 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkingCopyFactory.java @@ -21,12 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import org.eclipse.jgit.lib.Repository; import sonia.scm.repository.spi.GitContext; -import sonia.scm.repository.util.WorkdirFactory; +import sonia.scm.repository.work.WorkingCopyFactory; -public interface GitWorkdirFactory extends WorkdirFactory { +public interface GitWorkingCopyFactory extends WorkingCopyFactory { } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 4f3e31f4d7..d52d0a1574 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -42,10 +42,10 @@ import org.eclipse.jgit.transport.RemoteRefUpdate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.WorkingCopy; import sonia.scm.user.User; import java.io.IOException; @@ -135,8 +135,8 @@ class AbstractGitCommand } } - > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) { + > R inClone(Function workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) { + try (WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, initialBranch)) { Repository repository = workingCopy.getWorkingRepository(); logger.debug("cloned repository to folder {}", repository.getWorkTree()); return workerSupplier.apply(new Git(repository)).run(); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java index c66f5cab55..c7f0327312 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java @@ -21,20 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.RepositoryProvider; import java.io.Closeable; import java.io.File; @@ -44,7 +40,7 @@ import java.io.IOException; * * @author Sebastian Sdorra */ -public class GitContext implements Closeable +public class GitContext implements Closeable, RepositoryProvider { /** @@ -108,6 +104,11 @@ public class GitContext implements Closeable return repository; } + @Override + public Repository get() { + return getRepository(); + } + File getDirectory() { return directory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index 5f915535b1..9f362edeb6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -36,7 +36,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.filter.PathFilter; -import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; @@ -53,7 +53,7 @@ import static sonia.scm.NotFoundException.notFound; public class GitMergeCommand extends AbstractGitCommand implements MergeCommand { - private final GitWorkdirFactory workdirFactory; + private final GitWorkingCopyFactory workingCopyFactory; private static final Set STRATEGIES = ImmutableSet.of( MergeStrategy.MERGE_COMMIT, @@ -61,9 +61,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand MergeStrategy.SQUASH ); - GitMergeCommand(GitContext context, GitWorkdirFactory workdirFactory) { + GitMergeCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory) { super(context); - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } @Override @@ -73,19 +73,19 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand @Override public MergeConflictResult computeConflicts(MergeCommandRequest request) { - return inClone(git -> new ConflictWorker(git, request), workdirFactory, request.getTargetBranch()); + return inClone(git -> new ConflictWorker(git, request), workingCopyFactory, request.getTargetBranch()); } private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) { switch(request.getMergeStrategy()) { case SQUASH: - return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); case FAST_FORWARD_IF_POSSIBLE: - return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); case MERGE_COMMIT: - return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); default: throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index d808434c1d..9e0f5449d3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -34,7 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ConcurrentModificationException; import sonia.scm.NoChangesMadeException; -import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.web.lfs.LfsBlobStoreFactory; @@ -50,18 +50,18 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class); private static final Striped REGISTER_LOCKS = Striped.lock(5); - private final GitWorkdirFactory workdirFactory; + private final GitWorkingCopyFactory workingCopyFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory; - GitModifyCommand(GitContext context, GitWorkdirFactory workdirFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { + GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context); - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override public String execute(ModifyCommandRequest request) { - return inClone(clone -> new ModifyWorker(clone, request), workdirFactory, request.getBranch()); + return inClone(clone -> new ModifyWorker(clone, request), workingCopyFactory, request.getBranch()); } private class ModifyWorker extends GitCloneWorker implements ModifyWorkerHelper { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 4aa6ab3a08..862631c32c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -264,12 +264,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public MergeCommand getMergeCommand() { - return new GitMergeCommand(context, handler.getWorkdirFactory()); + return new GitMergeCommand(context, handler.getWorkingCopyFactory()); } @Override public ModifyCommand getModifyCommand() { - return new GitModifyCommand(context, handler.getWorkdirFactory(), lfsBlobStoreFactory); + return new GitModifyCommand(context, handler.getWorkingCopyFactory(), lfsBlobStoreFactory); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java similarity index 58% rename from scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java rename to scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java index ff0101f297..11019b288c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java @@ -24,37 +24,41 @@ package sonia.scm.repository.spi; +import com.google.common.base.Stopwatch; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ScmTransportProtocol; -import sonia.scm.repository.GitWorkdirFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.util.SimpleWorkdirFactory; -import sonia.scm.repository.util.WorkdirProvider; -import sonia.scm.util.SystemUtil; +import sonia.scm.repository.work.SimpleWorkingCopyFactory.ParentAndClone; -import javax.inject.Inject; import java.io.File; import java.io.IOException; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; -public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { +class GitWorkingCopyInitializer { - @Inject - public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) { - super(workdirProvider); + private static final Logger LOG = LoggerFactory.getLogger(GitWorkingCopyInitializer.class); + + private final SimpleGitWorkingCopyFactory simpleGitWorkingCopyFactory; + private final GitContext context; + + public GitWorkingCopyInitializer(SimpleGitWorkingCopyFactory simpleGitWorkingCopyFactory, GitContext context) { + this.simpleGitWorkingCopyFactory = simpleGitWorkingCopyFactory; + this.context = context; } - @Override - public ParentAndClone cloneRepository(GitContext context, File target, String initialBranch) { + public ParentAndClone initialize(File target, String initialBranch) { + LOG.trace("clone repository {}", context.getRepository().getId()); + Stopwatch stopwatch = Stopwatch.createStarted(); try { Repository clone = Git.cloneRepository() - .setURI(createScmTransportProtocolUri(context.getDirectory())) + .setURI(simpleGitWorkingCopyFactory.createScmTransportProtocolUri(context.getDirectory())) .setDirectory(target) .setBranch(initialBranch) .call() @@ -66,38 +70,11 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory(null, clone); + return new ParentAndClone<>(null, clone, target); } catch (GitAPIException | IOException e) { throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e); + } finally { + LOG.trace("took {} to clone repository {}", stopwatch.stop(), context.getRepository().getId()); } } - - private String createScmTransportProtocolUri(File bareRepository) { - if (SystemUtil.isWindows()) { - return ScmTransportProtocol.NAME + ":///" + bareRepository.getAbsolutePath().replaceAll("\\\\", "/"); - } else { - return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath(); - } - } - - @Override - protected void closeRepository(Repository repository) { - // we have to check for null here, because we do not create a repository for - // the parent in cloneRepository - if (repository != null) { - repository.close(); - } - } - - @Override - protected void closeWorkdirInternal(Repository workdir) throws Exception { - if (workdir != null) { - workdir.close(); - } - } - - @Override - protected sonia.scm.repository.Repository getScmRepository(GitContext context) { - return context.getRepository(); - } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyReclaimer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyReclaimer.java new file mode 100644 index 0000000000..3b605b531e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyReclaimer.java @@ -0,0 +1,77 @@ +/* + * 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.spi; + +import com.google.common.base.Stopwatch; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.SimpleWorkingCopyFactory.ParentAndClone; + +import java.io.File; +import java.io.IOException; + +class GitWorkingCopyReclaimer { + + private static final Logger LOG = LoggerFactory.getLogger(GitWorkingCopyReclaimer.class); + + private final GitContext context; + + public GitWorkingCopyReclaimer(GitContext context) { + this.context = context; + } + + public ParentAndClone reclaim(File target, String initialBranch) throws SimpleWorkingCopyFactory.ReclaimFailedException { + LOG.trace("reclaim repository {}", context.getRepository().getId()); + Stopwatch stopwatch = Stopwatch.createStarted(); + Repository repo = openTarget(target); + try (Git git = Git.open(target)) { + git.reset().setMode(ResetCommand.ResetType.HARD).call(); + git.clean().setForce(true).setCleanDirectories(true).call(); + git.fetch().call(); + git.checkout().setForced(true).setName("origin/" + initialBranch).call(); + git.branchDelete().setBranchNames(initialBranch).setForce(true).call(); + git.checkout().setName(initialBranch).setCreateBranch(true).call(); + return new ParentAndClone<>(null, repo, target); + } catch (GitAPIException | IOException e) { + throw new SimpleWorkingCopyFactory.ReclaimFailedException(e); + } finally { + LOG.trace("took {} to reclaim repository {}", stopwatch.stop(), context.getRepository().getId()); + } + } + + private Repository openTarget(File target) throws SimpleWorkingCopyFactory.ReclaimFailedException { + try { + return GitUtil.open(target); + } catch (IOException e) { + throw new SimpleWorkingCopyFactory.ReclaimFailedException(e); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactory.java new file mode 100644 index 0000000000..a4fa38111e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactory.java @@ -0,0 +1,78 @@ +/* + * 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.spi; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ScmTransportProtocol; +import sonia.scm.repository.GitWorkingCopyFactory; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.WorkingCopyPool; +import sonia.scm.util.SystemUtil; + +import javax.inject.Inject; +import java.io.File; + +public class SimpleGitWorkingCopyFactory extends SimpleWorkingCopyFactory implements GitWorkingCopyFactory { + + @Inject + public SimpleGitWorkingCopyFactory(WorkingCopyPool workdirProvider) { + super(workdirProvider); + } + + @Override + public ParentAndClone initialize(GitContext context, File target, String initialBranch) { + return new GitWorkingCopyInitializer(this, context).initialize(target, initialBranch); + } + + @Override + public ParentAndClone reclaim(GitContext context, File target, String initialBranch) throws SimpleWorkingCopyFactory.ReclaimFailedException { + return new GitWorkingCopyReclaimer(context).reclaim(target, initialBranch); + } + + String createScmTransportProtocolUri(File bareRepository) { + if (SystemUtil.isWindows()) { + return ScmTransportProtocol.NAME + ":///" + bareRepository.getAbsolutePath().replaceAll("\\\\", "/"); + } else { + return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath(); + } + } + + @Override + protected void closeRepository(Repository repository) { + // we have to check for null here, because we do not create a repository for + // the parent in cloneRepository + if (repository != null) { + repository.close(); + } + } + + @Override + protected void closeWorkingCopy(Repository workingCopy) throws Exception { + if (workingCopy != null) { + workingCopy.close(); + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index aeac72dd06..7cd1042115 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- @@ -33,8 +33,8 @@ import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper; import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper; import sonia.scm.api.v2.resources.GitRepositoryConfigMapper; import sonia.scm.plugin.Extension; -import sonia.scm.repository.GitWorkdirFactory; -import sonia.scm.repository.spi.SimpleGitWorkdirFactory; +import sonia.scm.repository.GitWorkingCopyFactory; +import sonia.scm.repository.spi.SimpleGitWorkingCopyFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; /** @@ -52,13 +52,13 @@ public class GitServletModule extends ServletModule bind(GitRepositoryResolver.class); bind(GitReceivePackFactory.class); bind(ScmTransportProtocol.class); - + bind(LfsBlobStoreFactory.class); bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass()); bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass()); bind(GitRepositoryConfigMapper.class).to(Mappers.getMapper(GitRepositoryConfigMapper.class).getClass()); - bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class); + bind(GitWorkingCopyFactory.class).to(SimpleGitWorkingCopyFactory.class); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index 0fe594f9fb..53b13ccacb 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -56,7 +56,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { private ConfigurationStoreFactory factory; @Mock - private GitWorkdirFactory gitWorkdirFactory; + private GitWorkingCopyFactory gitWorkingCopyFactory; @Override @@ -87,7 +87,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { RepositoryLocationResolver locationResolver, File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory, null); + scheduler, locationResolver, gitWorkingCopyFactory, null); repositoryHandler.init(contextProvider); GitConfig config = new GitConfig(); @@ -101,7 +101,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - scheduler, locationResolver, gitWorkdirFactory, null); + scheduler, locationResolver, gitWorkingCopyFactory, null); GitConfig config = new GitConfig(); config.setDisabled(false); config.setGcExpression("gc exp"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index ed21f683fb..0bf61a739b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -40,11 +40,12 @@ import org.junit.jupiter.api.Assertions; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Added; -import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeStrategy; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.user.User; import java.io.BufferedWriter; @@ -424,14 +425,14 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { } private GitMergeCommand createCommand(Consumer interceptor) { - return new GitMergeCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider())) { + return new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider()))) { @Override - > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) { + > R inClone(Function workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) { Function interceptedWorkerSupplier = git -> { interceptor.accept(git); return workerSupplier.apply(git); }; - return super.inClone(interceptedWorkerSupplier, workdirFactory, initialBranch); + return super.inClone(interceptedWorkerSupplier, workingCopyFactory, initialBranch); } }; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java index 391ac2d762..4c1763966e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java @@ -27,7 +27,8 @@ package sonia.scm.repository.spi; import org.junit.Rule; import org.junit.Test; import sonia.scm.repository.spi.MergeConflictResult.SingleMergeConflict; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import java.io.IOException; @@ -91,7 +92,7 @@ public class GitMergeCommand_Conflict_Test extends AbstractGitCommandTestBase { } private MergeConflictResult computeMergeConflictResult(String branchToMerge, String targetBranch) { - GitMergeCommand gitMergeCommand = new GitMergeCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider())); + GitMergeCommand gitMergeCommand = new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider()))); MergeCommandRequest mergeCommandRequest = new MergeCommandRequest(); mergeCommandRequest.setBranchToMerge(branchToMerge); mergeCommandRequest.setTargetBranch(targetBranch); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 746f05f61c..e72ad1af7e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -42,7 +42,8 @@ import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; @@ -323,7 +324,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); } @FunctionalInterface diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java index bdad922b01..6709505c90 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java @@ -35,7 +35,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.store.Blob; import sonia.scm.store.BlobStore; import sonia.scm.web.lfs.LfsBlobStoreFactory; @@ -130,7 +131,7 @@ public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase { } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); } @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java index ae1c2fd854..5898845a74 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java @@ -38,7 +38,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; @@ -101,7 +102,7 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); + return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); } @FunctionalInterface diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java similarity index 50% rename from scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java rename to scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java index 4d76a4952d..9a32b5d01c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java @@ -21,12 +21,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.URIish; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -35,17 +38,20 @@ import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.HookContextFactory; -import sonia.scm.repository.util.WorkdirProvider; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.repository.work.WorkingCopy; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import static com.google.inject.util.Providers.of; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { +public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -66,7 +72,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void emptyPoolShouldCreateNewWorkdir() { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); File masterRepo = createRepositoryDirectory(); try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { @@ -84,7 +90,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void shouldCheckoutInitialBranch() { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) { assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) @@ -96,7 +102,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void shouldCheckoutDefaultBranch() { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) @@ -108,7 +114,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void cloneFromPoolShouldNotBeReused() { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); File firstDirectory; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { @@ -122,7 +128,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void cloneFromPoolShouldBeDeletedOnClose() { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); File directory; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { @@ -130,4 +136,85 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { } assertThat(directory).doesNotExist(); } + + @Test + public void shouldReclaimCleanDirectoryWithSameBranch() throws Exception { + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); + File workdir = createExistingClone(factory); + + factory.reclaim(createContext(), workdir, "master"); + + assertBranchCheckedOutAndClean(workdir, "master"); + } + + @Test + public void shouldReclaimCleanDirectoryWithOtherBranch() throws Exception { + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); + File workdir = createExistingClone(factory); + + factory.reclaim(createContext(), workdir, "test-branch"); + + assertBranchCheckedOutAndClean(workdir, "test-branch"); + } + + @Test + public void shouldReclaimDirectoryWithDeletedFileInIndex() throws Exception { + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); + File workdir = createExistingClone(factory); + Git.open(workdir).rm().addFilepattern("a.txt").call(); + + factory.reclaim(createContext(), workdir, "master"); + + assertBranchCheckedOutAndClean(workdir, "master"); + } + + @Test + public void shouldReclaimDirectoryWithDeletedFileInDirectory() throws Exception { + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); + File workdir = createExistingClone(factory); + Files.delete(workdir.toPath().resolve("a.txt")); + + factory.reclaim(createContext(), workdir, "master"); + + assertBranchCheckedOutAndClean(workdir, "master"); + } + + @Test + public void shouldReclaimDirectoryWithAdditionalFileInDirectory() throws Exception { + SimpleGitWorkingCopyFactory factory = new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); + File workdir = createExistingClone(factory); + Path newDirectory = workdir.toPath().resolve("new"); + Files.createDirectories(newDirectory); + Files.createFile(newDirectory.resolve("newFile")); + + factory.reclaim(createContext(), workdir, "master"); + + assertBranchCheckedOutAndClean(workdir, "master"); + } + + public File createExistingClone(SimpleGitWorkingCopyFactory factory) throws Exception { + File workdir = temporaryFolder.newFolder(); + extract(workdir, "sonia/scm/repository/spi/scm-git-spi-test-workdir.zip"); + Git.open(workdir).remoteSetUrl().setRemoteUri(new URIish(factory.createScmTransportProtocolUri(repositoryDirectory))).setRemoteName("origin").call(); + return workdir; + } + + private void assertBranchCheckedOutAndClean(File workdir, String expectedBranch) throws Exception { + Git git = Git.open(workdir); + assertThat(git.getRepository().getBranch()).isEqualTo(expectedBranch); + Status workdirStatus = git.status().call(); + assertStatusClean(workdirStatus); + } + + private void assertStatusClean(Status workdirStatus) { + assertThat(workdirStatus.getAdded()).isEmpty(); + assertThat(workdirStatus.getChanged()).isEmpty(); + assertThat(workdirStatus.getConflicting()).isEmpty(); + assertThat(workdirStatus.getIgnoredNotInIndex()).isEmpty(); + assertThat(workdirStatus.getMissing()).isEmpty(); + assertThat(workdirStatus.getModified()).isEmpty(); + assertThat(workdirStatus.getRemoved()).isEmpty(); + assertThat(workdirStatus.getUntracked()).isEmpty(); + assertThat(workdirStatus.getUncommittedChanges()).isEmpty(); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-workdir.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-workdir.zip new file mode 100644 index 0000000000..34fc6c7794 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-workdir.zip differ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 386483a43a..6c33607750 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -42,7 +42,7 @@ import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.spi.HgRepositoryServiceProvider; -import sonia.scm.repository.spi.HgWorkdirFactory; +import sonia.scm.repository.spi.HgWorkingCopyFactory; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; import sonia.scm.util.SystemUtil; @@ -78,7 +78,7 @@ public class HgRepositoryHandler private final Provider hgContextProvider; - private final HgWorkdirFactory workdirFactory; + private final HgWorkingCopyFactory workingCopyFactory; private final JAXBContext jaxbContext; @@ -86,10 +86,10 @@ public class HgRepositoryHandler public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, Provider hgContextProvider, RepositoryLocationResolver repositoryLocationResolver, - PluginLoader pluginLoader, HgWorkdirFactory workdirFactory) { + PluginLoader pluginLoader, HgWorkingCopyFactory workingCopyFactory) { super(storeFactory, repositoryLocationResolver, pluginLoader); this.hgContextProvider = hgContextProvider; - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; try { @@ -259,8 +259,8 @@ public class HgRepositoryHandler } } - public HgWorkdirFactory getWorkdirFactory() { - return workdirFactory; + public HgWorkingCopyFactory getWorkingCopyFactory() { + return workingCopyFactory; } public JAXBContext getJaxbContext() { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java index 408daedd81..3a291b4501 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -34,7 +34,7 @@ import sonia.scm.ContextEntry; import sonia.scm.repository.Branch; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.BranchRequest; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.WorkingCopy; import sonia.scm.user.User; /** @@ -45,16 +45,16 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class); - private final HgWorkdirFactory workdirFactory; + private final HgWorkingCopyFactory workingCopyFactory; - HgBranchCommand(HgCommandContext context, HgWorkdirFactory workdirFactory) { + HgBranchCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { super(context); - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } @Override public Branch branch(BranchRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) { + try (WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(getContext(), request.getParentBranch())) { com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository(); Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository); @@ -70,7 +70,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { @Override public void deleteOrClose(String branchName) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(getContext(), branchName)) { + try (WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(getContext(), branchName)) { User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); LOG.debug("Closing branch '{}' in repository {}", branchName, getRepository().getNamespaceAndName()); @@ -104,7 +104,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { private void pullChangesIntoCentralRepository(WorkingCopy workingCopy, String branch) { try { PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); - workdirFactory.configure(pullCommand); + workingCopyFactory.configure(pullCommand); pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); } catch (Exception e) { // TODO handle failed update diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java index e588a67182..39181d3d67 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java @@ -21,33 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.Repository; - import com.google.common.base.Strings; - import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.RepositoryProvider; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.File; import java.io.IOException; import java.util.Map; import java.util.function.BiConsumer; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra */ -public class HgCommandContext implements Closeable +public class HgCommandContext implements Closeable, RepositoryProvider { /** Field description */ @@ -155,6 +154,11 @@ public class HgCommandContext implements Closeable return scmRepository; } + @Override + public sonia.scm.repository.Repository get() { + return getScmRepository(); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java index 8e63c5b3c8..012592c7a4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; @@ -33,7 +33,7 @@ import com.aragost.javahg.commands.RemoveCommand; import com.aragost.javahg.commands.StatusCommand; import sonia.scm.NoChangesMadeException; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.WorkingCopy; import java.io.File; import java.io.IOException; @@ -43,17 +43,17 @@ import java.util.List; public class HgModifyCommand implements ModifyCommand { private HgCommandContext context; - private final HgWorkdirFactory workdirFactory; + private final HgWorkingCopyFactory workingCopyFactory; - public HgModifyCommand(HgCommandContext context, HgWorkdirFactory workdirFactory) { + public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { this.context = context; - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } @Override public String execute(ModifyCommandRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) { + try (WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, request.getBranch())) { Repository workingRepository = workingCopy.getWorkingRepository(); request.getRequests().forEach( partialRequest -> { @@ -112,7 +112,7 @@ public class HgModifyCommand implements ModifyCommand { private List pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy workingCopy) { try { com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); - workdirFactory.configure(pullCommand); + workingCopyFactory.configure(pullCommand); return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); } catch (Exception e) { throw new IntegrateChangesFromWorkdirException(context.getScmRepository(), diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index 3995146e42..ce64b06982 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -120,7 +120,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider @Override public BranchCommand getBranchCommand() { - return new HgBranchCommand(context, handler.getWorkdirFactory()); + return new HgBranchCommand(context, handler.getWorkingCopyFactory()); } /** @@ -232,7 +232,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider @Override public ModifyCommand getModifyCommand() { - return new HgModifyCommand(context, handler.getWorkdirFactory()); + return new HgModifyCommand(context, handler.getWorkingCopyFactory()); } /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkingCopyFactory.java similarity index 89% rename from scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java rename to scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkingCopyFactory.java index e4c816cb0c..15c39aa77e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkingCopyFactory.java @@ -21,13 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import com.aragost.javahg.Repository; import com.aragost.javahg.commands.PullCommand; -import sonia.scm.repository.util.WorkdirFactory; +import sonia.scm.repository.work.WorkingCopyFactory; -public interface HgWorkdirFactory extends WorkdirFactory { +public interface HgWorkingCopyFactory extends WorkingCopyFactory { void configure(PullCommand pullCommand); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java similarity index 54% rename from scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java rename to scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java index 5a25aa247d..4e4c4cf80c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java @@ -21,16 +21,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import com.aragost.javahg.BaseRepository; import com.aragost.javahg.Repository; import com.aragost.javahg.commands.CloneCommand; +import com.aragost.javahg.commands.ExecutionException; import com.aragost.javahg.commands.PullCommand; +import com.aragost.javahg.commands.StatusCommand; +import com.aragost.javahg.commands.UpdateCommand; import com.aragost.javahg.commands.flags.CloneCommandFlags; -import sonia.scm.repository.util.SimpleWorkdirFactory; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.WorkingCopyPool; +import sonia.scm.util.IOUtil; import sonia.scm.web.HgRepositoryEnvironmentBuilder; import javax.inject.Inject; @@ -40,29 +45,58 @@ import java.io.IOException; import java.util.Map; import java.util.function.BiConsumer; -public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory implements HgWorkdirFactory { +public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory implements HgWorkingCopyFactory { private final Provider hgRepositoryEnvironmentBuilder; @Inject - public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) { + public SimpleHgWorkingCopyFactory(Provider hgRepositoryEnvironmentBuilder, WorkingCopyPool workdirProvider) { super(workdirProvider); this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; } @Override - public ParentAndClone cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException { - BiConsumer> repositoryMapBiConsumer = - (repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment); - Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer); + public ParentAndClone initialize(HgCommandContext context, File target, String initialBranch) { + Repository centralRepository = openCentral(context); CloneCommand cloneCommand = CloneCommandFlags.on(centralRepository); if (initialBranch != null) { cloneCommand.updaterev(initialBranch); } - cloneCommand.execute(target.getAbsolutePath()); + try { + cloneCommand.execute(target.getAbsolutePath()); + } catch (IOException e) { + throw new InternalRepositoryException(context.getScmRepository(), "could not clone repository", e); + } BaseRepository clone = Repository.open(target); - return new ParentAndClone<>(centralRepository, clone); + return new ParentAndClone<>(centralRepository, clone, target); + } + + @Override + // The hg api to create a command is meant to be used from the command classes, not from their "flags" base classes. + @SuppressWarnings("java:S3252") + protected ParentAndClone reclaim(HgCommandContext context, File target, String initialBranch) throws ReclaimFailedException { + Repository centralRepository = openCentral(context); + try { + BaseRepository clone = Repository.open(target); + for (String unknown : StatusCommand.on(clone).execute().getUnknown()) { + delete(clone.getDirectory(), unknown); + } + UpdateCommand.on(clone).rev(initialBranch).clean().execute(); + return new ParentAndClone<>(centralRepository, clone, target); + } catch (ExecutionException | IOException e) { + throw new ReclaimFailedException(e); + } + } + + public Repository openCentral(HgCommandContext context) { + BiConsumer> repositoryMapBiConsumer = + (repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment); + return context.openWithSpecialEnvironment(repositoryMapBiConsumer); + } + + private void delete(File directory, String unknownFile) throws IOException { + IOUtil.delete(new File(directory, unknownFile)); } @Override @@ -71,13 +105,8 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory b.getName().equals("new_branch")).isNotEmpty(); assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("default"); @@ -74,7 +75,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase { branchRequest.setParentBranch("test-branch"); branchRequest.setNewBranch("new_branch"); - Branch newBranch = new HgBranchCommand(cmdContext, workdirFactory).branch(branchRequest); + Branch newBranch = new HgBranchCommand(cmdContext, workingCopyFactory).branch(branchRequest); assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("test-branch"); @@ -84,7 +85,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase { public void shouldCloseBranch() { String branchToBeClosed = "test-branch"; - new HgBranchCommand(cmdContext, workdirFactory).deleteOrClose(branchToBeClosed); + new HgBranchCommand(cmdContext, workingCopyFactory).deleteOrClose(branchToBeClosed); assertThat(readBranches()).filteredOn(b -> b.getName().equals(branchToBeClosed)).isEmpty(); } @@ -92,8 +93,9 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase { public void shouldThrowInternalRepositoryException() { String branchToBeClosed = "default"; - new HgBranchCommand(cmdContext, workdirFactory).deleteOrClose(branchToBeClosed); - assertThrows(InternalRepositoryException.class, () -> new HgBranchCommand(cmdContext, workdirFactory).deleteOrClose(branchToBeClosed)); + HgBranchCommand hgBranchCommand = new HgBranchCommand(cmdContext, workingCopyFactory); + hgBranchCommand.deleteOrClose(branchToBeClosed); + assertThrows(InternalRepositoryException.class, () -> hgBranchCommand.deleteOrClose(branchToBeClosed)); } private List readBranches() { diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java index 1a944f0681..d55005f675 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import com.google.inject.util.Providers; @@ -35,7 +35,8 @@ import sonia.scm.NotFoundException; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.web.HgRepositoryEnvironmentBuilder; import java.io.File; @@ -55,13 +56,13 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase { public void initHgModifyCommand() { HgHookManager hookManager = HgTestUtil.createHookManager(); HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); - SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider()) { + SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) { @Override public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { // we do not want to configure http hooks in this unit test } }; - hgModifyCommand = new HgModifyCommand(cmdContext, workdirFactory + hgModifyCommand = new HgModifyCommand(cmdContext, workingCopyFactory ); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java new file mode 100644 index 0000000000..8a8164e072 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java @@ -0,0 +1,144 @@ +/* + * 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.spi; + +import com.aragost.javahg.Repository; +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.work.SimpleCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.repository.work.WorkingCopy; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleHgWorkingCopyFactoryTest extends AbstractHgCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private WorkdirProvider workdirProvider; + + private SimpleHgWorkingCopyFactory workingCopyFactory; + + @Before + public void bindScmProtocol() throws IOException { + workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + HgHookManager hookManager = HgTestUtil.createHookManager(); + HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); + workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new SimpleCachingWorkingCopyPool(workdirProvider)) { + @Override + public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { + // we do not want to configure http hooks in this unit test + } + }; + } + + @Test + public void shouldSwitchBranch() { + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default"); + + File initialDirectory = workingCopy.getDirectory(); + workingCopy.close(); + + assertThat(initialDirectory).exists(); + assertThat(initialDirectory.toPath().resolve("f.txt")).exists(); + + WorkingCopy cachedWorkingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "test-branch"); + assertThat(cachedWorkingCopy.getDirectory()).isEqualTo(initialDirectory); + assertThat(cachedWorkingCopy.getDirectory().toPath().resolve("f.txt")).doesNotExist(); + } + + @Test + public void shouldReplaceFileWithContentFromNewBranch() throws IOException { + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "test-branch"); + + File initialDirectory = workingCopy.getDirectory(); + Path fileToBeReplaced = initialDirectory.toPath().resolve("f.txt"); + Files.createFile(fileToBeReplaced); + Files.write(fileToBeReplaced, Collections.singleton("some content")); + + workingCopy.close(); + + assertThat(initialDirectory).exists(); + assertThat(fileToBeReplaced).hasContent("some content"); + + WorkingCopy cachedWorkingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default"); + assertThat(cachedWorkingCopy.getDirectory()).isEqualTo(initialDirectory); + assertThat(cachedWorkingCopy.getDirectory().toPath().resolve("f.txt")).exists(); + assertThat(fileToBeReplaced).hasContent("f"); + } + + @Test + public void shouldDeleteUntrackedFile() throws IOException { + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "test-branch"); + + File initialDirectory = workingCopy.getDirectory(); + Path fileToBeDeleted = initialDirectory.toPath().resolve("x.txt"); + Files.createFile(fileToBeDeleted); + Files.write(fileToBeDeleted, Collections.singleton("some content")); + + workingCopy.close(); + + assertThat(initialDirectory).exists(); + assertThat(fileToBeDeleted).hasContent("some content"); + + WorkingCopy cachedWorkingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default"); + assertThat(cachedWorkingCopy.getDirectory()).isEqualTo(initialDirectory); + assertThat(cachedWorkingCopy.getDirectory().toPath().resolve("x.txt")).doesNotExist(); + } + + @Test + public void shouldDeleteUntrackedDirectory() throws IOException { + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "test-branch"); + + File initialDirectory = workingCopy.getDirectory(); + Path directoryToBeDeleted = initialDirectory.toPath().resolve("newDir"); + Files.createDirectories(directoryToBeDeleted); + Path fileToBeDeleted = directoryToBeDeleted.resolve("y.txt"); + Files.createFile(fileToBeDeleted); + Files.write(fileToBeDeleted, Collections.singleton("some content")); + + workingCopy.close(); + + assertThat(initialDirectory).exists(); + assertThat(fileToBeDeleted).hasContent("some content"); + + WorkingCopy cachedWorkingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default"); + assertThat(cachedWorkingCopy.getDirectory()).isEqualTo(initialDirectory); + assertThat(cachedWorkingCopy.getDirectory().toPath().resolve("newDir")).isEmptyDirectory(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkingCopyFactory.java similarity index 89% rename from scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java rename to scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkingCopyFactory.java index c7969ed5a9..67793d1e90 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkingCopyFactory.java @@ -21,13 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; import sonia.scm.repository.spi.SvnContext; -import sonia.scm.repository.util.WorkdirFactory; +import sonia.scm.repository.work.WorkingCopyFactory; import java.io.File; -public interface SvnWorkDirFactory extends WorkdirFactory { +public interface SvnWorkingCopyFactory extends WorkingCopyFactory { } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactory.java similarity index 50% rename from scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java rename to scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactory.java index f1069afede..a2996d4148 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactory.java @@ -21,35 +21,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository; -import org.junit.Test; -import sonia.scm.repository.util.CloseableWrapper; +package sonia.scm.repository.spi; -import java.util.function.Consumer; +import sonia.scm.repository.SvnWorkingCopyFactory; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.WorkingCopyPool; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; +import javax.inject.Inject; +import java.io.File; -public class CloseableWrapperTest { +public class SimpleSvnWorkingCopyFactory extends SimpleWorkingCopyFactory implements SvnWorkingCopyFactory { - @Test - public void shouldExecuteGivenMethodAtClose() { - Consumer wrapped = new Consumer() { - // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy - @Override - public void accept(AutoCloseable s) { - } - }; + @Inject + public SimpleSvnWorkingCopyFactory(WorkingCopyPool workingCopyPool) { + super(workingCopyPool); + } - Consumer closer = spy(wrapped); + @Override + protected ParentAndClone initialize(SvnContext context, File workingCopy, String initialBranch) { + return new SvnWorkingCopyInitializer(context).initialize(workingCopy); + } - AutoCloseable autoCloseable = () -> {}; - try (CloseableWrapper wrapper = new CloseableWrapper<>(autoCloseable, closer)) { - // nothing to do here - } + @Override + protected ParentAndClone reclaim(SvnContext context, File target, String initialBranch) throws SimpleWorkingCopyFactory.ReclaimFailedException { + return new SvnWorkingCopyReclaimer(context).reclaim(target); + } - verify(closer).accept(autoCloseable); + @Override + protected void closeRepository(File workingCopy) { + // No resources to be closed for svn + } + + @Override + protected void closeWorkingCopy(File workingCopy) { + // No resources to be closed for svn } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java index 4a3ea032b3..639bf25e8a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java @@ -21,24 +21,19 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; - import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.SvnUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.File; @@ -46,7 +41,7 @@ import java.io.File; * * @author Sebastian Sdorra */ -public class SvnContext implements Closeable { +public class SvnContext implements Closeable, RepositoryProvider { private static final Logger LOG = LoggerFactory.getLogger(SvnContext.class); @@ -64,6 +59,11 @@ public class SvnContext implements Closeable { return repository; } + @Override + public Repository get() { + return getRepository(); + } + public File getDirectory() { return directory; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java index 1c7b8db5e7..57b7a3ad68 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java @@ -33,8 +33,8 @@ import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.SvnWorkDirFactory; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.SvnWorkingCopyFactory; +import sonia.scm.repository.work.WorkingCopy; import java.io.File; import java.io.IOException; @@ -43,19 +43,19 @@ import java.nio.file.Path; public class SvnModifyCommand implements ModifyCommand { private SvnContext context; - private SvnWorkDirFactory workDirFactory; + private SvnWorkingCopyFactory workingCopyFactory; private Repository repository; - SvnModifyCommand(SvnContext context, SvnWorkDirFactory workDirFactory) { + SvnModifyCommand(SvnContext context, SvnWorkingCopyFactory workingCopyFactory) { this.context = context; this.repository = context.getRepository(); - this.workDirFactory = workDirFactory; + this.workingCopyFactory = workingCopyFactory; } @Override public String execute(ModifyCommandRequest request) { SVNClientManager clientManager = SVNClientManager.newInstance(); - try (WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null)) { + try (WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, null)) { File workingDirectory = workingCopy.getDirectory(); modifyWorkingDirectory(request, clientManager, workingDirectory); return commitChanges(clientManager, workingDirectory, request.getCommitMessage()); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 65879834aa..1548fba869 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -28,7 +28,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; -import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.SvnWorkingCopyFactory; import sonia.scm.repository.api.Command; import javax.inject.Inject; @@ -54,10 +54,10 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider @Inject SvnRepositoryServiceProvider(SvnRepositoryHandler handler, - Repository repository, SvnWorkDirFactory workdirFactory) + Repository repository, SvnWorkingCopyFactory workingCopyFactory) { this.context = new SvnContext(repository, handler.getDirectory(repository.getId())); - this.workDirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } //~--- methods -------------------------------------------------------------- @@ -153,7 +153,7 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider } public ModifyCommand getModifyCommand() { - return new SvnModifyCommand(context, workDirFactory); + return new SvnModifyCommand(context, workingCopyFactory); } /** @@ -185,5 +185,5 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider /** Field description */ private final SvnContext context; - private final SvnWorkDirFactory workDirFactory; + private final SvnWorkingCopyFactory workingCopyFactory; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index c615902b3e..0a5bff15b7 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -21,25 +21,25 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import com.google.inject.Inject; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; -import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.SvnWorkingCopyFactory; @Extension public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { private SvnRepositoryHandler handler; - private SvnWorkDirFactory workdirFactory; + private SvnWorkingCopyFactory workingCopyFactory; @Inject - public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkDirFactory workdirFactory) { + public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkingCopyFactory workingCopyFactory) { this.handler = handler; - this.workdirFactory = workdirFactory; + this.workingCopyFactory = workingCopyFactory; } @Override @@ -47,7 +47,7 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { SvnRepositoryServiceProvider provider = null; if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new SvnRepositoryServiceProvider(handler, repository, workdirFactory); + provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory); } return provider; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyInitializer.java similarity index 66% rename from scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java rename to scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyInitializer.java index dcd06f9207..140c7e10f3 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyInitializer.java @@ -30,36 +30,25 @@ import org.tmatesoft.svn.core.wc2.SvnCheckout; import org.tmatesoft.svn.core.wc2.SvnOperationFactory; import org.tmatesoft.svn.core.wc2.SvnTarget; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Repository; -import sonia.scm.repository.SvnWorkDirFactory; -import sonia.scm.repository.util.SimpleWorkdirFactory; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.work.SimpleWorkingCopyFactory.ParentAndClone; -import javax.inject.Inject; import java.io.File; -public class SimpleSvnWorkDirFactory extends SimpleWorkdirFactory implements SvnWorkDirFactory { +class SvnWorkingCopyInitializer { + private final SvnContext context; - @Inject - public SimpleSvnWorkDirFactory(WorkdirProvider workdirProvider) { - super(workdirProvider); + public SvnWorkingCopyInitializer(SvnContext context) { + this.context = context; } - @Override - protected Repository getScmRepository(SvnContext context) { - return context.getRepository(); - } - - @Override - protected ParentAndClone cloneRepository(SvnContext context, File workingCopy, String initialBranch) { - + public ParentAndClone initialize(File workingCopy) { final SvnOperationFactory svnOperationFactory = new SvnOperationFactory(); SVNURL source; try { source = SVNURL.fromFile(context.getDirectory()); } catch (SVNException ex) { - throw new InternalRepositoryException(getScmRepository(context), "error creating svn url from central directory", ex); + throw new InternalRepositoryException(context.getRepository(), "error creating svn url from central directory", ex); } try { @@ -68,19 +57,11 @@ public class SimpleSvnWorkDirFactory extends SimpleWorkdirFactory(context.getDirectory(), workingCopy); - } - - @Override - protected void closeRepository(File workingCopy) { - } - - @Override - protected void closeWorkdirInternal(File workdir) { + return new ParentAndClone<>(context.getDirectory(), workingCopy, workingCopy); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyReclaimer.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyReclaimer.java new file mode 100644 index 0000000000..f72efa8e8d --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnWorkingCopyReclaimer.java @@ -0,0 +1,55 @@ +/* + * 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.spi; + +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNRevision; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.SimpleWorkingCopyFactory.ParentAndClone; + +import java.io.File; + +import static org.tmatesoft.svn.core.SVNDepth.INFINITY; + +class SvnWorkingCopyReclaimer { + private final SvnContext context; + + public SvnWorkingCopyReclaimer(SvnContext context) { + this.context = context; + } + + public ParentAndClone reclaim(File target) throws SimpleWorkingCopyFactory.ReclaimFailedException { + SVNClientManager clientManager = SVNClientManager.newInstance(); + try { + clientManager.getWCClient().doRevert(new File[] {target}, INFINITY, null); + clientManager.getWCClient().doCleanup(target, true, true, true, true, true, false); + clientManager.getUpdateClient().doUpdate(target, SVNRevision.HEAD, INFINITY, false, false); + } catch (SVNException e) { + throw new SimpleWorkingCopyFactory.ReclaimFailedException(e); + } + return new ParentAndClone<>(context.getDirectory(), target, target); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 52803adb61..078c613418 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; import com.google.inject.servlet.ServletModule; @@ -29,8 +29,8 @@ import org.mapstruct.factory.Mappers; import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; -import sonia.scm.repository.SvnWorkDirFactory; -import sonia.scm.repository.spi.SimpleSvnWorkDirFactory; +import sonia.scm.repository.SvnWorkingCopyFactory; +import sonia.scm.repository.spi.SimpleSvnWorkingCopyFactory; /** * @@ -43,6 +43,6 @@ public class SvnServletModule extends ServletModule { protected void configureServlets() { bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); - bind(SvnWorkDirFactory.class).to(SimpleSvnWorkDirFactory.class); + bind(SvnWorkingCopyFactory.class).to(SimpleSvnWorkingCopyFactory.class); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactoryTest.java similarity index 59% rename from scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java rename to scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactoryTest.java index a21a0319e6..1d7fcf2b54 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkingCopyFactoryTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import org.junit.Before; @@ -29,16 +29,17 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tmatesoft.svn.core.SVNException; -import sonia.scm.repository.Repository; -import sonia.scm.repository.util.WorkdirProvider; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.SimpleCachingWorkingCopyPool; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.repository.work.WorkingCopy; import java.io.File; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; -public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { +public class SimpleSvnWorkingCopyFactoryTest extends AbstractSvnCommandTestBase { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -53,7 +54,7 @@ public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { @Test public void shouldCheckoutLatestRevision() throws SVNException, IOException { - SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + SimpleSvnWorkingCopyFactory factory = new SimpleSvnWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")) @@ -64,8 +65,8 @@ public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { } @Test - public void cloneFromPoolshouldNotBeReused() { - SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + public void cloneFromPoolShouldNotBeReused() { + SimpleSvnWorkingCopyFactory factory = new SimpleSvnWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); File firstDirectory; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { @@ -79,23 +80,51 @@ public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { @Test public void shouldDeleteCloneOnClose() { - SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + SimpleSvnWorkingCopyFactory factory = new SimpleSvnWorkingCopyFactory(new NoneCachingWorkingCopyPool(workdirProvider)); File directory; File workingRepository; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { directory = workingCopy.getDirectory(); workingRepository = workingCopy.getWorkingRepository(); - } + assertThat(directory).doesNotExist(); assertThat(workingRepository).doesNotExist(); } @Test - public void shouldReturnRepository() { - SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); - Repository scmRepository = factory.getScmRepository(createContext()); - assertThat(scmRepository).isSameAs(repository); + public void shouldDeleteUntrackedFileOnReclaim() throws IOException { + SimpleSvnWorkingCopyFactory factory = new SimpleSvnWorkingCopyFactory(new SimpleCachingWorkingCopyPool(workdirProvider)); + + WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null); + File directory = workingCopy.getWorkingRepository(); + File untracked = new File(directory, "untracked"); + untracked.createNewFile(); + + workingCopy.close(); + assertThat(untracked).exists(); + + workingCopy = factory.createWorkingCopy(createContext(), null); + + assertThat(workingCopy.getWorkingRepository()).isEqualTo(directory); + assertThat(untracked).doesNotExist(); + } + + @Test + public void shouldRestoreDeletedFileOnReclaim() throws IOException { + SimpleSvnWorkingCopyFactory factory = new SimpleSvnWorkingCopyFactory(new SimpleCachingWorkingCopyPool(workdirProvider)); + + WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null); + File directory = workingCopy.getWorkingRepository(); + File a_txt = new File(directory, "a.txt"); + a_txt.delete(); + workingCopy.close(); + assertThat(a_txt).doesNotExist(); + + workingCopy = factory.createWorkingCopy(createContext(), null); + + assertThat(workingCopy.getWorkingRepository()).isEqualTo(directory); + assertThat(a_txt).exists(); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java index 533e012dc3..dbace87411 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java @@ -33,8 +33,9 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; import sonia.scm.repository.Person; -import sonia.scm.repository.util.WorkdirProvider; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.repository.work.WorkingCopy; import java.io.File; import java.io.IOException; @@ -48,7 +49,7 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { private SvnModifyCommand svnModifyCommand; private SvnContext context; - private SimpleSvnWorkDirFactory workDirFactory; + private SimpleSvnWorkingCopyFactory workingCopyFactory; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -56,8 +57,8 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { @Before public void initSvnModifyCommand() { context = createContext(); - workDirFactory = new SimpleSvnWorkDirFactory(new WorkdirProvider(context.getDirectory())); - svnModifyCommand = new SvnModifyCommand(context, workDirFactory); + workingCopyFactory = new SimpleSvnWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(context.getDirectory()))); + svnModifyCommand = new SvnModifyCommand(context, workingCopyFactory); } @Before @@ -80,7 +81,7 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); svnModifyCommand.execute(request); - WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, null); assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/a.txt")).doesNotExist(); assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/c")).exists(); } @@ -96,7 +97,7 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { svnModifyCommand.execute(request); - WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, null); assertThat(new File(workingCopy.getWorkingRepository(), "Test123")).exists(); } @@ -123,7 +124,7 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { svnModifyCommand.execute(request); - WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + WorkingCopy workingCopy = workingCopyFactory.createWorkingCopy(context, null); assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).exists(); assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).hasContent(""); } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java index 4b65f9094a..08781b0070 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.lifecycle.modules; import com.google.common.base.Throwables; @@ -77,6 +77,7 @@ public class ApplicationModuleProvider implements ModuleProvider { } moduleList.add(new MapperModule()); moduleList.add(new ExecutorModule()); + moduleList.add(new WorkingCopyPoolModule(pluginLoader)); return moduleList; } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModule.java new file mode 100644 index 0000000000..a6871b8b74 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModule.java @@ -0,0 +1,51 @@ +/* + * 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.lifecycle.modules; + +import com.google.inject.AbstractModule; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.WorkingCopyPool; + +public class WorkingCopyPoolModule extends AbstractModule { + public static final String DEFAULT_WORKING_COPY_POOL_STRATEGY = NoneCachingWorkingCopyPool.class.getName(); + public static final String WORKING_COPY_POOL_STRATEGY_PROPERTY = "scm.workingCopyPoolStrategy"; + private final ClassLoader classLoader; + + public WorkingCopyPoolModule(PluginLoader pluginLoader) { + this.classLoader = pluginLoader.getUberClassLoader(); + } + + @Override + protected void configure() { + String workingCopyPoolStrategy = System.getProperty(WORKING_COPY_POOL_STRATEGY_PROPERTY, DEFAULT_WORKING_COPY_POOL_STRATEGY); + try { + Class strategyClass = (Class) classLoader.loadClass(workingCopyPoolStrategy); + bind(WorkingCopyPool.class).to(strategyClass); + } catch (Exception e) { + throw new IllegalStateException("could not instantiate class for working copy pool: " + workingCopyPoolStrategy, e); + } + } +} diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 314dac3de4..77b6aadb8a 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -219,6 +219,10 @@ "2qRyyaVcJ1": { "displayName": "Ungültig formatiertes Element", "description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen." + }, + "3tS0mjSoo1": { + "displayName": "Fehler bei der Erstellung eines Arbeitsverzeichnisses", + "description": "Der Server konnte kein Arbeitsverzeichnis zur Abarbeitung der Anfrage erstellen. Bitte prüfen Sie die Server Logs für genauere Informationen." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index fba82da650..1cce4f266e 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -219,6 +219,10 @@ "2qRyyaVcJ1": { "displayName": "Invalid format in element", "description": "The input had some invalid formats. Please check the server log for further information." + }, + "3tS0mjSoo1": { + "displayName": "Error creating a new working directory", + "description": "The server could not create a new working directory to process the request. Please check the server log for further information." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModuleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModuleTest.java new file mode 100644 index 0000000000..22797fa0ed --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/modules/WorkingCopyPoolModuleTest.java @@ -0,0 +1,105 @@ +/* + * 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.lifecycle.modules; + +import com.google.inject.Binder; +import com.google.inject.binder.AnnotatedBindingBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.work.NoneCachingWorkingCopyPool; +import sonia.scm.repository.work.SimpleWorkingCopyFactory; +import sonia.scm.repository.work.WorkingCopy; +import sonia.scm.repository.work.WorkingCopyPool; + +import java.io.File; + +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sonia.scm.lifecycle.modules.WorkingCopyPoolModule.WORKING_COPY_POOL_STRATEGY_PROPERTY; + +@ExtendWith(MockitoExtension.class) +class WorkingCopyPoolModuleTest { + + @Mock + PluginLoader pluginLoader; + @Mock + Binder binder; + @Mock + AnnotatedBindingBuilder bindingBuilder; + + @BeforeEach + void initClassLoader() { + lenient().when(pluginLoader.getUberClassLoader()).thenReturn(getClass().getClassLoader()); + } + + @AfterEach + void cleanProperty() { + System.clearProperty(WORKING_COPY_POOL_STRATEGY_PROPERTY); + } + + @Test + void shouldBindToDefaultWithoutProperty() { + System.clearProperty(WORKING_COPY_POOL_STRATEGY_PROPERTY); + WorkingCopyPoolModule module = new WorkingCopyPoolModule(pluginLoader); + when(binder.bind(WorkingCopyPool.class)).thenReturn(bindingBuilder); + + module.configure(binder); + + verify(bindingBuilder).to(NoneCachingWorkingCopyPool.class); + } + + @Test + void shouldBindToCustomPoolFromProperty() { + System.setProperty(WORKING_COPY_POOL_STRATEGY_PROPERTY, TestPool.class.getName()); + WorkingCopyPoolModule module = new WorkingCopyPoolModule(pluginLoader); + when(binder.bind(WorkingCopyPool.class)).thenReturn(bindingBuilder); + + module.configure(binder); + + verify(bindingBuilder).to(TestPool.class); + } + + static class TestPool implements WorkingCopyPool { + @Override + public WorkingCopy getWorkingCopy(SimpleWorkingCopyFactory.WorkingCopyContext context) { + return null; + } + + @Override + public void contextClosed(SimpleWorkingCopyFactory.WorkingCopyContext workingCopyContext, File workdir) { + + } + + @Override + public void shutdown() { + + } + } +}