diff --git a/scm-core/pom.xml b/scm-core/pom.xml
index f19d50064d..8437b256b2 100644
--- a/scm-core/pom.xml
+++ b/scm-core/pom.xml
@@ -131,6 +131,12 @@
mapstruct-jdk8
+
+ org.mapstruct
+ mapstruct-processor
+ provided
+
+
com.webcohesion.enunciate
@@ -208,6 +214,12 @@
shiro-unit
test
+
+ org.hibernate
+ hibernate-validator
+ 5.3.6.Final
+ compile
+
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java
similarity index 100%
rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java
rename to scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java
similarity index 100%
rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java
rename to scm-core/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java
similarity index 100%
rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java
rename to scm-core/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
index 0a2267e888..6c4324c259 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
@@ -6,6 +6,8 @@ import sonia.scm.repository.spi.MergeCommand;
import sonia.scm.repository.spi.MergeCommandRequest;
import sonia.scm.repository.util.AuthorUtil;
+import java.util.Set;
+
/**
* Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if
* the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of
@@ -55,6 +57,24 @@ public class MergeCommandBuilder {
this.mergeCommand = mergeCommand;
}
+ /**
+ * Use this to check if merge-strategy is supported by mergeCommand.
+ *
+ * @return boolean.
+ */
+ public boolean isSupported(MergeStrategy strategy) {
+ return mergeCommand.isSupported(strategy);
+ }
+
+ /**
+ * Use this to get a Set of all supported merge strategies by merge command.
+ *
+ * @return boolean.
+ */
+ public Set getSupportedMergeStrategies() {
+ return mergeCommand.getSupportedMergeStrategies();
+ }
+
/**
* Use this to set the branch that should be merged into the target branch.
*
@@ -92,6 +112,21 @@ public class MergeCommandBuilder {
return this;
}
+ /**
+ * Use this to set the strategy of the merge commit manually.
+ *
+ * This is optional and for {@link #executeMerge()} only.
+ *
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder setMergeStrategy(MergeStrategy strategy) {
+ if (!mergeCommand.isSupported(strategy)) {
+ throw new IllegalArgumentException("merge strategy not supported: " + strategy);
+ }
+ request.setMergeStrategy(strategy);
+ return this;
+ }
+
/**
* Use this to set a template for the commit message. If no message is set, a default message will be used.
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java
new file mode 100644
index 0000000000..4ccf1f706b
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java
@@ -0,0 +1,7 @@
+package sonia.scm.repository.api;
+
+public enum MergeStrategy {
+ MERGE_COMMIT,
+ FAST_FORWARD_IF_POSSIBLE,
+ SQUASH
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java
new file mode 100644
index 0000000000..ae9fbb08b4
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java
@@ -0,0 +1,27 @@
+package sonia.scm.repository.api;
+
+import sonia.scm.BadRequestException;
+import sonia.scm.repository.Repository;
+
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+
+@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
+public class MergeStrategyNotSupportedException extends BadRequestException {
+
+ private static final long serialVersionUID = 256498734456613496L;
+
+ private static final String CODE = "6eRhF9gU41";
+
+ public MergeStrategyNotSupportedException(Repository repository, MergeStrategy strategy) {
+ super(entity(repository).build(), createMessage(strategy));
+ }
+
+ @Override
+ public String getCode() {
+ return CODE;
+ }
+
+ private static String createMessage(MergeStrategy strategy) {
+ return "merge strategy " + strategy + " is not supported by this repository";
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java
index a9655a157f..12de0722a4 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java
@@ -2,6 +2,9 @@ package sonia.scm.repository.spi;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
+import sonia.scm.repository.api.MergeStrategy;
+
+import java.util.Set;
public interface MergeCommand {
MergeCommandResult merge(MergeCommandRequest request);
@@ -9,4 +12,8 @@ public interface MergeCommand {
MergeDryRunCommandResult dryRun(MergeCommandRequest request);
MergeConflictResult computeConflicts(MergeCommandRequest request);
+
+ boolean isSupported(MergeStrategy strategy);
+
+ Set getSupportedMergeStrategies();
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
index 223cf8c49e..0751920d01 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
@@ -5,6 +5,7 @@ import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
+import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor;
import java.io.Serializable;
@@ -17,6 +18,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
private String targetBranch;
private Person author;
private String messageTemplate;
+ private MergeStrategy mergeStrategy;
public String getBranchToMerge() {
return branchToMerge;
@@ -50,6 +52,14 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
this.messageTemplate = messageTemplate;
}
+ public MergeStrategy getMergeStrategy() {
+ return mergeStrategy;
+ }
+
+ public void setMergeStrategy(MergeStrategy mergeStrategy) {
+ this.mergeStrategy = mergeStrategy;
+ }
+
public boolean isValid() {
return !Strings.isNullOrEmpty(getBranchToMerge())
&& !Strings.isNullOrEmpty(getTargetBranch());
@@ -74,12 +84,13 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
return Objects.equal(branchToMerge, other.branchToMerge)
&& Objects.equal(targetBranch, other.targetBranch)
- && Objects.equal(author, other.author);
+ && Objects.equal(author, other.author)
+ && Objects.equal(mergeStrategy, other.mergeStrategy);
}
@Override
public int hashCode() {
- return Objects.hashCode(branchToMerge, targetBranch, author);
+ return Objects.hashCode(branchToMerge, targetBranch, author, mergeStrategy);
}
@Override
@@ -88,6 +99,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl
.add("branchToMerge", branchToMerge)
.add("targetBranch", targetBranch)
.add("author", author)
+ .add("mergeStrategy", mergeStrategy)
.toString();
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java
index c705e36e12..406c6849f2 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java
@@ -178,19 +178,28 @@ class AbstractGitCommand
}
ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException {
+ sonia.scm.repository.Repository scmRepository = context.getRepository();
+ return resolveRevisionOrThrowNotFound(repository, revision, scmRepository);
+ }
+
+ static ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision, sonia.scm.repository.Repository scmRepository) throws IOException {
ObjectId resolved = repository.resolve(revision);
if (resolved == null) {
- throw notFound(entity("Revision", revision).in(context.getRepository()));
+ throw notFound(entity("Revision", revision).in(scmRepository));
} else {
return resolved;
}
}
- abstract class GitCloneWorker {
+ abstract static class GitCloneWorker {
private final Git clone;
+ private final GitContext context;
+ private final sonia.scm.repository.Repository repository;
- GitCloneWorker(Git clone) {
+ GitCloneWorker(Git clone, GitContext context, sonia.scm.repository.Repository repository) {
this.clone = clone;
+ this.context = context;
+ this.repository = repository;
}
abstract R run() throws IOException;
@@ -199,6 +208,10 @@ class AbstractGitCommand
return clone;
}
+ GitContext getContext() {
+ return context;
+ }
+
void checkOutBranch(String branchName) throws IOException {
try {
clone.checkout().setName(branchName).call();
@@ -225,7 +238,7 @@ class AbstractGitCommand
ObjectId resolveRevision(String revision) throws IOException {
ObjectId resolved = clone.getRepository().resolve(revision);
if (resolved == null) {
- return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision);
+ return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision, context.getRepository());
} else {
return resolved;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java
new file mode 100644
index 0000000000..64a20a33cb
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java
@@ -0,0 +1,36 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand;
+import org.eclipse.jgit.api.MergeResult;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.MergeCommandResult;
+
+import java.io.IOException;
+
+class GitFastForwardIfPossible extends GitMergeStrategy {
+
+ private GitMergeStrategy fallbackMerge;
+
+ GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
+ super(clone, request, context, repository);
+ fallbackMerge = new GitMergeCommit(clone, request, context, repository);
+ }
+
+ @Override
+ MergeCommandResult run() throws IOException {
+ MergeResult fastForwardResult = mergeWithFastForwardOnlyMode();
+ if (fastForwardResult.getMergeStatus().isSuccessful()) {
+ push();
+ return MergeCommandResult.success();
+ } else {
+ return fallbackMerge.run();
+ }
+ }
+
+ private MergeResult mergeWithFastForwardOnlyMode() throws IOException {
+ MergeCommand mergeCommand = getClone().merge();
+ mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY);
+ return doMergeInClone(mergeCommand);
+ }
+}
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
index 2bedec93f0..4d6878d130 100644
--- 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
@@ -1,41 +1,39 @@
package sonia.scm.repository.spi;
-import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.filter.PathFilter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
-import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
+import sonia.scm.repository.api.MergeStrategy;
+import sonia.scm.repository.api.MergeStrategyNotSupportedException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.text.MessageFormat;
+import java.util.Set;
+
+import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE;
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
- private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
-
- private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
- "Merge of branch {0} into {1}",
- "",
- "Automatic merge by SCM-Manager.");
-
private final GitWorkdirFactory workdirFactory;
+ private static final Set STRATEGIES = ImmutableSet.of(
+ MergeStrategy.MERGE_COMMIT,
+ MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
+ MergeStrategy.SQUASH
+ );
+
GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
@@ -43,14 +41,36 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
@Override
public MergeCommandResult merge(MergeCommandRequest request) {
- return inClone(clone -> new MergeWorker(clone, request), workdirFactory, request.getTargetBranch());
+ return mergeWithStrategy(request);
+ }
+
+ @Override
+ public MergeConflictResult computeConflicts(MergeCommandRequest request) {
+ WorkingCopyCloser closer = new WorkingCopyCloser();
+ return inClone(git -> new ConflictWorker(git, request, closer), workdirFactory, request.getTargetBranch());
+ }
+
+ private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) {
+ switch(request.getMergeStrategy()) {
+ case SQUASH:
+ return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workdirFactory, request.getTargetBranch());
+
+ case FAST_FORWARD_IF_POSSIBLE:
+ return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workdirFactory, request.getTargetBranch());
+
+ case MERGE_COMMIT:
+ return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workdirFactory, request.getTargetBranch());
+
+ default:
+ throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
+ }
}
@Override
public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
try {
Repository repository = context.open();
- ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ ResolveMerger merger = (ResolveMerger) RECURSIVE.newMerger(repository, true);
return new MergeDryRunCommandResult(
merger.merge(
resolveRevisionOrThrowNotFound(repository, request.getBranchToMerge()),
@@ -61,70 +81,13 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
}
@Override
- public MergeConflictResult computeConflicts(MergeCommandRequest request) {
- WorkingCopyCloser closer = new WorkingCopyCloser();
- return inClone(git -> new ConflictWorker(git, request, closer), workdirFactory, request.getTargetBranch());
+ public boolean isSupported(MergeStrategy strategy) {
+ return STRATEGIES.contains(strategy);
}
- private class MergeWorker extends GitCloneWorker {
-
- private final String target;
- private final String toMerge;
- private final Person author;
- private final String messageTemplate;
-
- private MergeWorker(Git clone, MergeCommandRequest request) {
- super(clone);
- this.target = request.getTargetBranch();
- this.toMerge = request.getBranchToMerge();
- this.author = request.getAuthor();
- this.messageTemplate = request.getMessageTemplate();
- }
-
- @Override
- MergeCommandResult run() throws IOException {
- MergeResult result = doMergeInClone();
- if (result.getMergeStatus().isSuccessful()) {
- doCommit();
- push();
- return MergeCommandResult.success();
- } else {
- return analyseFailure(result);
- }
- }
-
- private MergeResult doMergeInClone() throws IOException {
- MergeResult result;
- try {
- ObjectId sourceRevision = resolveRevision(toMerge);
- result = getClone().merge()
- .setFastForward(FastForwardMode.NO_FF)
- .setCommit(false) // we want to set the author manually
- .include(toMerge, sourceRevision)
- .call();
- } catch (GitAPIException e) {
- throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
- }
- return result;
- }
-
- private void doCommit() {
- logger.debug("merged branch {} into {}", toMerge, target);
- doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author);
- }
-
- private String determineMessageTemplate() {
- if (Strings.isNullOrEmpty(messageTemplate)) {
- return MERGE_COMMIT_MESSAGE_TEMPLATE;
- } else {
- return messageTemplate;
- }
- }
-
- private MergeCommandResult analyseFailure(MergeResult result) {
- logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
- return MergeCommandResult.failure(result.getConflicts().keySet());
- }
+ @Override
+ public Set getSupportedMergeStrategies() {
+ return STRATEGIES;
}
private class ConflictWorker extends GitCloneWorker {
@@ -133,7 +96,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
private final WorkingCopyCloser closer;
private ConflictWorker(Git git, MergeCommandRequest request, WorkingCopyCloser closer) {
- super(git);
+ super(git, context, repository);
this.git = git;
this.request = request;
this.closer = closer;
@@ -145,7 +108,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
MergeResult mergeResult;
try {
mergeResult = getClone().merge()
- .setFastForward(FastForwardMode.NO_FF)
+ .setFastForward(org.eclipse.jgit.api.MergeCommand.FastForwardMode.NO_FF)
.setCommit(false)
.include(request.getBranchToMerge(), sourceRevision)
.call();
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java
new file mode 100644
index 0000000000..6aa68a0ea8
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java
@@ -0,0 +1,31 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand;
+import org.eclipse.jgit.api.MergeResult;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.MergeCommandResult;
+
+import java.io.IOException;
+
+class GitMergeCommit extends GitMergeStrategy {
+
+ GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
+ super(clone, request, context, repository);
+ }
+
+ @Override
+ MergeCommandResult run() throws IOException {
+ MergeCommand mergeCommand = getClone().merge();
+ mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF);
+ MergeResult result = doMergeInClone(mergeCommand);
+
+ if (result.getMergeStatus().isSuccessful()) {
+ doCommit();
+ push();
+ return MergeCommandResult.success();
+ } else {
+ return analyseFailure(result);
+ }
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java
new file mode 100644
index 0000000000..1d53b99c99
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java
@@ -0,0 +1,72 @@
+package sonia.scm.repository.spi;
+
+import com.google.common.base.Strings;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.repository.InternalRepositoryException;
+import sonia.scm.repository.Person;
+import sonia.scm.repository.api.MergeCommandResult;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker {
+
+ private static final Logger logger = LoggerFactory.getLogger(GitMergeStrategy.class);
+
+ private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
+ "Merge of branch {0} into {1}",
+ "",
+ "Automatic merge by SCM-Manager.");
+
+ private final String target;
+ private final String toMerge;
+ private final Person author;
+ private final String messageTemplate;
+
+ GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) {
+ super(clone, context, repository);
+ this.target = request.getTargetBranch();
+ this.toMerge = request.getBranchToMerge();
+ this.author = request.getAuthor();
+ this.messageTemplate = request.getMessageTemplate();
+ }
+
+ MergeResult doMergeInClone(MergeCommand mergeCommand) throws IOException {
+ MergeResult result;
+ try {
+ ObjectId sourceRevision = resolveRevision(toMerge);
+ mergeCommand
+ .setCommit(false) // we want to set the author manually
+ .include(toMerge, sourceRevision);
+
+ result = mergeCommand.call();
+ } catch (GitAPIException e) {
+ throw new InternalRepositoryException(getContext().getRepository(), "could not merge branch " + toMerge + " into " + target, e);
+ }
+ return result;
+ }
+
+ void doCommit() {
+ logger.debug("merged branch {} into {}", toMerge, target);
+ doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author);
+ }
+
+ private String determineMessageTemplate() {
+ if (Strings.isNullOrEmpty(messageTemplate)) {
+ return MERGE_COMMIT_MESSAGE_TEMPLATE;
+ } else {
+ return messageTemplate;
+ }
+ }
+
+ MergeCommandResult analyseFailure(MergeResult result) {
+ logger.info("could not merge branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
+ return MergeCommandResult.failure(result.getConflicts().keySet());
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java
new file mode 100644
index 0000000000..b688956404
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java
@@ -0,0 +1,31 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.MergeCommandResult;
+import org.eclipse.jgit.api.MergeCommand;
+
+import java.io.IOException;
+
+class GitMergeWithSquash extends GitMergeStrategy {
+
+ GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
+ super(clone, request, context, repository);
+ }
+
+ @Override
+ MergeCommandResult run() throws IOException {
+ MergeCommand mergeCommand = getClone().merge();
+ mergeCommand.setSquash(true);
+ MergeResult result = doMergeInClone(mergeCommand);
+
+ if (result.getMergeStatus().isSuccessful()) {
+ doCommit();
+ push();
+ return MergeCommandResult.success();
+ } else {
+ return analyseFailure(result);
+ }
+ }
+}
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 a68be0a4da..19234c32e3 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
@@ -46,7 +46,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
private final ModifyCommandRequest request;
ModifyWorker(Git clone, ModifyCommandRequest request) {
- super(clone);
+ super(clone, context, repository);
this.workDir = clone.getRepository().getWorkTree();
this.request = request;
}
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 5586a2f710..fcd721c3a2 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
@@ -15,10 +15,12 @@ import org.junit.Test;
import sonia.scm.NotFoundException;
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.user.User;
import java.io.IOException;
+import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -62,6 +64,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -88,6 +91,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("empty_merge");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -109,6 +113,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -132,6 +137,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMessageTemplate("simple");
@@ -152,6 +158,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -173,6 +180,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -192,6 +200,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setTargetBranch("mergeable");
request.setBranchToMerge("master");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -211,12 +220,112 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
}
+ @Test
+ public void shouldSquashCommitsIfSquashIsEnabled() throws IOException, GitAPIException {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+ request.setBranchToMerge("squash");
+ request.setTargetBranch("master");
+ request.setMessageTemplate("this is a squash");
+ request.setMergeStrategy(MergeStrategy.SQUASH);
+
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ Repository repository = createContext().open();
+ assertThat(mergeCommandResult.isSuccess()).isTrue();
+
+ Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
+ RevCommit mergeCommit = commits.iterator().next();
+ PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
+ String message = mergeCommit.getFullMessage();
+ assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
+ assertThat(message).isEqualTo("this is a squash");
+ }
+
+ @Test
+ public void shouldSquashThreeCommitsIntoOne() throws IOException, GitAPIException {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+ request.setBranchToMerge("squash");
+ request.setTargetBranch("master");
+ request.setMessageTemplate("squash three commits");
+ request.setMergeStrategy(MergeStrategy.SQUASH);
+ Repository gitRepository = createContext().open();
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ assertThat(mergeCommandResult.isSuccess()).isTrue();
+
+ Iterable commits = new Git(gitRepository).log().add(gitRepository.resolve("master")).setMaxCount(1).call();
+ RevCommit mergeCommit = commits.iterator().next();
+ PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
+ String message = mergeCommit.getFullMessage();
+ assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
+ assertThat(message).isEqualTo("squash three commits");
+
+ GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext(), repository);
+ List changes = modificationsCommand.getModifications("master").getAdded();
+ assertThat(changes.size()).isEqualTo(3);
+ }
+
+
+ @Test
+ public void shouldMergeWithFastForward() throws IOException, GitAPIException {
+ Repository repository = createContext().open();
+
+ ObjectId featureBranchHead = new Git(repository).log().add(repository.resolve("squash")).setMaxCount(1).call().iterator().next().getId();
+
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setBranchToMerge("squash");
+ request.setTargetBranch("master");
+ request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE);
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ assertThat(mergeCommandResult.isSuccess()).isTrue();
+
+ Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
+ RevCommit mergeCommit = commits.iterator().next();
+ assertThat(mergeCommit.getParentCount()).isEqualTo(1);
+ PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
+ assertThat(mergeAuthor.getName()).isEqualTo("Philip J Fry");
+ assertThat(mergeCommit.getId()).isEqualTo(featureBranchHead);
+ }
+
+ @Test
+ public void shouldDoMergeCommitIfFastForwardIsNotPossible() throws IOException, GitAPIException {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setTargetBranch("master");
+ request.setBranchToMerge("mergeable");
+ request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE);
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ assertThat(mergeCommandResult.isSuccess()).isTrue();
+
+ Repository repository = createContext().open();
+ Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
+ RevCommit mergeCommit = commits.iterator().next();
+ PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
+ assertThat(mergeCommit.getParentCount()).isEqualTo(2);
+ String message = mergeCommit.getFullMessage();
+ assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
+ assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
+ assertThat(message).contains("master", "mergeable");
+ }
+
@Test(expected = NotFoundException.class)
public void shouldHandleNotExistingSourceBranchInMerge() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("mergeable");
request.setBranchToMerge("not_existing");
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
command.merge(request);
}
@@ -225,6 +334,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
public void shouldHandleNotExistingTargetBranchInMerge() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
+ request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setTargetBranch("not_existing");
request.setBranchToMerge("master");
diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip
index 4310e63733..8e43da1d82 100644
Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java
deleted file mode 100644
index df59ba2abc..0000000000
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package sonia.scm.api.v2.resources;
-
-import com.webcohesion.enunciate.metadata.rs.ResponseCode;
-import com.webcohesion.enunciate.metadata.rs.StatusCodes;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.http.HttpStatus;
-import sonia.scm.ConcurrentModificationException;
-import sonia.scm.repository.NamespaceAndName;
-import sonia.scm.repository.RepositoryPermissions;
-import sonia.scm.repository.api.MergeCommandBuilder;
-import sonia.scm.repository.api.MergeCommandResult;
-import sonia.scm.repository.api.MergeDryRunCommandResult;
-import sonia.scm.repository.api.RepositoryService;
-import sonia.scm.repository.api.RepositoryServiceFactory;
-import sonia.scm.web.VndMediaType;
-
-import javax.inject.Inject;
-import javax.validation.Valid;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Response;
-
-@Slf4j
-public class MergeResource {
-
- private final RepositoryServiceFactory serviceFactory;
- private final MergeResultToDtoMapper mapper;
-
- @Inject
- public MergeResource(RepositoryServiceFactory serviceFactory, MergeResultToDtoMapper mapper) {
- this.serviceFactory = serviceFactory;
- this.mapper = mapper;
- }
-
- @POST
- @Path("")
- @Produces(VndMediaType.MERGE_RESULT)
- @Consumes(VndMediaType.MERGE_COMMAND)
- @StatusCodes({
- @ResponseCode(code = 204, condition = "merge has been executed successfully"),
- @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
- @ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to write the repository"),
- @ResponseCode(code = 409, condition = "The branches could not be merged automatically due to conflicts (conflicting files will be returned)"),
- @ResponseCode(code = 500, condition = "internal server error")
- })
- public Response merge(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
- NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
- log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
- try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
- RepositoryPermissions.push(repositoryService.getRepository()).check();
- MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge();
- if (mergeCommandResult.isSuccess()) {
- return Response.noContent().build();
- } else {
- return Response.status(HttpStatus.SC_CONFLICT).entity(mapper.map(mergeCommandResult)).build();
- }
- }
- }
-
- @POST
- @Path("dry-run/")
- @StatusCodes({
- @ResponseCode(code = 204, condition = "merge can be done automatically"),
- @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
- @ResponseCode(code = 409, condition = "The branches can not be merged automatically due to conflicts"),
- @ResponseCode(code = 500, condition = "internal server error")
- })
- public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
-
- NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
- log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
- try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
- if (RepositoryPermissions.push(repositoryService.getRepository()).isPermitted()) {
- MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun();
- if (mergeCommandResult.isMergeable()) {
- return Response.noContent().build();
- } else {
- throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision());
- }
- } else {
- return Response.noContent().build();
- }
- }
- }
-
- private MergeCommandBuilder createMergeCommand(MergeCommandDto mergeCommand, RepositoryService repositoryService) {
- return repositoryService
- .getMergeCommand()
- .setBranchToMerge(mergeCommand.getSourceRevision())
- .setTargetBranch(mergeCommand.getTargetRevision());
- }
-}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
index 1cfc8b332b..2294dc600e 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
@@ -42,7 +42,6 @@ public class RepositoryResource {
private final Provider diffRootResource;
private final Provider modificationsRootResource;
private final Provider fileHistoryRootResource;
- private final Provider mergeResource;
private final Provider incomingRootResource;
@Inject
@@ -57,8 +56,8 @@ public class RepositoryResource {
Provider diffRootResource,
Provider modificationsRootResource,
Provider fileHistoryRootResource,
- Provider incomingRootResource,
- Provider mergeResource) {
+ Provider incomingRootResource
+ ) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -72,8 +71,8 @@ public class RepositoryResource {
this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource;
- this.mergeResource = mergeResource;
this.incomingRootResource = incomingRootResource;
+
}
/**
@@ -208,9 +207,6 @@ public class RepositoryResource {
return incomingRootResource.get();
}
- @Path("merge/")
- public MergeResource merge() {return mergeResource.get(); }
-
private Supplier loadBy(String namespace, String name) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName)));
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java
index 67a6f26de5..c09f90c83d 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java
@@ -63,12 +63,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper fileHistoryRootResource;
protected Provider repositoryCollectionResource;
protected Provider incomingRootResource;
- protected Provider mergeResource;
RepositoryRootResource getRepositoryRootResource() {
@@ -39,8 +38,7 @@ public abstract class RepositoryTestBase {
diffRootResource,
modificationsRootResource,
fileHistoryRootResource,
- incomingRootResource,
- mergeResource)), repositoryCollectionResource);
+ incomingRootResource)), repositoryCollectionResource);
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
index 478b3efc92..672771aadd 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
@@ -45,7 +45,6 @@ public class ResourceLinksMock {
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
- when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo));
when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo));