Merge with 2.0.0-m3

This commit is contained in:
Rene Pfeuffer
2019-11-11 13:18:07 +01:00
27 changed files with 455 additions and 424 deletions

View File

@@ -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>

View File

@@ -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.
*

View File

@@ -0,0 +1,7 @@
package sonia.scm.repository.api;
public enum MergeStrategy {
MERGE_COMMIT,
FAST_FORWARD_IF_POSSIBLE,
SQUASH
}

View File

@@ -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";
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

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

View File

@@ -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());
}
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();
public boolean isSupported(MergeStrategy strategy) {
return STRATEGIES.contains(strategy);
}
@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());
}
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();

View File

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

View File

@@ -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());
}
}

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -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());
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"));
}
}
}

View File

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

View File

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