Merge pull request #1166 from scm-manager/feature/support_workdir_cache

Support workdir cache
This commit is contained in:
Sebastian Sdorra
2020-06-03 16:40:32 +02:00
committed by GitHub
65 changed files with 1598 additions and 453 deletions

View File

@@ -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))

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<R, W, C> implements WorkdirFactory<R, W, C> {
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
private final WorkdirProvider workdirProvider;
public SimpleWorkdirFactory(WorkdirProvider workdirProvider) {
this.workdirProvider = workdirProvider;
}
@Override
public WorkingCopy<R, W> createWorkingCopy(C context, String initialBranch) {
try {
File directory = workdirProvider.createNewWorkdir();
ParentAndClone<R, W> 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<R, W> 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<R, W> {
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;
}
}
}

View File

@@ -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 <R, W> WorkingCopy<R, W> getWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.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 :-)
}
}

View File

@@ -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.
* <br>
* 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.
* <br>
* Possible enhancements:
* <ul>
* <li>Monitoring of times</li>
* <li>Allow multiple cached directories for busy repositories (possibly taking initial branches into account)</li>
* <li>Measure allocated disk space and set a limit</li>
* <li>Remove directories not used for a longer time</li>
* <li>Wait for a cached directory on parallel requests</li>
* </ul>
*/
public class SimpleCachingWorkingCopyPool implements WorkingCopyPool {
private static final Logger LOG = LoggerFactory.getLogger(SimpleCachingWorkingCopyPool.class);
private final Map<String, File> workdirs = new ConcurrentHashMap<>();
private final WorkdirProvider workdirProvider;
@Inject
public SimpleCachingWorkingCopyPool(WorkdirProvider workdirProvider) {
this.workdirProvider = workdirProvider;
}
@Override
public <R, W> WorkingCopy<R, W> getWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.WorkingCopyContext workingCopyContext) {
String id = workingCopyContext.getScmRepository().getId();
File existingWorkdir = workdirs.remove(id);
if (existingWorkdir != null) {
Stopwatch stopwatch = Stopwatch.createStarted();
try {
WorkingCopy<R, W> 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 <R, W> WorkingCopy<R, W> createNewWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.WorkingCopyContext workingCopyContext) {
Stopwatch stopwatch = Stopwatch.createStarted();
File newWorkdir = workdirProvider.createNewWorkdir();
WorkingCopy<R, W> 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);
}
}
}

View File

@@ -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:
*
* <dl>
* <dt>{@link #initialize(C, File, String)}</dt>
* <dd>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).</dd>
* <dt>{@link #reclaim(C, File, String)}</dt>
* <dd>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).</dd>
* <dt>{@link #closeWorkingCopy(W)}</dt>
* <dd>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.</dd>
* <dt>{@link #closeRepository(R)}</dt>
* <dd>Closes resources allocated for the central repository.</dd>
* </dl>
* <br>
* The general process looks like this:
* <br>
* <img src="doc-files/SimpleWorkingCopyFactory_Sequence.png"/>
* @param <R> Type of central repository location
* @param <W> Type of working copy for repository
* @param <C> 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<R, W, C extends RepositoryProvider> implements WorkingCopyFactory<R, W, C>, ServletContextListener {
private static final Logger LOG = LoggerFactory.getLogger(SimpleWorkingCopyFactory.class);
private final WorkingCopyPool workingCopyPool;
public SimpleWorkingCopyFactory(WorkingCopyPool workingCopyPool) {
this.workingCopyPool = workingCopyPool;
}
@Override
public WorkingCopy<R, W> 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<R, W> initialize(C context, File target, String initialBranch);
protected abstract ParentAndClone<R, W> 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<R, W> {
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<R, W> reclaim(File workdir) throws SimpleWorkingCopyFactory.ReclaimFailedException {
return createWorkingCopyFromParentAndClone(SimpleWorkingCopyFactory.this.reclaim(repositoryContext, workdir, requestedBranch));
}
public WorkingCopy<R, W> initialize(File workdir) {
return createWorkingCopyFromParentAndClone(SimpleWorkingCopyFactory.this.initialize(repositoryContext, workdir, requestedBranch));
}
public WorkingCopy<R, W> createWorkingCopyFromParentAndClone(ParentAndClone<R, W> parentAndClone) {
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), () -> close(parentAndClone), parentAndClone.getDirectory());
}
private void close(ParentAndClone<R, W> 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);
}
}
}
}

