diff --git a/pom.xml b/pom.xml
index 3166c6ec75..3ab3cb82a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -439,6 +439,13 @@
Example:
+ *
+ *
+ *
+ * You can collect multiple changes before they are executed with a call to {@link #execute()}.
+ *
+ *
+ * commandBuilder
+ * .setBranch("feature/branch")
+ * .setCommitMessage("make some changes")
+ * .setAuthor(new Person())
+ * .createFile("file/to/create").withData(inputStream)
+ * .deleteFile("old/file/to/delete")
+ * .execute();
+ *
+ *
true.
+ * @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 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)}.
+ * @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 SimpleContentLoader modifyFile(String path) {
+ return new SimpleContentLoader(
+ 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, branch and at least one request 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 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.
+ */
+ 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 Consumertrue 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();
+ 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