diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd71d1232..ed03e68e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Add support for pr merge with prior rebase ([#1332](https://github.com/scm-manager/scm-manager/pull/1332)) - Tags overview for repository [#1331](https://github.com/scm-manager/scm-manager/pull/1331) - Permissions can be specified for namespaces ([#1335](https://github.com/scm-manager/scm-manager/pull/1335)) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java index 98a9938346..a4083cef1d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java @@ -21,11 +21,22 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; public enum MergeStrategy { - MERGE_COMMIT, - FAST_FORWARD_IF_POSSIBLE, - SQUASH + MERGE_COMMIT(true), + FAST_FORWARD_IF_POSSIBLE(true), + SQUASH(true), + REBASE(false); + + private final boolean commitMessageAllowed; + + MergeStrategy(boolean commitMessageAllowed) { + this.commitMessageAllowed = commitMessageAllowed; + } + + public boolean isCommitMessageAllowed() { + return commitMessageAllowed; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index bd4e6b26b5..38b6b6dee5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -60,7 +60,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand private static final Set STRATEGIES = ImmutableSet.of( MergeStrategy.MERGE_COMMIT, MergeStrategy.FAST_FORWARD_IF_POSSIBLE, - MergeStrategy.SQUASH + MergeStrategy.SQUASH, + MergeStrategy.REBASE ); @Inject @@ -94,6 +95,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand case MERGE_COMMIT: return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); + case REBASE: + return inClone(clone -> new GitMergeRebase(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); + default: throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java new file mode 100644 index 0000000000..65cba4f7ba --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.RebaseResult; +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.Repository; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +public class GitMergeRebase extends GitMergeStrategy { + + private static final Logger logger = LoggerFactory.getLogger(GitMergeRebase.class); + + private final MergeCommandRequest request; + + GitMergeRebase(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + this.request = request; + } + + @Override + MergeCommandResult run() throws IOException { + RebaseResult result; + String branchToMerge = request.getBranchToMerge(); + String targetBranch = request.getTargetBranch(); + try { + checkOutBranch(branchToMerge); + result = + getClone() + .rebase() + .setUpstream(targetBranch) + .call(); + } catch (GitAPIException e) { + throw new InternalRepositoryException(getContext().getRepository(), "could not rebase branch " + branchToMerge + " onto " + targetBranch, e); + } + + if (result.getStatus().isSuccessful()) { + return fastForwardTargetBranch(branchToMerge, targetBranch, result); + } else { + logger.info("could not rebase branch {} into {} with rebase status '{}' due to ...", branchToMerge, targetBranch, result.getStatus()); + logger.info("... conflicts: {}", result.getConflicts()); + logger.info("... failing paths: {}", result.getFailingPaths()); + logger.info("... message: {}", result); + return MergeCommandResult.failure(branchToMerge, targetBranch, Optional.ofNullable(result.getConflicts()).orElse(Collections.singletonList("UNKNOWN"))); + } + } + + private MergeCommandResult fastForwardTargetBranch(String branchToMerge, String targetBranch, RebaseResult result) throws IOException { + try { + getClone().checkout().setName(targetBranch).call(); + ObjectId sourceRevision = resolveRevision(branchToMerge); + getClone() + .merge() + .setFastForward(MergeCommand.FastForwardMode.FF_ONLY) + .include(branchToMerge, sourceRevision) + .call(); + push(); + return MergeCommandResult.success(getTargetRevision().name(), branchToMerge, sourceRevision.name()); + } catch (GitAPIException e) { + return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts()); + } + + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 716b172981..84c17a299a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -473,6 +473,47 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { } + @Test + public void shouldAllowMergeWithRebase() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.REBASE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getParentCount()).isEqualTo(1); + assertThat(mergeCommit.getParent(0).name()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + assertThat(mergeCommit.getName()).isEqualTo(mergeCommandResult.getNewHeadRevision()); + assertThat(mergeCommit.getName()).doesNotStartWith("91b99de908fcd04772798a31c308a64aea1a5523"); + } + + @Test + public void shouldRejectRebaseMergeIfBranchCannotBeRebased() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("not-rebasable"); + request.setMergeStrategy(MergeStrategy.REBASE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isFalse(); + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit headCommit = commits.iterator().next(); + assertThat(headCommit.getName()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + + } + private GitMergeCommand createCommand() { return createCommand(git -> { }); diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 80483d04d1..4d23fa0284 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexDtoGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexDtoGeneratorTest.java index 97c9714513..94c78f798b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexDtoGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexDtoGeneratorTest.java @@ -132,6 +132,7 @@ class IndexDtoGeneratorTest { when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(scmPathInfo)); when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(scmPathInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo)); + when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo)); when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo))); } }