View File

@@ -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<T extends AutoCloseable> implements AutoCloseable {
import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
private final T wrapped;
private final Consumer<T> cleanup;
public class WorkdirCreationException extends ExceptionWithContext {
public CloseableWrapper(T wrapped, Consumer<T> 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;
}
}

View File

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

View File

@@ -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<R, W> implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class);
public final class WorkingCopy<R, W> implements AutoCloseable {
private final File directory;
private final W workingRepository;
private final R centralRepository;
private final Consumer<W> cleanupWorkdir;
private final Consumer<R> cleanupCentral;
private final Runnable close;
public WorkingCopy(W workingRepository, R centralRepository, Consumer<W> cleanupWorkdir, Consumer<R> 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<R, W> 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);
}
}

View File

@@ -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<R, W, C> {
WorkingCopy<R, W> createWorkingCopy(C context, String initialBranch);
package sonia.scm.repository.work;
public interface WorkingCopyFactory<R, W, C> {
WorkingCopy<R, W> createWorkingCopy(C repositoryContext, String initialBranch);
}

View File

@@ -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 {
<R, W> WorkingCopy<R, W> getWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.WorkingCopyContext context);
void contextClosed(SimpleWorkingCopyFactory<?, ?, ?>.WorkingCopyContext workingCopyContext, File workdir);
void shutdown();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -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
*

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<Object, Path, ?>.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();
}
}

View File

@@ -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<Closeable, Closeable, Context> simpleWorkdirFactory;
private SimpleWorkingCopyFactory<Closeable, Closeable, Context> 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<Closeable, Closeable, Context>(workdirProvider) {
WorkingCopyPool configurableTestWorkingCopyPool = new WorkingCopyPool() {
@Override
protected Repository getScmRepository(Context context) {
return REPOSITORY;
public <R, W> WorkingCopy<R, W> getWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.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<Closeable, Closeable, Context>(configurableTestWorkingCopyPool) {
@Override
protected void closeRepository(Closeable repository) throws IOException {
repository.close();
}
@Override
protected void closeWorkdirInternal(Closeable workdir) throws Exception {
workdir.close();
protected ParentAndClone<Closeable, Closeable> initialize(Context context, File target, String initialBranch) {
initialBranchForLastCloneCall = initialBranch;
return new ParentAndClone<>(parent, clone, target);
}
@Override
protected ParentAndClone<Closeable, Closeable> cloneRepository(Context context, File target, String initialBranch) {
initialBranchForLastCloneCall = initialBranch;
return new ParentAndClone<>(parent, clone);
protected ParentAndClone<Closeable, Closeable> 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<Closeable, Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {
try (WorkingCopy<Closeable, Closeable> 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<Closeable, Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {}
try (WorkingCopy<Closeable, Closeable> 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<Closeable, Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {}
try (WorkingCopy<Closeable, Closeable> workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {}
verify(clone).close();
}
@Test
public void shouldDeleteWorkdirIfNotCached() {
Context context = new Context();
try (WorkingCopy<Closeable, Closeable> workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {}
assertThat(workdir).doesNotExist();
}
@Test
public void shouldNotDeleteWorkdirIfCached() {
Context context = new Context();
workdirIsCached = true;
try (WorkingCopy<Closeable, Closeable> workingCopy = simpleWorkingCopyFactory.createWorkingCopy(context, null)) {}
assertThat(workdir).exists();
}
@Test
public void shouldPropagateInitialBranch() {
Context context = new Context();
try (WorkingCopy<Closeable, Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) {
try (WorkingCopy<Closeable, Closeable> 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;
}
}
}

View File

@@ -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) {

View File

@@ -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<Repository, Repository, GitContext> {
public interface GitWorkingCopyFactory extends WorkingCopyFactory<Repository, Repository, GitContext> {
}

View File

@@ -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, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
try (WorkingCopy<Repository, Repository> workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) {
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) {
try (WorkingCopy<Repository, Repository> workingCopy = workingCopyFactory.createWorkingCopy(context, initialBranch)) {
Repository repository = workingCopy.getWorkingRepository();
logger.debug("cloned repository to folder {}", repository.getWorkTree());
return workerSupplier.apply(new Git(repository)).run();

View File

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

View File

@@ -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<MergeStrategy> 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());

View File

@@ -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<Lock> 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<String> implements ModifyWorkerHelper {

View File

@@ -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

View File

@@ -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<Repository, Repository, GitContext> 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<Repository, Repository> cloneRepository(GitContext context, File target, String initialBranch) {
public ParentAndClone<Repository, Repository> 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<Repository, Re
throw notFound(entity("Branch", initialBranch).in(context.getRepository()));
}
return new ParentAndClone<>(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();
}
}

View File

@@ -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<Repository, Repository> 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);
}
}
}

View File

@@ -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<Repository, Repository, GitContext> implements GitWorkingCopyFactory {
@Inject
public SimpleGitWorkingCopyFactory(WorkingCopyPool workdirProvider) {
super(workdirProvider);
}
@Override
public ParentAndClone<Repository, Repository> initialize(GitContext context, File target, String initialBranch) {
return new GitWorkingCopyInitializer(this, context).initialize(target, initialBranch);
}
@Override
public ParentAndClone<Repository, Repository> 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();
}
}
}

View File

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

View File

@@ -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");

View File

@@ -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<Git> interceptor) {
return new GitMergeCommand(createContext(), new SimpleGitWorkdirFactory(new WorkdirProvider())) {
return new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider()))) {
@Override
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) {
Function<Git, W> interceptedWorkerSupplier = git -> {
interceptor.accept(git);
return workerSupplier.apply(git);
};
return super.inClone(interceptedWorkerSupplier, workdirFactory, initialBranch);
return super.inClone(interceptedWorkerSupplier, workingCopyFactory, initialBranch);
}
};
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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();
}
}

View File

@@ -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<HgContext> 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<HgContext> 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() {

View File

@@ -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<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> 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<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext(), branchName)) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> 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<com.aragost.javahg.Repository, com.aragost.javahg.Repository> 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

