diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d52e869d6..410d0f533e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +### Added +- Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) + ### Fixed - Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277)) diff --git a/docs/en/development/integration-tests.md b/docs/en/development/integration-tests.md new file mode 100644 index 0000000000..4b9589acfe --- /dev/null +++ b/docs/en/development/integration-tests.md @@ -0,0 +1,24 @@ +--- +title: Integration Tests +subtitle: How to run integration tests +displayToc: false +--- + +You can find the integration tests in the module **scm-it** (and a few still in **scm-webapp**). To run them, +simply start maven with the profile `it`: + +``` +mvn install -Pit -DskipUnitTests -pl :scm-webapp,:scm-it +``` + +This will start a jetty server and execute all integration tests in the maven phase `integration-test` using the +normal failsafe plugin. Integration tests are all classes ending with `ITCase` and are written using JUnit. + +To develop integration tests, you should start a local server with the **scm-integration-test-plugin**. This plugin is +used as a way to introspect server internals. For example you can register event listeners here and access their +triggers with a REST endpoint. Of course, this plugin is not and should not be installed in productive systems. +You can start the server with this plugin using the following maven call: + +``` +mvn run -pl :scm-integration-test-plugin +``` diff --git a/docs/en/navigation.yml b/docs/en/navigation.yml index 9c30b03156..3d5b6518a8 100644 --- a/docs/en/navigation.yml +++ b/docs/en/navigation.yml @@ -31,6 +31,7 @@ - /development/definition-of-done/ - /development/ui-dod/ - /development/decision-table/ + - /development/integration-tests/ - section: Plugin Development entries: diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java b/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java index ff49542222..588fc342d2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookContext.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; //~--- non-JDK imports -------------------------------------------------------- @@ -33,18 +33,18 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.spi.HookContextProvider; +import sonia.scm.repository.spi.HookMergeDetectionProvider; /** * The context for all repository hooks. With the {@link HookContext} class it * is able to send messages back to the client, retrieve {@link Changeset}s - * which are added during this push/commit and gives informations about changed + * which are added during this push/commit and gives informations about changed * branches and tags. * * @author Sebastian Sdorra * @since 1.33 */ -public final class HookContext -{ +public final class HookContext { /** * the logger for HookContext @@ -52,8 +52,6 @@ public final class HookContext private static final Logger logger = LoggerFactory.getLogger(HookContext.class); - //~--- constructors --------------------------------------------------------- - /** * Constructs ... * @@ -62,9 +60,7 @@ public final class HookContext * @param repository * @param preProcessorUtil */ - HookContext(HookContextProvider provider, Repository repository, - PreProcessorUtil preProcessorUtil) - { + HookContext(HookContextProvider provider, Repository repository, PreProcessorUtil preProcessorUtil) { this.provider = provider; this.repository = repository; this.preProcessorUtil = preProcessorUtil; @@ -77,41 +73,33 @@ public final class HookContext * about changed branches during the current hook. * * @return {@link HookBranchProvider} - * - * @throws HookFeatureIsNotSupportedException if the feature is not supported + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported * by the underlying provider - * + * * @since 1.45 */ - public HookBranchProvider getBranchProvider() - { - if (logger.isDebugEnabled()) - { - logger.debug("create branch provider for repository {}", - repository.getName()); - } + public HookBranchProvider getBranchProvider() { + logger.debug("create branch provider for repository {}", + repository.getName()); return provider.getBranchProvider(); } - + /** * Returns a {@link HookTagProvider} which is able to return informations * about changed tags during the current hook. * * @return {@link HookTagProvider} - * - * @throws HookFeatureIsNotSupportedException if the feature is not supported + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported * by the underlying provider - * + * * @since 1.50 */ - public HookTagProvider getTagProvider() - { - if (logger.isDebugEnabled()) - { - logger.debug("create tag provider for repository {}", - repository.getName()); - } + public HookTagProvider getTagProvider() { + logger.debug("create tag provider for repository {}", + repository.getName()); return provider.getTagProvider(); } @@ -122,25 +110,19 @@ public final class HookContext * * * @return {@link HookChangesetBuilder} - * - * @throws HookFeatureIsNotSupportedException if the feature is not supported + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported * by the underlying provider */ - public HookChangesetBuilder getChangesetProvider() - { - if (logger.isDebugEnabled()) - { - logger.debug("create changeset provider for repository {}", - repository.getName()); - } + public HookChangesetBuilder getChangesetProvider() { + logger.debug("create changeset provider for repository {}", + repository.getName()); - //J- return new HookChangesetBuilder( - repository, + repository, preProcessorUtil, provider.getChangesetProvider() ); - //J+ } /** @@ -152,21 +134,33 @@ public final class HookContext * * @return {@link HookMessageProvider} which is able to send message back to * the scm client - * - * @throws HookFeatureIsNotSupportedException if the feature is not supported + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported * by the underlying provider */ - public HookMessageProvider getMessageProvider() - { - if (logger.isDebugEnabled()) - { - logger.debug("create message provider for repository {}", - repository.getName()); - } + public HookMessageProvider getMessageProvider() { + logger.debug("create message provider for repository {}", + repository.getName()); return provider.getMessageProvider(); } + /** + * Returns a {@link HookMergeDetectionProvider} which is able to check whether two + * branches have been merged with the incoming changesets. + * + * @return {@link HookMergeDetectionProvider} which is able to detect merges. + * + * @throws HookFeatureIsNotSupportedException if the feature is not supported + * by the underlying provider + */ + public HookMergeDetectionProvider getMergeDetectionProvider() { + logger.debug("create merge detection provider for repository {}", + repository.getName()); + + return provider.getMergeDetectionProvider(); + } + /** * Returns true if the underlying provider support the requested feature. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java b/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java index 0f0aaf0832..da664b5772 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookFeature.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; /** @@ -49,11 +49,18 @@ public enum HookFeature * @since 1.45 */ BRANCH_PROVIDER, - + /** * Hook tag provider - * + * * @since 1.50 */ - TAG_PROVIDER; + TAG_PROVIDER, + + /** + * Provider to detect merges + * + * @since 2.4.0 + */ + MERGE_DETECTION_PROVIDER } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java index ddb8cb9a9b..f9af110ea5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookContextProvider.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -50,7 +50,7 @@ public abstract class HookContextProvider /** * Return the provider specific {@link HookMessageProvider} or throws a {@link HookFeatureIsNotSupportedException}. * The method will throw a {@link HookException} if the client is already disconnected. - * + * * @return provider specific {@link HookMessageProvider} */ public final HookMessageProvider getMessageProvider() @@ -86,31 +86,31 @@ public abstract class HookContextProvider /** * Return the provider specific {@link HookBranchProvider} or throws a {@link HookFeatureIsNotSupportedException}. - * + * * @return provider specific {@link HookBranchProvider} - * + * * @since 1.45 */ public HookBranchProvider getBranchProvider() { throw new HookFeatureIsNotSupportedException(HookFeature.BRANCH_PROVIDER); } - + /** * Return the provider specific {@link HookTagProvider} or throws a {@link HookFeatureIsNotSupportedException}. - * + * * @return provider specific {@link HookTagProvider} - * + * * @since 1.50 */ - public HookTagProvider getTagProvider() + public HookTagProvider getTagProvider() { throw new HookFeatureIsNotSupportedException(HookFeature.TAG_PROVIDER); } /** * Return the provider specific {@link HookChangesetProvider} or throws a {@link HookFeatureIsNotSupportedException}. - * + * * @return provider specific {@link HookChangesetProvider} */ public HookChangesetProvider getChangesetProvider() @@ -118,11 +118,21 @@ public abstract class HookContextProvider throw new HookFeatureIsNotSupportedException(HookFeature.CHANGESET_PROVIDER); } + /** + * Return the provider specific {@link HookMergeDetectionProvider} or throws a {@link HookFeatureIsNotSupportedException}. + * + * @return provider specific {@link HookMergeDetectionProvider} + */ + public HookMergeDetectionProvider getMergeDetectionProvider() + { + throw new HookFeatureIsNotSupportedException(HookFeature.MERGE_DETECTION_PROVIDER); + } + //~--- methods -------------------------------------------------------------- /** * Creates a new provider specific {@link HookMessageProvider} or throws a {@link HookFeatureIsNotSupportedException}. - * + * * @return provider specific {@link HookChangesetProvider} */ protected HookMessageProvider createMessageProvider() diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookMergeDetectionProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookMergeDetectionProvider.java new file mode 100644 index 0000000000..3deacd22c3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookMergeDetectionProvider.java @@ -0,0 +1,42 @@ +/* + * 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; + +/** + * @since 2.4.0 + */ +public interface HookMergeDetectionProvider { + + /** + * Checks whether branch has been merged into target. So this will also return + * true, when branch has been deleted with this change. + * + * @param target The name of the branch to check, whether the other branch has been merged into. + * @param branch The name of the branch to check, whether it has been merged into the other branch. + * @return true when branch has been merged into target, false + * otherwise. + */ + boolean branchesMerged(String target, String branch); +} diff --git a/scm-it/pom.xml b/scm-it/pom.xml index 90c22cda75..41ab2ab3d2 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -192,6 +192,7 @@ 2.10 + sonia.scm scm-webapp @@ -200,6 +201,15 @@ ${project.build.outputDirectory} scm-webapp.war + + + sonia.scm.plugins + scm-integration-test-plugin + ${project.version} + smp + ${scm.home}/plugins + scm-integration-test-plugin.smp + @@ -273,7 +283,7 @@ DEVELOPMENT - target/scm-it + ${project.parent.build.directory}/scm-it ${project.basedir}/../scm-webapp/src/main/resources/logback.default.xml diff --git a/scm-it/src/test/java/sonia/scm/it/MergeDetectionITCase.java b/scm-it/src/test/java/sonia/scm/it/MergeDetectionITCase.java new file mode 100644 index 0000000000..f0115e8c77 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/MergeDetectionITCase.java @@ -0,0 +1,185 @@ +/* + * 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.it; + +import io.restassured.RestAssured; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.Person; +import sonia.scm.repository.client.api.RepositoryClient; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; + +class MergeDetectionITCase { + + private static final Person ARTHUR = new Person("arthur", "arthur@hitchhiker.com"); + + RepositoryClient client; + String masterFile; + String developFile; + + @BeforeEach + void createRepository(@TempDir Path tempDir) throws IOException { + TestData.createDefault(); + + client = RepositoryUtil.createRepositoryClient("git", tempDir.toFile()); + + masterFile = createFile(tempDir, "hg2g.md"); + developFile = createFile(tempDir, "how_to_make_tea.md"); + + client.getAddCommand().add(masterFile); + client.getCommitCommand().commit(ARTHUR, "Add base file"); + client.getPushCommand().push(); + + client.getBranchCommand().branch("develop"); + client.getCheckoutCommand().checkout("develop"); + client.getAddCommand().add(developFile); + client.getCommitCommand().commit(ARTHUR, "add more important things"); + client.getPushCommand().push(); + } + + @AfterEach + void disableMergeDetection() { + RestAssured.given() + .auth().preemptive().basic(USER_SCM_ADMIN, USER_SCM_ADMIN) + .when() + .contentType("application/json") + .accept("application/json") + .body(toJson("{}")) + .post(RestUtil.createResourceUrl("integration-test/merge-detection/")) + .then() + .statusCode(204); + } + + @Test + void shouldDetectSimpleMergeAsMerged() throws IOException { + client.getCheckoutCommand().checkout("master"); + client.getMergeCommand().noFf().merge("develop"); + + initializeMergeDetection("master", "develop"); + + client.getPushCommand().push(); + + Assertions.assertThat(getMergeDetectionResult("preMergeDetection", 0)).isTrue(); + Assertions.assertThat(getMergeDetectionResult("postMergeDetection", 0)).isTrue(); + } + + @Test + void shouldDetectFastForwardAsMerged() throws IOException { + client.getCheckoutCommand().checkout("master"); + client.getMergeCommand().merge("develop"); + + initializeMergeDetection("master", "develop"); + + client.getPushCommand().push(); + + Assertions.assertThat(getMergeDetectionResult("preMergeDetection", 0)).isTrue(); + Assertions.assertThat(getMergeDetectionResult("postMergeDetection", 0)).isTrue(); + } + + @Test + void shouldDetectMergeWhenBranchHasBeenDeletedAsMerged() throws IOException { + client.getCheckoutCommand().checkout("master"); + client.getMergeCommand().merge("develop"); + client.getPushCommand().push(); + + initializeMergeDetection("master", "develop"); + + client.getDeleteRemoteBranchCommand().delete("develop"); + client.getPushCommand().push(); + + Assertions.assertThat(getMergeDetectionResult("preMergeDetection", 0)).isTrue(); + Assertions.assertThat(getMergeDetectionResult("postMergeDetection", 0)).isTrue(); + } + + @Test + void shouldDetectNormalPushAsNotMerged(@TempDir Path tempDir) throws IOException { + client.getCheckoutCommand().checkout("develop"); + writeFile(tempDir, developFile, "other content"); + client.getAddCommand().add(developFile); + client.getCommitCommand().commit(ARTHUR, "simple commit"); + + initializeMergeDetection("master", "develop"); + + client.getPushCommand().push(); + + Assertions.assertThat(getMergeDetectionResult("preMergeDetection", 0)).isFalse(); + Assertions.assertThat(getMergeDetectionResult("postMergeDetection", 0)).isFalse(); + } + + private boolean getMergeDetectionResult(String type, int n) { + return RestAssured.given() + .auth().preemptive().basic(USER_SCM_ADMIN, USER_SCM_ADMIN) + .when() + .accept("application/json") + .get(RestUtil.createResourceUrl("integration-test/")) + .then() + .statusCode(200) + .extract() + .jsonPath() + .getBoolean("_embedded." + + type + + "[" + n + "].merged"); + } + + private void initializeMergeDetection(String target, String branch) { + RestAssured.given() + .auth().preemptive().basic(USER_SCM_ADMIN, USER_SCM_ADMIN) + .when() + .contentType("application/json") + .accept("application/json") + .body(toJson(format("{'target': '%s', 'branch': '%s'}", target, branch))) + .post(RestUtil.createResourceUrl("integration-test/merge-detection/")) + .then() + .statusCode(204); + } + + private String createFile(Path tempDir, String name) throws IOException { + Files.createFile(tempDir.resolve(name)); + writeFile(tempDir, name, "Some content"); + return name; + } + + private void writeFile(Path tempDir, String name, String content) throws IOException { + Path file = tempDir.resolve(name); + Files.write(file, singletonList(content)); + } + + private String toJson(String json) { + return json.replaceAll("'", "\""); + } +} diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index a407c38471..f4f927fe0e 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -45,6 +45,7 @@ scm-git-plugin scm-svn-plugin scm-legacy-plugin + scm-integration-test-plugin diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index dee72ce345..b55aec06bd 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -337,6 +337,16 @@ public final class GitUtil return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS); } + public static Ref getBranchIdOrCurrentHead(org.eclipse.jgit.lib.Repository gitRepository, String requestedBranch) throws IOException { + if ( Strings.isNullOrEmpty(requestedBranch) ) { + logger.trace("no default branch configured, use repository head as default"); + Optional repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository); + return repositoryHeadRef.orElse(null); + } else { + return GitUtil.getBranchId(gitRepository, requestedBranch); + } + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java new file mode 100644 index 0000000000..03a5b59837 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java @@ -0,0 +1,81 @@ +/* + * 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.api; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceiveCommand; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.spi.GitLogComputer; +import sonia.scm.repository.spi.HookMergeDetectionProvider; +import sonia.scm.repository.spi.LogCommandRequest; + +import java.util.List; + +public class GitReceiveHookMergeDetectionProvider implements HookMergeDetectionProvider { + private final Repository repository; + private final String repositoryId; + private final List receiveCommands; + + public GitReceiveHookMergeDetectionProvider(Repository repository, String repositoryId, List receiveCommands) { + this.repository = repository; + this.repositoryId = repositoryId; + this.receiveCommands = receiveCommands; + } + + @Override + public boolean branchesMerged(String target, String branch) { + LogCommandRequest request = new LogCommandRequest(); + request.setBranch(findRelevantRevisionForBranchIfToBeUpdated(branch)); + request.setAncestorChangeset(findRelevantRevisionForBranchIfToBeUpdated(target)); + request.setPagingLimit(1); + + return new GitLogComputer(repositoryId, repository).compute(request).getTotal() == 0; + } + + private String findRelevantRevisionForBranchIfToBeUpdated(String branch) { + return receiveCommands + .stream() + .filter(receiveCommand -> isReceiveCommandForBranch(branch, receiveCommand)) + .map(this::getRelevantRevision) + .map(AnyObjectId::getName) + .findFirst() + .orElse(branch); + } + + private boolean isReceiveCommandForBranch(String branch, ReceiveCommand receiveCommand) { + return receiveCommand.getType() != ReceiveCommand.Type.CREATE + && GitUtil.getBranch(receiveCommand.getRef()).equals(branch); + } + + private ObjectId getRelevantRevision(ReceiveCommand receiveCommand) { + if (receiveCommand.getType() == ReceiveCommand.Type.DELETE) { + return receiveCommand.getOldId(); + } else { + return receiveCommand.getNewId(); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index d52d0a1574..9e13e82971 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -41,7 +41,6 @@ import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; @@ -59,6 +58,7 @@ import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; +import static sonia.scm.repository.GitUtil.getBranchIdOrCurrentHead; //~--- JDK imports ------------------------------------------------------------ @@ -123,15 +123,9 @@ class AbstractGitCommand Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { if ( Strings.isNullOrEmpty(requestedBranch) ) { String defaultBranchName = context.getConfig().getDefaultBranch(); - if (!Strings.isNullOrEmpty(defaultBranchName)) { - return GitUtil.getBranchId(gitRepository, defaultBranchName); - } else { - logger.trace("no default branch configured, use repository head as default"); - Optional repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository); - return repositoryHeadRef.orElse(null); - } + return getBranchIdOrCurrentHead(gitRepository, defaultBranchName); } else { - return GitUtil.getBranchId(gitRepository, requestedBranch); + return getBranchIdOrCurrentHead(gitRepository, requestedBranch); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java index 8e642a18ce..bc8d633add 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java @@ -21,27 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; - import sonia.scm.repository.api.GitHookBranchProvider; import sonia.scm.repository.api.GitHookMessageProvider; +import sonia.scm.repository.api.GitHookTagProvider; +import sonia.scm.repository.api.GitReceiveHookMergeDetectionProvider; import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookFeature; import sonia.scm.repository.api.HookMessageProvider; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.api.HookTagProvider; import java.util.EnumSet; import java.util.List; import java.util.Set; -import sonia.scm.repository.api.GitHookTagProvider; -import sonia.scm.repository.api.HookTagProvider; /** * @@ -50,24 +49,34 @@ import sonia.scm.repository.api.HookTagProvider; public class GitHookContextProvider extends HookContextProvider { - /** Field description */ - private static final Set SUPPORTED_FEATURES = - EnumSet.of(HookFeature.MESSAGE_PROVIDER, HookFeature.CHANGESET_PROVIDER, - HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); + /** + * Field description + */ + private static final Set SUPPORTED_FEATURES = EnumSet.of( + HookFeature.MESSAGE_PROVIDER, + HookFeature.CHANGESET_PROVIDER, + HookFeature.BRANCH_PROVIDER, + HookFeature.TAG_PROVIDER, + HookFeature.MERGE_DETECTION_PROVIDER + ); //~--- constructors --------------------------------------------------------- /** * Constructs a new instance - * * @param receivePack git receive pack * @param receiveCommands received commands */ - public GitHookContextProvider(ReceivePack receivePack, - List receiveCommands) - { + public GitHookContextProvider( + ReceivePack receivePack, + List receiveCommands, + Repository repository, + String repositoryId + ) { this.receivePack = receivePack; this.receiveCommands = receiveCommands; + this.repository = repository; + this.repositoryId = repositoryId; this.changesetProvider = new GitHookChangesetProvider(receivePack, receiveCommands); } @@ -99,20 +108,20 @@ public class GitHookContextProvider extends HookContextProvider return changesetProvider; } + @Override + public HookMergeDetectionProvider getMergeDetectionProvider() { + return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands); + } + @Override public Set getSupportedFeatures() { return SUPPORTED_FEATURES; } - //~--- fields --------------------------------------------------------------- - - /** Field description */ private final GitHookChangesetProvider changesetProvider; - - /** Field description */ private final List receiveCommands; - - /** Field description */ private final ReceivePack receivePack; + private final Repository repository; + private final String repositoryId; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index da00ba451b..340df65c16 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -27,19 +27,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.filter.AndTreeFilter; -import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.NotFoundException; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; @@ -48,9 +41,6 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.util.IOUtil; import java.io.IOException; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -183,123 +173,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand @Override @SuppressWarnings("unchecked") public ChangesetPagingResult getChangesets(LogCommandRequest request) { - if (logger.isDebugEnabled()) { - logger.debug("fetch changesets for request: {}", request); - } - - ChangesetPagingResult changesets = null; - GitChangesetConverter converter = null; - RevWalk revWalk = null; - - try (org.eclipse.jgit.lib.Repository repository = open()) { - if (!repository.getAllRefs().isEmpty()) { - int counter = 0; - int start = request.getPagingStart(); - - if (start < 0) { - if (logger.isErrorEnabled()) { - logger.error("start parameter is negative, reset to 0"); - } - - start = 0; - } - - List changesetList = Lists.newArrayList(); - int limit = request.getPagingLimit(); - ObjectId startId = null; - - if (!Strings.isNullOrEmpty(request.getStartChangeset())) { - startId = repository.resolve(request.getStartChangeset()); - } - - ObjectId endId = null; - - if (!Strings.isNullOrEmpty(request.getEndChangeset())) { - endId = repository.resolve(request.getEndChangeset()); - } - - Ref branch = getBranchOrDefault(repository,request.getBranch()); - - ObjectId ancestorId = null; - - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { - ancestorId = repository.resolve(request.getAncestorChangeset()); - if (ancestorId == null) { - throw notFound(entity(REVISION, request.getAncestorChangeset()).in(this.repository)); - } - } - - revWalk = new RevWalk(repository); - - converter = new GitChangesetConverter(repository, revWalk); - - if (!Strings.isNullOrEmpty(request.getPath())) { - revWalk.setTreeFilter( - AndTreeFilter.create( - PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); - } - - if (branch != null) { - if (startId != null) { - revWalk.markStart(revWalk.lookupCommit(startId)); - } else { - revWalk.markStart(revWalk.lookupCommit(branch.getObjectId())); - } - - if (ancestorId != null) { - revWalk.markUninteresting(revWalk.lookupCommit(ancestorId)); - } - - Iterator iterator = revWalk.iterator(); - - while (iterator.hasNext()) { - RevCommit commit = iterator.next(); - - if ((counter >= start) - && ((limit < 0) || (counter < start + limit))) { - changesetList.add(converter.createChangeset(commit)); - } - - counter++; - - if (commit.getId().equals(endId)) { - break; - } - } - } else if (ancestorId != null) { - throw notFound(entity(REVISION, request.getBranch()).in(this.repository)); - } - - if (branch != null) { - changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName())); - } else { - changesets = new ChangesetPagingResult(counter, changesetList); - } - } else if (logger.isWarnEnabled()) { - logger.warn("the repository {} seems to be empty", - this.repository.getName()); - - changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST); + try (org.eclipse.jgit.lib.Repository gitRepository = open()) { + if (Strings.isNullOrEmpty(request.getBranch())) { + request.setBranch(context.getConfig().getDefaultBranch()); } + return new GitLogComputer(this.repository.getId(), gitRepository).compute(request); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not create change log", e); } - catch (MissingObjectException e) - { - throw notFound(entity(REVISION, e.getObjectId().getName()).in(repository)); - } - catch (NotFoundException e) - { - throw e; - } - catch (Exception ex) - { - throw new InternalRepositoryException(repository, "could not create change log", ex); - } - finally - { - IOUtil.close(converter); - GitUtil.release(revWalk); - } - - return changesets; } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java new file mode 100644 index 0000000000..eae8ea5851 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java @@ -0,0 +1,186 @@ +/* + * 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 com.google.common.base.Strings; +import com.google.common.collect.Lists; +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.filter.AndTreeFilter; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.util.IOUtil; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +public class GitLogComputer { + + private static final Logger LOG = LoggerFactory.getLogger(GitLogComputer.class); + + private final String repositoryId; + private final Repository gitRepository; + + public GitLogComputer(String repositoryId, Repository repository) { + this.repositoryId = repositoryId; + this.gitRepository = repository; + } + + public ChangesetPagingResult compute(LogCommandRequest request) { + LOG.debug("fetch changesets for request: {}", request); + + GitChangesetConverter converter = null; + RevWalk revWalk = null; + + try { + if (!gitRepository.getAllRefs().isEmpty()) { + int counter = 0; + int start = request.getPagingStart(); + + if (start < 0) { + LOG.error("start parameter is negative, reset to 0"); + + start = 0; + } + + List changesetList = Lists.newArrayList(); + int limit = request.getPagingLimit(); + ObjectId startId = null; + + if (!Strings.isNullOrEmpty(request.getStartChangeset())) { + startId = gitRepository.resolve(request.getStartChangeset()); + } + + ObjectId endId = null; + + if (!Strings.isNullOrEmpty(request.getEndChangeset())) { + endId = gitRepository.resolve(request.getEndChangeset()); + } + + Ref branch = GitUtil.getBranchIdOrCurrentHead(gitRepository, request.getBranch()); + ObjectId branchId; + if (branch == null) { + if (request.getBranch() != null) { + try { + branchId = ObjectId.fromString(request.getBranch()); + } catch (InvalidObjectIdException e) { + throw notFound(entity(GitLogCommand.REVISION, request.getBranch()).in(sonia.scm.repository.Repository.class, repositoryId)); + } + } else { + branchId = null; + } + } else { + branchId = branch.getObjectId(); + } + + ObjectId ancestorId = null; + + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { + ancestorId = gitRepository.resolve(request.getAncestorChangeset()); + if (ancestorId == null) { + throw notFound(entity(GitLogCommand.REVISION, request.getAncestorChangeset()).in(sonia.scm.repository.Repository.class, repositoryId)); + } + } + + revWalk = new RevWalk(gitRepository); + + converter = new GitChangesetConverter(gitRepository, revWalk); + + if (!Strings.isNullOrEmpty(request.getPath())) { + revWalk.setTreeFilter( + AndTreeFilter.create( + PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); + } + + if (branchId != null) { + if (startId != null) { + revWalk.markStart(revWalk.lookupCommit(startId)); + } else { + revWalk.markStart(revWalk.lookupCommit(branchId)); + } + + if (ancestorId != null) { + revWalk.markUninteresting(revWalk.lookupCommit(ancestorId)); + } + + Iterator iterator = revWalk.iterator(); + + while (iterator.hasNext()) { + RevCommit commit = iterator.next(); + + if ((counter >= start) + && ((limit < 0) || (counter < start + limit))) { + changesetList.add(converter.createChangeset(commit)); + } + + counter++; + + if (commit.getId().equals(endId)) { + break; + } + } + } else if (ancestorId != null) { + throw notFound(entity(GitLogCommand.REVISION, request.getBranch()).in(sonia.scm.repository.Repository.class, repositoryId)); + } + + if (branch != null) { + return new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName())); + } else { + return new ChangesetPagingResult(counter, changesetList); + } + } else { + LOG.debug("the repository with id {} seems to be empty", this.repositoryId); + + return new ChangesetPagingResult(0, Collections.emptyList()); + } + } catch (MissingObjectException e) { + throw notFound(entity(GitLogCommand.REVISION, e.getObjectId().getName()).in(sonia.scm.repository.Repository.class, repositoryId)); + } catch (NotFoundException e) { + throw e; + } catch (Exception ex) { + throw new InternalRepositoryException(entity(sonia.scm.repository.Repository.class, repositoryId).build(), "could not create change log", ex); + } finally { + IOUtil.close(converter); + GitUtil.release(revWalk); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java index c7b323224c..a9d74e0357 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- @@ -122,8 +122,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook logger.trace("resolved repository to {}", repositoryId); - GitHookContextProvider context = new GitHookContextProvider(rpack, - receiveCommands); + GitHookContextProvider context = new GitHookContextProvider(rpack, receiveCommands, repository, repositoryId); hookEventFacade.handle(repositoryId).fireHookEvent(type, context); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCheckoutCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCheckoutCommand.java new file mode 100644 index 0000000000..1db0140041 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCheckoutCommand.java @@ -0,0 +1,49 @@ +/* + * 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.client.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import sonia.scm.repository.client.api.RepositoryClientException; + +import java.io.IOException; + +public class GitCheckoutCommand implements CheckoutCommand { + + private Git git; + + GitCheckoutCommand(Git git) { + this.git = git; + } + + @Override + public void checkout(String name) throws IOException { + try { + git.checkout().setName(name).call(); + } catch (GitAPIException ex) { + throw new RepositoryClientException("could not checkout branch or revision", ex); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitDeleteRemoteBranchCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitDeleteRemoteBranchCommand.java new file mode 100644 index 0000000000..69dc53a958 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitDeleteRemoteBranchCommand.java @@ -0,0 +1,62 @@ +/* + * 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.client.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.RefSpec; +import sonia.scm.repository.client.api.RepositoryClientException; + +import java.io.IOException; + +public class GitDeleteRemoteBranchCommand implements DeleteRemoteBranchCommand { + + private final Git git; + private final CredentialsProvider credentialsProvider; + + GitDeleteRemoteBranchCommand(Git git, CredentialsProvider credentialsProvider) { + this.git = git; + this.credentialsProvider = credentialsProvider; + } + + @Override + public void delete(String name) throws IOException { + try { + git.branchDelete().setBranchNames("refs/heads/" + name).call(); + RefSpec refSpec = new RefSpec() + .setSource(null) + .setDestination("refs/heads/" + name); + PushCommand push = git.push(); + if (credentialsProvider != null) { + push.setCredentialsProvider(credentialsProvider); + } + push.setRefSpecs(refSpec).call(); + } catch (GitAPIException ex) { + throw new RepositoryClientException("could not delete branch", ex); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java new file mode 100644 index 0000000000..464d32eba6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java @@ -0,0 +1,80 @@ +/* + * 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.client.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.client.api.RepositoryClientException; + +import java.io.IOException; + +public class GitMergeCommand implements MergeCommand { + + private final Git git; + + GitMergeCommand(Git git) { + this.git = git; + } + + @Override + public Changeset merge(MergeRequest request) throws IOException { + try (GitChangesetConverter converter = new GitChangesetConverter(git.getRepository())) { + ObjectId resolved = git.getRepository().resolve(request.getBranch()); + org.eclipse.jgit.api.MergeCommand mergeCommand = git.merge() + .include(request.getBranch(), resolved) + .setMessage(request.getMessage()); + + switch (request.getFfMode()) { + case FF: + mergeCommand.setFastForward(org.eclipse.jgit.api.MergeCommand.FastForwardMode.FF); + break; + case NO_FF: + mergeCommand.setFastForward(org.eclipse.jgit.api.MergeCommand.FastForwardMode.NO_FF); + break; + case FF_ONLY: + mergeCommand.setFastForward(org.eclipse.jgit.api.MergeCommand.FastForwardMode.FF_ONLY); + break; + default: + throw new IllegalStateException("Unknown FF mode: " + request.getFfMode()); + } + + MergeResult mergeResult = mergeCommand + .call(); + + try (RevWalk revWalk = new RevWalk(git.getRepository())) { + RevCommit commit = revWalk.parseCommit(mergeResult.getNewHead()); + return converter.createChangeset(commit); + } + } catch (GitAPIException ex) { + throw new RepositoryClientException("could not commit changes to repository", ex); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java index 665e4bc8eb..1cb11dbe2c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -49,7 +49,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider private static final Set SUPPORTED_COMMANDS = ImmutableSet.of(ClientCommand.ADD, ClientCommand.REMOVE, ClientCommand.COMMIT, ClientCommand.TAG, ClientCommand.BRANCH, - ClientCommand.PUSH); + ClientCommand.DELETE_REMOTE_BRANCH, ClientCommand.MERGE, ClientCommand.PUSH); //~--- constructors --------------------------------------------------------- @@ -118,6 +118,16 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider return new GitBranchCommand(git); } + @Override + public DeleteRemoteBranchCommand getDeleteRemoteBranchCommand() { + return new GitDeleteRemoteBranchCommand(git, credentialsProvider); + } + + @Override + public CheckoutCommand getCheckoutCommand() { + return new GitCheckoutCommand(git); + } + /** * Method description * @@ -178,6 +188,11 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider return new GitTagCommand(git); } + @Override + public MergeCommand getMergeCommand() { + return new GitMergeCommand(git); + } + @Override public File getWorkingCopy() { return git.getRepository().getWorkTree(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java index fb4cda8ca0..3e3f87131e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_DequoteOutputStreamTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import org.assertj.core.api.Assertions; @@ -48,7 +48,7 @@ public class GitDiffCommand_DequoteOutputStreamTest { stream.write(bytes, 0, bytes.length); stream.flush(); - Assertions.assertThat(buffer.toString()).isEqualTo("diff --git a/file úüþëéåëåé a b/file úüþëéåëåé b\n" + + Assertions.assertThat(buffer.toString("UTF-8")).isEqualTo("diff --git a/file úüþëéåëåé a b/file úüþëéåëåé b\n" + "new file mode 100644\n" + "index 0000000..8cb0607\n" + "--- /dev/null\n" + diff --git a/scm-plugins/scm-integration-test-plugin/pom.xml b/scm-plugins/scm-integration-test-plugin/pom.xml new file mode 100644 index 0000000000..2c69c5fbb2 --- /dev/null +++ b/scm-plugins/scm-integration-test-plugin/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + sonia.scm.plugins + scm-plugins + 2.4.0-SNAPSHOT + + + scm-integration-test-plugin + Add functions for integration tests. This is not intended for production systems. + 2.4.0-SNAPSHOT + smp + + + + + javax.servlet + javax.servlet-api + ${servlet.version} + provided + + + + diff --git a/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/IntegrationTestResource.java b/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/IntegrationTestResource.java new file mode 100644 index 0000000000..f6b606e001 --- /dev/null +++ b/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/IntegrationTestResource.java @@ -0,0 +1,106 @@ +/* + * 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.it.resource; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; +import sonia.scm.api.v2.resources.LinkBuilder; +import sonia.scm.api.v2.resources.ScmPathInfoStore; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Links.linkingTo; + +/** + * Web Service Resource to support integration tests. + */ +@Path(IntegrationTestResource.INTEGRATION_TEST_PATH_V2) +public class IntegrationTestResource { + + static final String INTEGRATION_TEST_PATH_V2 = "v2/integration-test"; + + private final ScmPathInfoStore scmPathInfoStore; + private final MergeDetectionHelper mergeDetectionHelper; + + @Inject + public IntegrationTestResource(ScmPathInfoStore scmPathInfoStore, MergeDetectionHelper mergeDetectionHelper) { + this.scmPathInfoStore = scmPathInfoStore; + this.mergeDetectionHelper = mergeDetectionHelper; + } + + @GET + @Path("") + @Produces("application/json") + public CollectionDto get() { + Links links = linkingTo() + .self(self()) + .build(); + Embedded embedded = embeddedBuilder() + .with("preMergeDetection", mergeDetectionHelper.getPreMergeDetections()) + .with("postMergeDetection", mergeDetectionHelper.getPostMergeDetections()) + .build(); + return new CollectionDto(links, embedded); + } + + @POST + @Path("merge-detection") + @Consumes("application/json") + public void initMergeDetection(MergeDetectionConfigurationDto mergeDetectionConfiguration) { + mergeDetectionHelper.initialize(mergeDetectionConfiguration.getTarget(), mergeDetectionConfiguration.getBranch()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), IntegrationTestResource.class); + return linkBuilder.method("get").parameters().href(); + } + + static class CollectionDto extends HalRepresentation { + + CollectionDto(Links links, Embedded embedded) { + super(links, embedded); + } + + @Override + protected HalRepresentation withEmbedded(String rel, HalRepresentation embeddedItem) { + return super.withEmbedded(rel, embeddedItem); + } + } + + @Getter + @Setter + static class MergeDetectionConfigurationDto { + private String target; + private String branch; + } +} diff --git a/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/MergeDetectionHelper.java b/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/MergeDetectionHelper.java new file mode 100644 index 0000000000..10d9fbc31a --- /dev/null +++ b/scm-plugins/scm-integration-test-plugin/src/main/java/sonia/scm/it/resource/MergeDetectionHelper.java @@ -0,0 +1,101 @@ +/* + * 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.it.resource; + +import com.github.legman.Subscribe; +import de.otto.edison.hal.HalRepresentation; +import lombok.AllArgsConstructor; +import lombok.Getter; +import sonia.scm.EagerSingleton; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.spi.HookMergeDetectionProvider; + +import java.util.ArrayList; +import java.util.List; + +@EagerSingleton +@Extension +public class MergeDetectionHelper { + + private final List preMergeDetections = new ArrayList<>(); + private final List postMergeDetections = new ArrayList<>(); + + private String target; + private String branch; + + @Subscribe + public void handlePreReceiveEvent(PreReceiveRepositoryHookEvent event) { + if (target == null || branch == null) { + return; + } + preMergeDetections.add(createDto(event)); + } + + @Subscribe + public void handlePostReceiveEvent(PostReceiveRepositoryHookEvent event) { + if (target == null || branch == null) { + return; + } + postMergeDetections.add(createDto(event)); + } + + public ResultDto createDto(RepositoryHookEvent event) { + HookMergeDetectionProvider mergeDetectionProvider = event.getContext().getMergeDetectionProvider(); + boolean merged = mergeDetectionProvider.branchesMerged(target, branch); + return new ResultDto( + event.getClass().getSimpleName(), + event.getRepository().getNamespace(), + event.getRepository().getName(), + merged + ); + } + + void initialize(String target, String branch) { + this.target = target; + this.branch = branch; + preMergeDetections.clear(); + postMergeDetections.clear(); + } + + public List getPreMergeDetections() { + return preMergeDetections; + } + + public List getPostMergeDetections() { + return postMergeDetections; + } + + @Getter + @AllArgsConstructor + static class ResultDto extends HalRepresentation { + private String type; + private String namespace; + private String name; + private boolean merged; + } +} diff --git a/scm-plugins/scm-integration-test-plugin/src/main/resources/META-INF/scm/plugin.xml b/scm-plugins/scm-integration-test-plugin/src/main/resources/META-INF/scm/plugin.xml new file mode 100644 index 0000000000..f2f70ba9ff --- /dev/null +++ b/scm-plugins/scm-integration-test-plugin/src/main/resources/META-INF/scm/plugin.xml @@ -0,0 +1,42 @@ + + + + + + 2 + + + Integration Test Support + Cloudogu GmbH + Test + + + + ${project.parent.version} + + + diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/CheckoutCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/CheckoutCommandBuilder.java new file mode 100644 index 0000000000..9fab6f40c9 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/CheckoutCommandBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.client.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.client.spi.CheckoutCommand; + +import java.io.IOException; + +/** + * @since 2.4.0 + */ +public final class CheckoutCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(CheckoutCommandBuilder.class); + + private final CheckoutCommand command; + + public CheckoutCommandBuilder(CheckoutCommand command) { + this.command = command; + } + + public CheckoutCommandBuilder checkout(String name) throws IOException { + LOG.debug("checkout {}", name); + + command.checkout(name); + + return this; + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java index bc022a284e..37e1cce24a 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/ClientCommand.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.api; /** @@ -31,5 +31,5 @@ package sonia.scm.repository.client.api; */ public enum ClientCommand { - ADD, REMOVE, COMMIT, PUSH, TAG, BRANCH + ADD, REMOVE, COMMIT, PUSH, TAG, BRANCH, DELETE_REMOTE_BRANCH, CHECKOUT, MERGE } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/DeleteRemoteBranchCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/DeleteRemoteBranchCommandBuilder.java new file mode 100644 index 0000000000..cc20321b15 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/DeleteRemoteBranchCommandBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.client.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.client.spi.DeleteRemoteBranchCommand; + +import java.io.IOException; + +/** + * @since 2.4.0 + */ +public final class DeleteRemoteBranchCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(DeleteRemoteBranchCommandBuilder.class); + + private DeleteRemoteBranchCommand command; + + public DeleteRemoteBranchCommandBuilder(DeleteRemoteBranchCommand command) { + this.command = command; + } + + public DeleteRemoteBranchCommandBuilder delete(String name) throws IOException { + LOG.debug("delete branch {}", name); + + command.delete(name); + + return this; + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/MergeCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/MergeCommandBuilder.java new file mode 100644 index 0000000000..ade806ade0 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/MergeCommandBuilder.java @@ -0,0 +1,63 @@ +/* + * 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.client.api; + +import sonia.scm.repository.client.spi.MergeCommand; +import sonia.scm.repository.client.spi.MergeRequest; + +import java.io.IOException; + +/** + * @since 2.4.0 + */ +public final class MergeCommandBuilder { + + private final MergeCommand command; + private final MergeRequest request = new MergeRequest(); + + MergeCommandBuilder(MergeCommand command) { + this.command = command; + } + + public MergeCommandBuilder ffOnly() { + request.setFfMode(MergeRequest.FastForwardMode.FF_ONLY); + return this; + } + + public MergeCommandBuilder noFf() { + request.setFfMode(MergeRequest.FastForwardMode.NO_FF); + return this; + } + + public MergeCommandBuilder ffIfPossible() { + request.setFfMode(MergeRequest.FastForwardMode.FF); + return this; + } + + public void merge(String branch) throws IOException { + request.setBranch(branch); + command.merge(request); + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java index 2dbb01d667..660cdb160b 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClient.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.api; import org.slf4j.Logger; @@ -60,6 +60,18 @@ public final class RepositoryClient implements Closeable { return new BranchCommandBuilder(clientProvider.getBranchCommand()); } + public DeleteRemoteBranchCommandBuilder getDeleteRemoteBranchCommand() { + logger.trace("delete branch command"); + + return new DeleteRemoteBranchCommandBuilder(clientProvider.getDeleteRemoteBranchCommand()); + } + + public CheckoutCommandBuilder getCheckoutCommand() { + logger.trace("create checkout command"); + + return new CheckoutCommandBuilder(clientProvider.getCheckoutCommand()); + } + public CommitCommandBuilder getCommitCommand() { logger.trace("create commit command"); @@ -84,10 +96,16 @@ public final class RepositoryClient implements Closeable { return new TagCommandBuilder(clientProvider.getTagCommand()); } + public MergeCommandBuilder getMergeCommand() { + logger.trace("create merge command"); + + return new MergeCommandBuilder(clientProvider.getMergeCommand()); + } + public File getWorkingCopy() { return clientProvider.getWorkingCopy(); } - + public boolean isCommandSupported(ClientCommand command) { return clientProvider.getSupportedClientCommands().contains(command); } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/CheckoutCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/CheckoutCommand.java new file mode 100644 index 0000000000..262ca3894c --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/CheckoutCommand.java @@ -0,0 +1,35 @@ +/* + * 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.client.spi; + +import java.io.IOException; + +/** + * @since 2.4.0 + */ +public interface CheckoutCommand { + + void checkout(String name) throws IOException; +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/DeleteRemoteBranchCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/DeleteRemoteBranchCommand.java new file mode 100644 index 0000000000..5dea57f965 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/DeleteRemoteBranchCommand.java @@ -0,0 +1,32 @@ +/* + * 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.client.spi; + +import java.io.IOException; + +public interface DeleteRemoteBranchCommand { + + void delete(String name) throws IOException; +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeCommand.java new file mode 100644 index 0000000000..a80031b655 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeCommand.java @@ -0,0 +1,37 @@ +/* + * 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.client.spi; + +import sonia.scm.repository.Changeset; + +import java.io.IOException; + +/** + * @since 2.4.0 + */ +public interface MergeCommand { + + Changeset merge(MergeRequest request) throws IOException; +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeRequest.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeRequest.java new file mode 100644 index 0000000000..826e5d9f03 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/MergeRequest.java @@ -0,0 +1,102 @@ +/* + * 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.client.spi; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * @since 2.4.0 + */ +public final class MergeRequest { + + private String branch; + private String message; + private FastForwardMode ffMode = FastForwardMode.FF; + + @Override + public boolean equals(Object obj) { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final MergeRequest other = (MergeRequest) obj; + + return Objects.equal(branch, other.branch) + && Objects.equal(message, other.message); + } + + @Override + public int hashCode() { + return Objects.hashCode(branch, message); + } + + public void reset() { + this.branch = null; + this.message = null; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("branch", branch) + .add("message", message) + .toString(); + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setFfMode(FastForwardMode ffMode) { + this.ffMode = ffMode; + } + + String getBranch() { + return branch; + } + + String getMessage() { + return message; + } + + public FastForwardMode getFfMode() { + return ffMode; + } + + public enum FastForwardMode { + FF_ONLY, FF, NO_FF + } +} diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java index feb1d3dd09..bfe4c5afe8 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/RepositoryClientProvider.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -87,6 +87,14 @@ public abstract class RepositoryClientProvider implements Closeable throw new ClientCommandNotSupportedException(ClientCommand.BRANCH); } + public DeleteRemoteBranchCommand getDeleteRemoteBranchCommand() { + throw new ClientCommandNotSupportedException(ClientCommand.DELETE_REMOTE_BRANCH); + } + + public CheckoutCommand getCheckoutCommand() { + throw new ClientCommandNotSupportedException(ClientCommand.CHECKOUT); + } + /** * Method description * @@ -131,6 +139,10 @@ public abstract class RepositoryClientProvider implements Closeable throw new ClientCommandNotSupportedException(ClientCommand.TAG); } + public MergeCommand getMergeCommand() { + throw new ClientCommandNotSupportedException(ClientCommand.MERGE); + } + /** * Returns the working copy of the repository client. * diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 2174f91c2e..31d18327f0 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -675,7 +675,7 @@ DEVELOPMENT - target/scm-it + ${project.parent.build.directory}/scm-it default 0.11.2 2.53.1 @@ -814,7 +814,7 @@ scm.home - target/scm-it + ${scm.home} scm.stage @@ -903,7 +903,7 @@ scm.home - target/scm-it + ${scm.home} ${project.basedir}/src/main/conf/jetty.xml