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));