diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java
index bddf9b378a..f5561c839a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java
@@ -16,12 +16,14 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.function.BiConsumer;
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)}
+ * - create new files ({@link #createFile(String)} (with the option to overwrite a file, if it already exists; by
+ * default a {@link sonia.scm.AlreadyExistsException} will be thrown)
* - modify existing files ({@link #modifyFile(String)}
* - delete existing files ({@link #deleteFile(String)}
* - move/rename existing files ({@link #moveFile(String, String)}
@@ -66,24 +68,24 @@ public class ModifyCommandBuilder {
/**
* 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)}.
+ * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#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))
+ public WithOverwriteFlagContentLoader createFile(String path) {
+ return new WithOverwriteFlagContentLoader(
+ (content, overwrite) -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content, overwrite))
);
}
/**
* 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)}.
+ * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#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(
+ public SimpleContentLoader modifyFile(String path) {
+ return new SimpleContentLoader(
content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content))
);
}
@@ -153,30 +155,39 @@ public class ModifyCommandBuilder {
return this;
}
- public class ContentLoader {
-
- private final Consumer contentConsumer;
-
- private ContentLoader(Consumer contentConsumer) {
- this.contentConsumer = contentConsumer;
- }
-
+ public interface ContentLoader {
/**
* 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;
- }
+ ModifyCommandBuilder withData(ByteSource data) throws IOException;
/**
* Specify the data of the file using an {@link InputStream}.
* @return The builder instance.
* @throws IOException If the data could not be read.
*/
+ ModifyCommandBuilder withData(InputStream data) throws IOException;
+ }
+
+ public class SimpleContentLoader implements ContentLoader {
+
+ private final Consumer contentConsumer;
+
+ private SimpleContentLoader(Consumer contentConsumer) {
+ this.contentConsumer = contentConsumer;
+ }
+
+ @Override
+ public ModifyCommandBuilder withData(ByteSource data) throws IOException {
+ File content = loadData(data);
+ contentConsumer.accept(content);
+ return ModifyCommandBuilder.this;
+ }
+
+ @Override
public ModifyCommandBuilder withData(InputStream data) throws IOException {
File content = loadData(data);
contentConsumer.accept(content);
@@ -184,6 +195,36 @@ public class ModifyCommandBuilder {
}
}
+ public class WithOverwriteFlagContentLoader implements ContentLoader {
+
+ private final ContentLoader contentLoader;
+ private boolean overwrite = false;
+
+ private WithOverwriteFlagContentLoader(BiConsumer contentConsumer) {
+ this.contentLoader = new SimpleContentLoader(file -> contentConsumer.accept(file, overwrite));
+ }
+
+ /**
+ * Set this to true to overwrite the file if it already exists. Otherwise an
+ * {@link sonia.scm.AlreadyExistsException} will be thrown.
+ * @return This loader instance.
+ */
+ public WithOverwriteFlagContentLoader setOverwrite(boolean overwrite) {
+ this.overwrite = overwrite;
+ return this;
+ }
+
+ @Override
+ public ModifyCommandBuilder withData(ByteSource data) throws IOException {
+ return contentLoader.withData(data);
+ }
+
+ @Override
+ public ModifyCommandBuilder withData(InputStream data) throws IOException {
+ return contentLoader.withData(data);
+ }
+ }
+
@SuppressWarnings("UnstableApiUsage") // Files only used internal
private File loadData(ByteSource data) throws IOException {
File file = createTemporaryFile();
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
index a8b3efd6d9..578944c47f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java
@@ -10,7 +10,7 @@ public interface ModifyCommand {
interface Worker {
void delete(String toBeDeleted);
- void create(String toBeCreated, File file) throws IOException;
+ void create(String toBeCreated, File file, boolean overwrite) throws IOException;
void modify(String path, File file);
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
index 94bf84ae70..723b287b6e 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java
@@ -124,15 +124,17 @@ public class ModifyCommandRequest implements Resetable, Validateable {
public static class CreateFileRequest extends ContentModificationRequest {
private final String path;
+ private final boolean overwrite;
- public CreateFileRequest(String path, File content) {
+ public CreateFileRequest(String path, File content, boolean overwrite) {
super(content);
this.path = path;
+ this.overwrite = overwrite;
}
@Override
public void execute(ModifyCommand.Worker worker) throws IOException {
- worker.create(path, getContent());
+ worker.create(path, getContent(), overwrite);
cleanup();
}
}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java
index d88d720cdb..db9a247d65 100644
--- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java
+++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java
@@ -1,6 +1,7 @@
package sonia.scm.repository.api;
import com.google.common.io.ByteSource;
+import com.sun.org.apache.xpath.internal.operations.Bool;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,6 +26,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.lenient;
@@ -96,7 +98,7 @@ class ModifyCommandBuilderTest {
void shouldExecuteCreateWithByteSourceContent() throws IOException {
ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
List contentCaptor = new ArrayList<>();
- doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any());
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
initCommand()
.createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes()))
@@ -110,7 +112,7 @@ class ModifyCommandBuilderTest {
void shouldExecuteCreateWithInputStreamContent() throws IOException {
ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
List contentCaptor = new ArrayList<>();
- doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any());
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
initCommand()
.createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes()))
@@ -120,11 +122,43 @@ class ModifyCommandBuilderTest {
assertThat(contentCaptor).contains("content");
}
+ @Test
+ void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture());
+
+ initCommand()
+ .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes()))
+ .execute();
+
+ assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
+ assertThat(overwriteCaptor.getValue()).isFalse();
+ assertThat(contentCaptor).contains("content");
+ }
+
+ @Test
+ void shouldExecuteCreateWithOverwriteIfSet() throws IOException {
+ ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class);
+ List contentCaptor = new ArrayList<>();
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture());
+
+ initCommand()
+ .createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes()))
+ .execute();
+
+ assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
+ assertThat(overwriteCaptor.getValue()).isTrue();
+ 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());
+ doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
initCommand()
.createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes()))
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
index 8825645a3c..2c473398c5 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java
@@ -8,9 +8,14 @@ import sonia.scm.repository.Repository;
import java.io.File;
import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static sonia.scm.AlreadyExistsException.alreadyExists;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+
public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand {
private final GitWorkdirFactory workdirFactory;
@@ -48,10 +53,18 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
}
@Override
- public void create(String toBeCreated, File file) throws IOException {
+ public void create(String toBeCreated, File file, boolean overwrite) throws IOException {
Path targetFile = new File(workDir, toBeCreated).toPath();
Files.createDirectories(targetFile.getParent());
- Files.copy(file.toPath(), targetFile);
+ if (overwrite) {
+ Files.copy(file.toPath(), targetFile, REPLACE_EXISTING);
+ } else {
+ try {
+ Files.copy(file.toPath(), targetFile);
+ } catch (FileAlreadyExistsException e) {
+ throw alreadyExists(entity("file", toBeCreated).in("branch", request.getBranch()).in(context.getRepository()));
+ }
+ }
try {
getClone().add().addFilepattern(toBeCreated).call();
} catch (GitAPIException e) {
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java
index e8574b7baf..2d9f5d0235 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java
@@ -13,6 +13,7 @@ import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import sonia.scm.AlreadyExistsException;
import sonia.scm.repository.Person;
import sonia.scm.repository.util.WorkdirProvider;
@@ -41,7 +42,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
ModifyCommandRequest request = new ModifyCommandRequest();
request.setBranch("master");
request.setCommitMessage("test commit");
- request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile));
+ request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
String newRef = command.execute(request);
@@ -63,7 +64,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
ModifyCommandRequest request = new ModifyCommandRequest();
request.setBranch("master");
request.setCommitMessage("test commit");
- request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile));
+ request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
command.execute(request);
@@ -73,6 +74,40 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
assertInTree(assertions);
}
+ @Test(expected = AlreadyExistsException.class)
+ public void shouldFailIfOverwritingExistingFileWithoutOverwriteFlag() throws IOException, GitAPIException {
+ File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
+
+ GitModifyCommand command = createCommand();
+
+ ModifyCommandRequest request = new ModifyCommandRequest();
+ request.setBranch("master");
+ request.setCommitMessage("test commit");
+ request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, false));
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+
+ command.execute(request);
+ }
+
+ @Test
+ public void shouldOverwriteExistingFileIfOverwriteFlagSet() throws IOException, GitAPIException {
+ File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
+
+ GitModifyCommand command = createCommand();
+
+ ModifyCommandRequest request = new ModifyCommandRequest();
+ request.setBranch("master");
+ request.setCommitMessage("test commit");
+ request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, true));
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+
+ command.execute(request);
+
+ TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isTrue();
+
+ assertInTree(assertions);
+ }
+
private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);