View File

@@ -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 */

View File

@@ -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<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> 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<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> 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(),

View File

@@ -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());
}
/**

View File

@@ -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<Repository, Repository, HgCommandContext> {
public interface HgWorkingCopyFactory extends WorkingCopyFactory<Repository, Repository, HgCommandContext> {
void configure(PullCommand pullCommand);
}

View File

@@ -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<Repository, Repository, HgCommandContext> implements HgWorkdirFactory {
public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Repository, Repository, HgCommandContext> implements HgWorkingCopyFactory {
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
@Inject
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) {
public SimpleHgWorkingCopyFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkingCopyPool workdirProvider) {
super(workdirProvider);
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
}
@Override
public ParentAndClone<Repository, Repository> cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException {
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer);
public ParentAndClone<Repository, Repository> 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<Repository, Repository> 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<sonia.scm.repository.Repository, Map<String, String>> 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<Repository, Rep
}
@Override
protected void closeWorkdirInternal(Repository workdir) throws Exception {
workdir.close();
}
@Override
protected sonia.scm.repository.Repository getScmRepository(HgCommandContext context) {
return context.getScmRepository();
protected void closeWorkingCopy(Repository workingCopy) {
workingCopy.close();
}
@Override

View File

@@ -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 --------------------------------------------------------
@@ -37,8 +37,8 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgContextProvider;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.spi.HgWorkdirFactory;
import sonia.scm.repository.spi.SimpleHgWorkdirFactory;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
/**
*
@@ -75,6 +75,6 @@ public class HgServletModule extends ServletModule
// bind servlets
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
bind(HgWorkdirFactory.class).to(SimpleHgWorkdirFactory.class);
bind(HgWorkingCopyFactory.class).to(SimpleHgWorkingCopyFactory.class);
}
}

View File

@@ -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.commands.PullCommand;
@@ -32,7 +32,8 @@ import sonia.scm.repository.Branch;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.BranchRequest;
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.util.List;
@@ -42,14 +43,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
private SimpleHgWorkdirFactory workdirFactory;
private SimpleHgWorkingCopyFactory workingCopyFactory;
@Before
public void initWorkdirFactory() {
public void initWorkingCopyFactory() {
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) {
workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(hgRepositoryEnvironmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
@Override
public void configure(PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test
@@ -62,7 +63,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase {
BranchRequest branchRequest = new BranchRequest();
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("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<Branch> readBranches() {

View File

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

View File

@@ -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<Repository, Repository> workingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default");
File initialDirectory = workingCopy.getDirectory();
workingCopy.close();
assertThat(initialDirectory).exists();
assertThat(initialDirectory.toPath().resolve("f.txt")).exists();
WorkingCopy<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> 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<Repository, Repository> cachedWorkingCopy = workingCopyFactory.createWorkingCopy(cmdContext, "default");
assertThat(cachedWorkingCopy.getDirectory()).isEqualTo(initialDirectory);
assertThat(cachedWorkingCopy.getDirectory().toPath().resolve("newDir")).isEmptyDirectory();
}
}

View File

@@ -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<File, File, SvnContext> {
public interface SvnWorkingCopyFactory extends WorkingCopyFactory<File, File, SvnContext> {
}

View File

@@ -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<File, File, SvnContext> implements SvnWorkingCopyFactory {
@Test
public void shouldExecuteGivenMethodAtClose() {
Consumer<AutoCloseable> wrapped = new Consumer<AutoCloseable>() {
// 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<AutoCloseable> closer = spy(wrapped);
@Override
protected ParentAndClone<File, File> initialize(SvnContext context, File workingCopy, String initialBranch) {
return new SvnWorkingCopyInitializer(context).initialize(workingCopy);
}
AutoCloseable autoCloseable = () -> {};
try (CloseableWrapper<AutoCloseable> wrapper = new CloseableWrapper<>(autoCloseable, closer)) {
// nothing to do here
}
@Override
protected ParentAndClone<File, File> 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
}
}

View File

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

View File

@@ -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<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null)) {
try (WorkingCopy<File, File> workingCopy = workingCopyFactory.createWorkingCopy(context, null)) {
File workingDirectory = workingCopy.getDirectory();
modifyWorkingDirectory(request, clientManager, workingDirectory);
return commitChanges(clientManager, workingDirectory, request.getCommitMessage());

View File

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

View File

@@ -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;

View File

@@ -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<File, File, SvnContext> 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<File, File> cloneRepository(SvnContext context, File workingCopy, String initialBranch) {
public ParentAndClone<File, File> 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<File, File, Sv
checkout.setSource(SvnTarget.fromURL(source));
checkout.run();
} catch (SVNException ex) {
throw new InternalRepositoryException(getScmRepository(context), "error running svn checkout", ex);
throw new InternalRepositoryException(context.getRepository(), "error running svn checkout", ex);
} finally {
svnOperationFactory.dispose();
}
return new ParentAndClone<>(context.getDirectory(), workingCopy);
}
@Override
protected void closeRepository(File workingCopy) {
}
@Override
protected void closeWorkdirInternal(File workdir) {
return new ParentAndClone<>(context.getDirectory(), workingCopy, workingCopy);
}
}

View File

@@ -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<File, File> 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);
}
}

View File

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

View File

@@ -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<File, File> 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<File, File> 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<File, File> 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<File, File> 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<File, File> 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();
}
}

View File

@@ -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<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null);
WorkingCopy<File, File> 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<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null);
WorkingCopy<File, File> 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<File, File> workingCopy = workDirFactory.createWorkingCopy(context, null);
WorkingCopy<File, File> workingCopy = workingCopyFactory.createWorkingCopy(context, null);
assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).exists();
assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).hasContent("");
}

View File

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

View File

@@ -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<? extends WorkingCopyPool> strategyClass = (Class<? extends WorkingCopyPool>) classLoader.loadClass(workingCopyPoolStrategy);
bind(WorkingCopyPool.class).to(strategyClass);
} catch (Exception e) {
throw new IllegalStateException("could not instantiate class for working copy pool: " + workingCopyPoolStrategy, e);
}
}
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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<WorkingCopyPool> 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 <R, W> WorkingCopy<R, W> getWorkingCopy(SimpleWorkingCopyFactory<R, W, ?>.WorkingCopyContext context) {
return null;
}
@Override
public void contextClosed(SimpleWorkingCopyFactory<?, ?, ?>.WorkingCopyContext workingCopyContext, File workdir) {
}
@Override
public void shutdown() {
}
}
}