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

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

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