mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -131,6 +131,12 @@
|
||||
<artifactId>mapstruct-jdk8</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- rest documentation -->
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
@@ -208,6 +214,12 @@
|
||||
<artifactId>shiro-unit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>5.3.6.Final</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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<MergeStrategy> 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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
public enum MergeStrategy {
|
||||
MERGE_COMMIT,
|
||||
FAST_FORWARD_IF_POSSIBLE,
|
||||
SQUASH
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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<MergeStrategy> getSupportedMergeStrategies();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<R> {
|
||||
abstract static class GitCloneWorker<R> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<MergeStrategy> 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<MergeCommandResult> {
|
||||
|
||||
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<MergeStrategy> getSupportedMergeStrategies() {
|
||||
return STRATEGIES;
|
||||
}
|
||||
|
||||
private class ConflictWorker extends GitCloneWorker<MergeConflictResult> {
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MergeCommandResult> {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<RevCommit> 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<RevCommit> 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<String> 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<RevCommit> 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<RevCommit> 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");
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ public class RepositoryResource {
|
||||
private final Provider<DiffRootResource> diffRootResource;
|
||||
private final Provider<ModificationsRootResource> modificationsRootResource;
|
||||
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
private final Provider<MergeResource> mergeResource;
|
||||
private final Provider<IncomingRootResource> incomingRootResource;
|
||||
|
||||
@Inject
|
||||
@@ -57,8 +56,8 @@ public class RepositoryResource {
|
||||
Provider<DiffRootResource> diffRootResource,
|
||||
Provider<ModificationsRootResource> modificationsRootResource,
|
||||
Provider<FileHistoryRootResource> fileHistoryRootResource,
|
||||
Provider<IncomingRootResource> incomingRootResource,
|
||||
Provider<MergeResource> mergeResource) {
|
||||
Provider<IncomingRootResource> 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<Repository> loadBy(String namespace, String name) {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName)));
|
||||
|
||||
@@ -63,12 +63,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.single(link("incomingChangesets", resourceLinks.incoming().changesets(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("incomingDiff", resourceLinks.incoming().diff(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
if (repositoryService.isSupported(Command.MERGE)) {
|
||||
if (RepositoryPermissions.push(repository).isPermitted()) {
|
||||
linksBuilder.single(link("merge", resourceLinks.merge().merge(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
linksBuilder.single(link("mergeDryRun", resourceLinks.merge().dryRun(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
}
|
||||
linksBuilder.single(link("changesets", resourceLinks.changeset().all(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(repository.getNamespace(), repository.getName())));
|
||||
|
||||
@@ -792,26 +792,6 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
public MergeLinks merge() {
|
||||
return new MergeLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class MergeLinks {
|
||||
private final LinkBuilder mergeLinkBuilder;
|
||||
|
||||
MergeLinks(ScmPathInfo pathInfo) {
|
||||
this.mergeLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, MergeResource.class);
|
||||
}
|
||||
|
||||
String merge(String namespace, String name) {
|
||||
return mergeLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("merge").parameters().method("merge").parameters().href();
|
||||
}
|
||||
|
||||
String dryRun(String namespace, String name) {
|
||||
return mergeLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("merge").parameters().method("dryRun").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public PermissionsLinks permissions() {
|
||||
return new PermissionsLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
@@ -183,6 +183,10 @@
|
||||
"65RdZ5atX1": {
|
||||
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
||||
"description": "Einige Dateien für die Plugin-Deinstallation konnten nicht gelöscht werden. Dieses kann zu Inkonsistenzen führen, so dass der SCM-Manager nicht mehr korrekt starten kann. Bitte prüfen Sie die Logs und bereinigen Sie das Plugin-Verzeichnis des SCM-Managers manuell. Um die Installation eines Plugins abzubrechen, löschen Sie die zugehörige smp Datei aus dem Plugin-Verzeichnis. Um ein Entfernen eines Plugins zu verhindern, entfernen Sie die Datei namens 'uninstall' aus dem entsprechenden Verzeichnis des Plugins."
|
||||
},
|
||||
"6eRhF9gU41": {
|
||||
"displayName": "Nicht unterstützte Merge-Strategie",
|
||||
"description": "Die gewählte Merge-Strategie wird von dem Repository nicht unterstützt."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -183,6 +183,10 @@
|
||||
"65RdZ5atX1": {
|
||||
"displayName": "Error removing plugin files",
|
||||
"description": "Some files to cancel the plugin (un)installation could not be deleted. This can lead to inconsistencies so that the SCM-Manager cannot restart properly. Please check the logs and clean up the plugin folder manually. To cancel the installation of a plugin, remove the corresponding smp file. To cancel the uninstallation, remove the file named 'uninstall' inside the directory for this plugin."
|
||||
},
|
||||
"6eRhF9gU41": {
|
||||
"displayName": "Merge strategy not supported",
|
||||
"description": "The selected merge strategy is not supported by the repository."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
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.repository.spi.MergeCommand;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.RepositoryTestData.createHeartOfGold;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini",
|
||||
username = "trillian",
|
||||
password = "secret"
|
||||
)
|
||||
public class MergeResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String MERGE_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/merge/";
|
||||
private Repository repository = createHeartOfGold();
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
@Mock
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@Mock
|
||||
private RepositoryService repositoryService;
|
||||
@Mock
|
||||
private MergeCommand mergeCommand;
|
||||
@InjectMocks
|
||||
private MergeCommandBuilder mergeCommandBuilder;
|
||||
private MergeResultToDtoMapperImpl mapper = new MergeResultToDtoMapperImpl();
|
||||
|
||||
private MergeResource mergeResource;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
mergeResource = new MergeResource(serviceFactory, mapper);
|
||||
super.mergeResource = Providers.of(mergeResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleIllegalInput() throws Exception {
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand_invalid.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL + "dry-run/")
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
System.out.println(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExecutingMergeCommand {
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@BeforeEach
|
||||
void initRepository() {
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||
lenient().when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder);
|
||||
when(repositoryService.getRepository()).thenReturn(repository);
|
||||
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownShiro() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSuccessfulMerge() throws Exception {
|
||||
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.success());
|
||||
mockUser();
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL)
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFailedMerge() throws Exception {
|
||||
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.failure(asList("file1", "file2")));
|
||||
mockUser();
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL)
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
assertThat(response.getContentAsString()).contains("file1", "file2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSuccessfulDryRun() throws Exception {
|
||||
when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true);
|
||||
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(true));
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL + "dry-run/")
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFailedDryRun() throws Exception {
|
||||
when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true);
|
||||
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(false));
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL + "dry-run/")
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipDryRunIfSubjectHasNoPushPermission() throws Exception {
|
||||
when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(false);
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post(MERGE_URL + "dry-run/")
|
||||
.content(mergeCommandJson)
|
||||
.contentType(VndMediaType.MERGE_COMMAND);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
}
|
||||
|
||||
|
||||
private void mockUser() {
|
||||
PrincipalCollection collection = mock(PrincipalCollection.class);
|
||||
when(subject.getPrincipals()).thenReturn(collection);
|
||||
when(collection.oneByType(User.class)).thenReturn(new User("dummy"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ public abstract class RepositoryTestBase {
|
||||
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
|
||||
protected Provider<IncomingRootResource> incomingRootResource;
|
||||
protected Provider<MergeResource> mergeResource;
|
||||
|
||||
|
||||
RepositoryRootResource getRepositoryRootResource() {
|
||||
@@ -39,8 +38,7 @@ public abstract class RepositoryTestBase {
|
||||
diffRootResource,
|
||||
modificationsRootResource,
|
||||
fileHistoryRootResource,
|
||||
incomingRootResource,
|
||||
mergeResource)), repositoryCollectionResource);
|
||||
incomingRootResource)), repositoryCollectionResource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user