diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
index 3249e54ec3..8e95e314bb 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
@@ -66,5 +66,5 @@ public enum Command
/**
* @since 2.0
*/
- MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH;
+ MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java
new file mode 100644
index 0000000000..28adca18ae
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java
@@ -0,0 +1,206 @@
+package sonia.scm.repository.api;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteSource;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.util.IOUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.function.Consumer;
+
+/**
+ * Use this {@link ModifyCommandBuilder} to make file changes to the head of a branch. You can
+ *
+ * - create new files ({@link #createFile(String)}
+ * - modify existing files ({@link #modifyFile(String)}
+ * - delete existing files ({@link #deleteFile(String)}
+ * - move/rename existing files ({@link #moveFile(String, String)}
+ *
+ *
+ * You can collect multiple changes before they are executed with a call to {@link #execute()}.
+ *
+ * Example:
+ *
+ * commandBuilder
+ * .setBranch("feature/branch")
+ * .setCommitMessage("make some changes")
+ * .setAuthor(new Person())
+ * .createFile("file/to/create").withData(inputStream)
+ * .deleteFile("old/file/to/delete")
+ * .execute();
+ *
+ *
+ */
+public class ModifyCommandBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandBuilder.class);
+
+ private final ModifyCommand command;
+ private final File workdir;
+
+ private final ModifyCommandRequest request = new ModifyCommandRequest();
+
+ ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) {
+ this.command = command;
+ this.workdir = workdirProvider.createNewWorkdir();
+ }
+
+ /**
+ * Set the branch that should be modified. The new commit will be made for this branch.
+ * @param branchToModify The branch to modify.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder setBranchToModify(String branchToModify) {
+ return this;
+ }
+
+ /**
+ * Create a new file. The content of the file will be specified in a subsequent call to
+ * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}.
+ * @param path The path and the name of the file that should be created.
+ * @return The loader to specify the content of the new file.
+ */
+ public ContentLoader createFile(String path) {
+ return new ContentLoader(
+ content -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content))
+ );
+ }
+
+ /**
+ * Modify an existing file. The new content of the file will be specified in a subsequent call to
+ * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}.
+ * @param path The path and the name of the file that should be modified.
+ * @return The loader to specify the new content of the file.
+ */
+ public ContentLoader modifyFile(String path) {
+ return new ContentLoader(
+ content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content))
+ );
+ }
+
+ /**
+ * Delete an existing file.
+ * @param path The path and the name of the file that should be deleted.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder deleteFile(String path) {
+ request.addRequest(new ModifyCommandRequest.DeleteFileRequest(path));
+ return this;
+ }
+
+ /**
+ * Move an existing file.
+ * @param sourcePath The path and the name of the file that should be moved.
+ * @param targetPath The new path and name the file should be moved to.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder moveFile(String sourcePath, String targetPath) {
+ request.addRequest(new ModifyCommandRequest.MoveFileRequest(sourcePath, targetPath));
+ return this;
+ }
+
+ /**
+ * Apply the changes and create a new commit with the given message and author.
+ * @return The revision of the new commit.
+ */
+ public String execute() {
+ try {
+ Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required");
+ return command.execute(request);
+ } finally {
+ try {
+ IOUtil.delete(workdir);
+ } catch (IOException e) {
+ LOG.warn("could not delete temporary workdir '{}'", workdir, e);
+ }
+ }
+ }
+
+ /**
+ * Set the commit message for the new commit.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder setCommitMessage(String message) {
+ request.setCommitMessage(message);
+ return this;
+ }
+
+ /**
+ * Set the author for the new commit.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder setAuthor(Person author) {
+ request.setAuthor(author);
+ return this;
+ }
+
+ /**
+ * Set the branch the changes should be made upon.
+ * @return This builder instance.
+ */
+ public ModifyCommandBuilder setBranch(String branch) {
+ request.setBranch(branch);
+ return this;
+ }
+
+ public class ContentLoader {
+
+ private final Consumer contentConsumer;
+
+ private ContentLoader(Consumer contentConsumer) {
+ this.contentConsumer = contentConsumer;
+ }
+
+ /**
+ * Specify the data of the file using a {@link ByteSource}.
+ * @return The builder instance.
+ * @throws IOException If the data could not be read.
+ */
+ public ModifyCommandBuilder withData(ByteSource data) throws IOException {
+ File content = loadData(data);
+ contentConsumer.accept(content);
+ return ModifyCommandBuilder.this;
+ }
+
+ /**
+ * Specify the data of the file using an {@link InputStream}.
+ * @return The builder instance.
+ * @throws IOException If the data could not be read.
+ */
+ public ModifyCommandBuilder withData(InputStream data) throws IOException {
+ File content = loadData(data);
+ contentConsumer.accept(content);
+ return ModifyCommandBuilder.this;
+ }
+ }
+
+ @SuppressWarnings("UnstableApiUsage") // Files only used internal
+ private File loadData(ByteSource data) throws IOException {
+ File file = createTemporaryFile();
+ data.copyTo(Files.asByteSink(file));
+ return file;
+ }
+
+ @SuppressWarnings("UnstableApiUsage") // Files and ByteStreams only used internal
+ private File loadData(InputStream data) throws IOException {
+ File file = createTemporaryFile();
+ try (OutputStream out = Files.asByteSink(file).openBufferedStream()) {
+ ByteStreams.copy(data, out);
+ }
+ return file;
+ }
+
+ private File createTemporaryFile() throws IOException {
+ return File.createTempFile("upload-", "", workdir);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index 5807ffa998..080d66ebf2 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -40,6 +40,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 java.io.Closeable;
import java.io.IOException;
@@ -91,22 +92,25 @@ public final class RepositoryService implements Closeable {
private final RepositoryServiceProvider provider;
private final Repository repository;
private final Set protocolProviders;
+ private final WorkdirProvider workdirProvider;
/**
* Constructs a new {@link RepositoryService}. This constructor should only
* be called from the {@link RepositoryServiceFactory}.
- * @param cacheManager cache manager
+ * @param cacheManager cache manager
* @param provider implementation for {@link RepositoryServiceProvider}
* @param repository the repository
+ * @param workdirProvider
*/
RepositoryService(CacheManager cacheManager,
- RepositoryServiceProvider provider, Repository repository,
- PreProcessorUtil preProcessorUtil, Set protocolProviders) {
+ RepositoryServiceProvider provider, Repository repository,
+ PreProcessorUtil preProcessorUtil, Set protocolProviders, WorkdirProvider workdirProvider) {
this.cacheManager = cacheManager;
this.provider = provider;
this.repository = repository;
this.preProcessorUtil = preProcessorUtil;
this.protocolProviders = protocolProviders;
+ this.workdirProvider = workdirProvider;
}
/**
@@ -399,6 +403,27 @@ public final class RepositoryService implements Closeable {
return new MergeCommandBuilder(provider.getMergeCommand());
}
+ /**
+ * The modify command makes changes to the head of a branch. It is possible to
+ *
+ * - create new files
+ * - delete existing files
+ * - modify/replace files
+ * - move files
+ *
+ *
+ * @return instance of {@link ModifyCommandBuilder}
+ * @throws CommandNotSupportedException if the command is not supported
+ * by the implementation of the repository service provider.
+ * @since 2.0.0
+ */
+ public ModifyCommandBuilder getModifyCommand() {
+ LOG.debug("create modify command for repository {}",
+ repository.getNamespaceAndName());
+
+ return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider);
+ }
+
/**
* Returns true if the command is supported by the repository service.
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index 8db3ab7546..fe2af9f9e1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -61,6 +61,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.security.ScmSecurityException;
import java.util.Set;
@@ -256,7 +257,7 @@ public final class RepositoryServiceFactory
}
service = new RepositoryService(cacheManager, provider, repository,
- preProcessorUtil, protocolProviders);
+ preProcessorUtil, protocolProviders, workdirProvider);
break;
}
@@ -373,4 +374,6 @@ public final class RepositoryServiceFactory
private final Set resolvers;
private Set protocolProviders;
+
+ private WorkdirProvider workdirProvider;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java
new file mode 100644
index 0000000000..07b29e8d21
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java
@@ -0,0 +1,18 @@
+package sonia.scm.repository.spi;
+
+import java.io.File;
+
+public interface ModifyCommand {
+
+ String execute(ModifyCommandRequest request);
+
+ interface Worker {
+ void delete(String toBeDeleted);
+
+ void create(String toBeCreated, File file);
+
+ void modify(String path, File file);
+
+ void move(String sourcePath, String targetPath);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java
new file mode 100644
index 0000000000..e8f2223bdd
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java
@@ -0,0 +1,155 @@
+package sonia.scm.repository.spi;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.Validateable;
+import sonia.scm.repository.Person;
+import sonia.scm.util.IOUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ModifyCommandRequest implements Resetable, Validateable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class);
+
+ private final List requests = new ArrayList<>();
+
+ private Person author;
+ private String commitMessage;
+ private String branch;
+
+ @Override
+ public void reset() {
+ requests.clear();
+ author = null;
+ commitMessage = null;
+ branch = null;
+ }
+
+ public void addRequest(PartialRequest request) {
+ this.requests.add(request);
+ }
+
+ public void setAuthor(Person author) {
+ this.author = author;
+ }
+
+ public void setCommitMessage(String commitMessage) {
+ this.commitMessage = commitMessage;
+ }
+
+ public void setBranch(String branch) {
+ this.branch = branch;
+ }
+
+ public List getRequests() {
+ return Collections.unmodifiableList(requests);
+ }
+
+ public Person getAuthor() {
+ return author;
+ }
+
+ public String getCommitMessage() {
+ return commitMessage;
+ }
+
+ public String getBranch() {
+ return branch;
+ }
+
+ @Override
+ public boolean isValid() {
+ return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && author != null && !requests.isEmpty();
+ }
+
+ public interface PartialRequest {
+ void execute(ModifyCommand.Worker worker);
+ }
+
+ public static class DeleteFileRequest implements PartialRequest {
+ private final String path;
+
+ public DeleteFileRequest(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void execute(ModifyCommand.Worker worker) {
+ worker.delete(path);
+ }
+ }
+
+ public static class MoveFileRequest implements PartialRequest {
+ private final String sourcePath;
+ private final String targetPath;
+
+ public MoveFileRequest(String sourcePath, String targetPath) {
+ this.sourcePath = sourcePath;
+ this.targetPath = targetPath;
+ }
+
+ @Override
+ public void execute(ModifyCommand.Worker worker) {
+ worker.move(sourcePath, targetPath);
+ }
+ }
+
+ private abstract static class ContentModificationRequest implements PartialRequest {
+
+ private final File content;
+
+ ContentModificationRequest(File content) {
+ this.content = content;
+ }
+
+ File getContent() {
+ return content;
+ }
+
+ void cleanup() {
+ try {
+ IOUtil.delete(content);
+ } catch (IOException e) {
+ LOG.warn("could not delete temporary file {}", content, e);
+ }
+ }
+ }
+
+ public static class CreateFileRequest extends ContentModificationRequest {
+
+ private final String path;
+
+ public CreateFileRequest(String path, File content) {
+ super(content);
+ this.path = path;
+ }
+
+ @Override
+ public void execute(ModifyCommand.Worker worker) {
+ worker.create(path, getContent());
+ cleanup();
+ }
+ }
+
+ public static class ModifyFileRequest extends ContentModificationRequest {
+
+ private final String path;
+
+ public ModifyFileRequest(String path, File content) {
+ super(content);
+ this.path = path;
+ }
+
+ @Override
+ public void execute(ModifyCommand.Worker worker) {
+ worker.modify(path, getContent());
+ cleanup();
+ }
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
index bf9cdf6a25..cdd0417cf7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
@@ -275,4 +275,12 @@ public abstract class RepositoryServiceProvider implements Closeable
{
throw new CommandNotSupportedException(Command.MERGE);
}
+
+ /**
+ * @since 2.0
+ */
+ public ModifyCommand getModifyCommand()
+ {
+ throw new CommandNotSupportedException(Command.MODIFY);
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java
index 0872242612..7236e0c3fe 100644
--- a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java
@@ -7,29 +7,21 @@ import sonia.scm.repository.Repository;
import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
public abstract class SimpleWorkdirFactory implements WorkdirFactory {
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
- private final File poolDirectory;
+ private final WorkdirProvider workdirProvider;
- public SimpleWorkdirFactory() {
- this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work"));
- }
-
- public SimpleWorkdirFactory(File poolDirectory) {
- this.poolDirectory = poolDirectory;
- if (!poolDirectory.exists() && !poolDirectory.mkdirs()) {
- throw new IllegalStateException("could not create pool directory " + poolDirectory);
- }
+ public SimpleWorkdirFactory(WorkdirProvider workdirProvider) {
+ this.workdirProvider = workdirProvider;
}
@Override
public WorkingCopy createWorkingCopy(C context) {
try {
- File directory = createNewWorkdir();
+ File directory = workdirProvider.createNewWorkdir();
ParentAndClone parentAndClone = cloneRepository(context, directory);
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory);
} catch (IOException e) {
@@ -45,10 +37,6 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory
protected abstract ParentAndClone cloneRepository(C context, File target) throws IOException;
- private File createNewWorkdir() throws IOException {
- return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
- }
-
private void close(R repository) {
try {
closeRepository(repository);
diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java
new file mode 100644
index 0000000000..35dae56faa
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java
@@ -0,0 +1,29 @@
+package sonia.scm.repository.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+public class WorkdirProvider {
+
+ private final File poolDirectory;
+
+ 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 File createNewWorkdir() {
+ try {
+ return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
+ } catch (IOException e) {
+ throw new RuntimeException("could not create temporary workdir", e);
+ }
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java
new file mode 100644
index 0000000000..7d4d96642a
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java
@@ -0,0 +1,184 @@
+package sonia.scm.repository.api;
+
+import com.google.common.io.ByteSource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junitpioneer.jupiter.TempDirectory;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@ExtendWith(TempDirectory.class)
+class ModifyCommandBuilderTest {
+
+ @Mock
+ ModifyCommand command;
+ @Mock
+ WorkdirProvider workdirProvider;
+ @Mock
+ ModifyCommand.Worker worker;
+
+ ModifyCommandBuilder commandBuilder;
+ Path workdir;
+
+ @BeforeEach
+ void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException {
+ workdir = Files.createDirectory(temp.resolve("workdir"));
+ lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile());
+ commandBuilder = new ModifyCommandBuilder(command, workdirProvider);
+ }
+
+ @BeforeEach
+ void initRequestCaptor() {
+ when(command.execute(any())).thenAnswer(
+ invocation -> {
+ ModifyCommandRequest request = invocation.getArgument(0);
+ request.getRequests().forEach(r -> r.execute(worker));
+ return "target";
+ }
+ );
+ }
+
+ @Test
+ void shouldReturnTargetRevisionFromCommit() {
+ String targetRevision = initCommand()
+ .deleteFile("toBeDeleted")
+ .execute();
+
+ assertThat(targetRevision).isEqualTo("target");
+ }
+
+ @Test
+ void shouldExecuteDelete() {
+ initCommand()
+ .deleteFile("toBeDeleted")
+ .execute();
+
+ verify(worker).delete("toBeDeleted");
+ }
+
+ @Test
+ void shouldExecuteMove() {
+ initCommand()
+ .moveFile("source", "target")
+ .execute();
+
+ verify(worker).move("source", "target");
+ }
+
+ @Test
+ void shouldExecuteCreateWithByteSourceContent() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any());
+
+ initCommand()
+ .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes()))
+ .execute();
+
+ assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
+ assertThat(contentCaptor).contains("content");
+ }
+
+ @Test
+ void shouldExecuteCreateWithInputStreamContent() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any());
+
+ initCommand()
+ .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes()))
+ .execute();
+
+ assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
+ assertThat(contentCaptor).contains("content");
+ }
+
+ @Test
+ void shouldExecuteCreateMultipleTimes() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any());
+
+ initCommand()
+ .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes()))
+ .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes()))
+ .execute();
+
+ List createdNames = nameCaptor.getAllValues();
+ assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1");
+ assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2");
+ assertThat(contentCaptor).contains("content_1", "content_2");
+ }
+
+ @Test
+ void shouldExecuteModify() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any());
+
+ initCommand()
+ .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
+ .execute();
+
+ assertThat(nameCaptor.getValue()).isEqualTo("toBeModified");
+ assertThat(contentCaptor).contains("content");
+ }
+
+ private ModifyCommandBuilder initCommand() {
+ return commandBuilder
+ .setBranch("branch")
+ .setCommitMessage("message")
+ .setAuthor(new Person());
+ }
+
+ @Test
+ void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class);
+ doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture());
+
+ initCommand()
+ .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
+ .execute();
+
+ assertThat(Files.list(temp)).isEmpty();
+ }
+
+ private static class ExtractContent implements Answer {
+ private final List contentCaptor;
+
+ public ExtractContent(List contentCaptor) {
+ this.contentCaptor = contentCaptor;
+ }
+
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0));
+ }
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
index 2ceafc19bb..330a4c956a 100644
--- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
+++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
@@ -24,7 +24,7 @@ public class RepositoryServiceTest {
@Test
public void shouldReturnMatchingProtocolsFromProvider() {
- RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
Stream supportedProtocols = repositoryService.getSupportedProtocols();
assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
@@ -32,7 +32,7 @@ public class RepositoryServiceTest {
@Test
public void shouldFindKnownProtocol() {
- RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
@@ -41,7 +41,7 @@ public class RepositoryServiceTest {
@Test
public void shouldFailForUnknownProtocol() {
- RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+ RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
assertThrows(IllegalArgumentException.class, () -> {
repositoryService.getProtocol(UnknownScmProtocol.class);
diff --git a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java
index 2d3c2ed59e..31aa11c604 100644
--- a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java
+++ b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java
@@ -28,7 +28,8 @@ public class SimpleWorkdirFactoryTest {
@Before
public void initFactory() throws IOException {
- simpleWorkdirFactory = new SimpleWorkdirFactory(temporaryFolder.newFolder()) {
+ WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
+ simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) {
@Override
protected Repository getScmRepository(Context context) {
return REPOSITORY;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java
index c30ff59afb..9edcf0c0ea 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java
@@ -7,16 +7,16 @@ import org.eclipse.jgit.transport.ScmTransportProtocol;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.util.SimpleWorkdirFactory;
+import sonia.scm.repository.util.WorkdirProvider;
+import javax.inject.Inject;
import java.io.File;
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory {
- public SimpleGitWorkdirFactory() {
- }
-
- SimpleGitWorkdirFactory(File poolDirectory) {
- super(poolDirectory);
+ @Inject
+ public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) {
+ super(workdirProvider);
}
@Override
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java
index e429aa5a42..aa5e641b31 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java
@@ -5,6 +5,7 @@ import org.junit.Rule;
import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.api.BranchRequest;
+import sonia.scm.repository.util.WorkdirProvider;
import java.io.IOException;
import java.util.List;
@@ -25,7 +26,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
branchRequest.setParentBranch(source.getName());
branchRequest.setNewBranch("new_branch");
- new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
+ new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
Branch newBranch = findBranch(context, "new_branch");
Assertions.assertThat(newBranch.getRevision()).isEqualTo(source.getRevision());
@@ -45,7 +46,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
BranchRequest branchRequest = new BranchRequest();
branchRequest.setNewBranch("new_branch");
- new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
+ new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java
index e8a62f5b86..8b3b4548d9 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java
@@ -24,6 +24,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
+import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.user.User;
import java.io.IOException;
@@ -244,6 +245,6 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
}
private GitMergeCommand createCommand() {
- return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
+ return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
}
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java
index 616df5e68e..1b3c730ef1 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java
@@ -11,6 +11,7 @@ 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 java.io.File;
@@ -29,19 +30,21 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
// keep this so that it will not be garbage collected (Transport keeps this in a week reference)
private ScmTransportProtocol proto;
+ private WorkdirProvider workdirProvider;
@Before
- public void bindScmProtocol() {
+ public void bindScmProtocol() throws IOException {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory);
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(proto);
+ workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
}
@Test
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
- SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File masterRepo = createRepositoryDirectory();
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
@@ -59,7 +62,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
@Test
public void cloneFromPoolShouldNotBeReused() throws IOException {
- SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File firstDirectory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
@@ -73,7 +76,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
@Test
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
- SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File directory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java
index 9565f672d5..b9194145e6 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java
@@ -4,6 +4,7 @@ import com.aragost.javahg.Repository;
import com.aragost.javahg.commands.CloneCommand;
import com.aragost.javahg.commands.PullCommand;
import sonia.scm.repository.util.SimpleWorkdirFactory;
+import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import javax.inject.Inject;
@@ -18,7 +19,8 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory hgRepositoryEnvironmentBuilder;
@Inject
- public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder) {
+ public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) {
+ super(workdirProvider);
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
}
@Override
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java
index c015aa0f62..7976af8b3b 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java
@@ -7,6 +7,7 @@ import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.api.BranchRequest;
+import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.IOException;
@@ -20,7 +21,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase {
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
- SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder)) {
+ SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) {
@Override
public void configure(PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test