diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java new file mode 100644 index 0000000000..553f0f5a00 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java @@ -0,0 +1,25 @@ +package sonia.scm.repository; + +import java.util.function.Consumer; + +public class CloseableWrapper implements AutoCloseable { + + private final C wrapped; + private final Consumer cleanup; + + public CloseableWrapper(C wrapped, Consumer cleanup) { + this.wrapped = wrapped; + this.cleanup = cleanup; + } + + public C get() { return wrapped; } + + @Override + public void close() { + try { + cleanup.accept(wrapped); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index c3436c2395..00add44466 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -90,6 +90,8 @@ public class GitRepositoryHandler private static final Object LOCK = new Object(); private final Scheduler scheduler; + + private final GitWorkdirPool workdirPool; private Task task; @@ -104,10 +106,11 @@ public class GitRepositoryHandler * @param scheduler */ @Inject - public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler) + public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, GitWorkdirPool workdirPool) { super(storeFactory, fileSystem); this.scheduler = scheduler; + this.workdirPool = workdirPool; } //~--- get methods ---------------------------------------------------------- @@ -235,4 +238,8 @@ public class GitRepositoryHandler { return new File(directory, DIRECTORY_REFS).exists(); } + + public GitWorkdirPool getWorkdirPool() { + return workdirPool; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirPool.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirPool.java new file mode 100644 index 0000000000..2c94a75be5 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirPool.java @@ -0,0 +1,46 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; + +import java.io.File; +import java.util.Random; + + +/** + * Config: + * + * 1. Overall and absolute maximum of temp work directories + * 2. Maximum number of temp work directories pooled overall + * 3. Maximum number of temp work directories pooled for one master repository + */ +public class GitWorkdirPool { + + private final Random random = new Random(); + private final File poolDirectory; + + public GitWorkdirPool() { + this(new File(System.getProperty("java.io.tmpdir"))); + } + + public GitWorkdirPool(File poolDirectory) { + this.poolDirectory = poolDirectory; + } + + public CloseableWrapper getWorkingCopy(File bareRepository) { + try { + Git clone = cloneRepository(bareRepository, new File(poolDirectory, Long.toString(random.nextLong()))); + return new CloseableWrapper<>(clone.getRepository(), r -> clone.close()); + } catch (GitAPIException e) { + throw new InternalRepositoryException("could not clone working copy of repository", e); + } + } + + protected Git cloneRepository(File bareRepository, File target) throws GitAPIException { + return Git.cloneRepository() + .setURI(bareRepository.getAbsolutePath()) + .setDirectory(target) + .call(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java new file mode 100644 index 0000000000..b648681f61 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -0,0 +1,35 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.ResolveMerger; +import sonia.scm.repository.CloseableWrapper; +import sonia.scm.repository.GitWorkdirPool; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; + +import java.io.IOException; + +public class GitMergeCommand extends AbstractGitCommand implements MergeCommand { + + private final GitWorkdirPool workdirPool; + + GitMergeCommand(GitContext context, Repository repository, GitWorkdirPool workdirPool) { + super(context, repository); + this.workdirPool = workdirPool; + } + + @Override + public boolean merge(MergeCommandRequest request) { + try (CloseableWrapper workingCopy = workdirPool.getWorkingCopy(context.open().getDirectory())) { + org.eclipse.jgit.lib.Repository repository = workingCopy.get(); + ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository); + boolean mergeResult = merger.merge(repository.resolve(request.getBranchToMerge()), repository.resolve(request.getTargetBranch())); + if (mergeResult) { + // TODO push and verify push was successful + } + return mergeResult; + } catch (IOException e) { + throw new InternalRepositoryException(e); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index dd6c1b5492..ac5c161dc4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -245,6 +245,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider public MergeDryRunCommand getMergeDryRunCommand() { return new GitMergeDryRunCommand(context, repository); } + + @Override + public MergeCommand getMergeCommand() { + return new GitMergeCommand(context, repository, handler.getWorkdirPool()); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java new file mode 100644 index 0000000000..34c5e8e7e2 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java @@ -0,0 +1,29 @@ +package sonia.scm.repository; + +import org.junit.Test; + +import java.util.function.Consumer; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class CloseableWrapperTest { + + @Test + public void x() { + Consumer wrapped = new Consumer() { + // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy + @Override + public void accept(String s) { + } + }; + + Consumer closer = spy(wrapped); + + try (CloseableWrapper wrapper = new CloseableWrapper<>("test", closer)) { + // nothing to do here + } + + verify(closer).accept("test"); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitWorkdirPoolTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitWorkdirPoolTest.java new file mode 100644 index 0000000000..133df75ea7 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitWorkdirPoolTest.java @@ -0,0 +1,59 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.spi.AbstractGitCommandTestBase; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class GitWorkdirPoolTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void emptyPoolShouldCreateNewWorkdir() throws IOException { + GitWorkdirPool pool = new GitWorkdirPool(temporaryFolder.newFolder()); + File masterRepo = createRepositoryDirectory(); + + CloseableWrapper workingCopy = pool.getWorkingCopy(masterRepo); + + assertThat(workingCopy) + .isNotNull() + .extracting(w -> w.get().getDirectory()) + .isNotEqualTo(masterRepo); + } + + @Test + public void cloneFromPoolShouldBeClosed() throws IOException { + PoolWithSpy pool = new PoolWithSpy(temporaryFolder.newFolder()); + File masterRepo = createRepositoryDirectory(); + + try (CloseableWrapper workingCopy = pool.getWorkingCopy(masterRepo)) { + assertThat(workingCopy).isNotNull(); + } + verify(pool.createdClone).close(); + } + + private static class PoolWithSpy extends GitWorkdirPool { + PoolWithSpy(File poolDirectory) { + super(poolDirectory); + } + + Git createdClone; + @Override + protected Git cloneRepository(File bareRepository, File destination) throws GitAPIException { + createdClone = spy(super.cloneRepository(bareRepository, destination)); + return createdClone; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index 496b71e656..7d56db00d7 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -50,7 +50,9 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase @After public void close() { - context.close(); + if (context != null) { + context.close(); + } } /**