diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c05e5423..3733f78776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,26 @@ 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 +- Delete branches directly in the UI ([#1422](https://github.com/scm-manager/scm-manager/pull/1422)) +- Lookup command which provides further repository information ([#1415](https://github.com/scm-manager/scm-manager/pull/1415)) +- Include messages from scm protocol in modification or merge errors ([#1420](https://github.com/scm-manager/scm-manager/pull/1420)) +- Enhance trace api to accepted status codes ([#1430](https://github.com/scm-manager/scm-manager/pull/1430)) + ### Changed - Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416)) ### Fixed +- Missing close of hg diff command ([#1417](https://github.com/scm-manager/scm-manager/pull/1417)) - Error on repository initialization with least-privilege user ([#1414](https://github.com/scm-manager/scm-manager/pull/1414)) +- Adhere to git quiet flag ([#1421](https://github.com/scm-manager/scm-manager/pull/1421)) +- Resolve svn binary diffs properly [#1427](https://github.com/scm-manager/scm-manager/pull/1427) + +## [2.9.1] - 2020-11-11 +### Fixed +- German translation for repositories view + ## [2.9.0] - 2020-11-06 ### Added diff --git a/docs/de/user/repo/assets/repository-branch-detailView.png b/docs/de/user/repo/assets/repository-branch-detailView.png index a73b369e26..d671846fef 100644 Binary files a/docs/de/user/repo/assets/repository-branch-detailView.png and b/docs/de/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/de/user/repo/assets/repository-branches-overview.png b/docs/de/user/repo/assets/repository-branches-overview.png index 9258f4be97..39dcf5e424 100644 Binary files a/docs/de/user/repo/assets/repository-branches-overview.png and b/docs/de/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/de/user/repo/branches.md b/docs/de/user/repo/branches.md index b52b6492b2..17afe92924 100644 --- a/docs/de/user/repo/branches.md +++ b/docs/de/user/repo/branches.md @@ -6,6 +6,7 @@ subtitle: Branches Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet. Der Tag "Default" gibt an welcher Branch aktuell, als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet. +Alle Branches mit Ausnahme des Default Branches können über den Mülleimer-Icon unwiderruflich gelöscht werden. Über den "Branch erstellen"-Button gelangt man zum Formular, um neue Branches anzulegen. @@ -19,4 +20,6 @@ Mit dem "Branch erstellen"-Formular können neue Branches für das Repository er ### Branch Detailseite Hier werden einige Befehle zum Arbeiten mit dem Branch auf einer Kommandozeile aufgeführt. +Handelt es sich nicht um den Default Branch des Repositories, kann im unteren Bereich der Branch unwiderruflich gelöscht werden. + ![Branch Detailseite](assets/repository-branch-detailView.png) diff --git a/docs/en/user/repo/assets/repository-branch-detailView.png b/docs/en/user/repo/assets/repository-branch-detailView.png index 2660d58a82..615ba696c1 100644 Binary files a/docs/en/user/repo/assets/repository-branch-detailView.png and b/docs/en/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/en/user/repo/assets/repository-branches-overview.png b/docs/en/user/repo/assets/repository-branches-overview.png index 32560b4f31..e63ebab775 100644 Binary files a/docs/en/user/repo/assets/repository-branches-overview.png and b/docs/en/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/en/user/repo/branches.md b/docs/en/user/repo/branches.md index 8c39eae6f0..370165710b 100644 --- a/docs/en/user/repo/branches.md +++ b/docs/en/user/repo/branches.md @@ -6,6 +6,7 @@ subtitle: Branches The branches overview shows the branches that are already existing. By clicking on a branch, the details page of the branch is shown. The tag "Default" shows which branch is currently set as the default branch of the repository in SCM-Manager. The default branch is always shown first when opening the repository in SCM-Manager. +All branches except the default branch of the repository can be deleted by clicking on the trash bin icon. The button "Create Branch" opens the form to create a new branch. @@ -18,5 +19,6 @@ New branches can be created with the "Create Branch" form. There, you have to ch ### Branch Details Page This page shows some commands to work with the branch on the command line. +If the branch is not the default branch of the repository it can be deleted using the action inside the bottom section. ![Branch Details Page](assets/repository-branch-detailView.png) diff --git a/pom.xml b/pom.xml index fa8eaab8d0..b8192de3a8 100644 --- a/pom.xml +++ b/pom.xml @@ -903,7 +903,7 @@ - 3.5.13 + 3.5.15 2.1 5.7.0 @@ -925,8 +925,8 @@ 1.6.2 - 9.4.33.v20201020 - 9.4.30.v20200611 + 9.4.34.v20201102 + 9.4.34.v20201102 1.2.0 diff --git a/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java index 694120dc2b..93c84e01cc 100644 --- a/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java +++ b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.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; import java.util.Collections; @@ -33,7 +33,7 @@ public class ConcurrentModificationException extends ExceptionWithContext { private static final String CODE = "2wR7UzpPG1"; - public ConcurrentModificationException(Class type, String id) { + public ConcurrentModificationException(Class type, String id) { this(Collections.singletonList(new ContextEntry(type, id))); } @@ -56,4 +56,4 @@ public class ConcurrentModificationException extends ExceptionWithContext { .collect(joining(" in ", "", " has been modified concurrently")); } } - + diff --git a/scm-core/src/main/java/sonia/scm/ContextEntry.java b/scm-core/src/main/java/sonia/scm/ContextEntry.java index d398e54609..15217e3173 100644 --- a/scm-core/src/main/java/sonia/scm/ContextEntry.java +++ b/scm-core/src/main/java/sonia/scm/ContextEntry.java @@ -21,22 +21,23 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.util.AssertUtil; +import java.io.Serializable; import java.util.Collections; import java.util.LinkedList; import java.util.List; -public class ContextEntry { +public class ContextEntry implements Serializable { private final String type; private final String id; - ContextEntry(Class type, String id) { + ContextEntry(Class type, String id) { this(type.getSimpleName(), id); } @@ -91,7 +92,7 @@ public class ContextEntry { return this.in(Repository.class, namespaceAndName.logString()); } - public ContextBuilder in(Class type, String id) { + public ContextBuilder in(Class type, String id) { context.add(new ContextEntry(type, id)); return this; } diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java index 1631aea178..16c0f0218c 100644 --- a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -24,6 +24,7 @@ package sonia.scm; +import java.io.Serializable; import java.util.List; import java.util.Optional; @@ -34,15 +35,26 @@ public abstract class ExceptionWithContext extends RuntimeException { private static final long serialVersionUID = 4327413456580409224L; private final List context; + private final List additionalMessages; - public ExceptionWithContext(List context, String message) { - super(message); - this.context = context; + protected ExceptionWithContext(List context, String message) { + this(context, null, message); } - public ExceptionWithContext(List context, String message, Exception cause) { + protected ExceptionWithContext(List context, List additionalMessages, String message) { + super(message); + this.context = context; + this.additionalMessages = additionalMessages; + } + + protected ExceptionWithContext(List context, String message, Exception cause) { + this(context, null, message, cause); + } + + protected ExceptionWithContext(List context, List additionalMessages, String message, Exception cause) { super(message, cause); this.context = context; + this.additionalMessages = additionalMessages; } public List getContext() { @@ -61,4 +73,26 @@ public abstract class ExceptionWithContext extends RuntimeException { public Optional getUrl() { return Optional.empty(); } + + public List getAdditionalMessages() { + return additionalMessages; + } + + public static class AdditionalMessage implements Serializable { + private final String key; + private final String message; + + public AdditionalMessage(String key, String message) { + this.key = key; + this.message = message; + } + + public String getKey() { + return key; + } + + public String getMessage() { + return message; + } + } } diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 2ca74fc60c..0c0a06926c 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.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; import java.util.Collections; @@ -35,7 +35,7 @@ public class NotFoundException extends ExceptionWithContext { private static final String CODE = "AGR7UzkhA1"; - public NotFoundException(Class type, String id) { + public NotFoundException(Class type, String id) { this(Collections.singletonList(new ContextEntry(type, id))); } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java index 5199446a4f..737d4b1fc2 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.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.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; @@ -40,7 +40,10 @@ public class ErrorDto { private List context; private String message; - @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List additionalMessages; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) @XmlElementWrapper(name = "violations") private List violations; @@ -53,4 +56,10 @@ public class ErrorDto { private String path; private String message; } + + @Getter @Setter + public static class AdditionalMessageDto { + private String key; + private String message; + } } diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java index 302f2ac6fe..5b0ebc453c 100644 --- a/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java +++ b/scm-core/src/main/java/sonia/scm/net/ahc/BaseHttpRequest.java @@ -257,6 +257,22 @@ public abstract class BaseHttpRequest return self(); } + /** + * Sets the response codes which should be traced as successful. + * + * Example: If 400 is set as {@link #acceptedStatusCodes} then all requests + * which get a response with status code 400 will be traced as successful (not failed) request + * + * @param codes status codes which should be traced as successful + * @return request instance + * + * @since 2.10.0 + */ + public T acceptStatusCodes(int... codes) { + this.acceptedStatusCodes = codes; + return self(); + } + /** * Disables tracing for the request. * This should only be done for internal requests. @@ -314,6 +330,18 @@ public abstract class BaseHttpRequest return spanKind; } + + /** + * Returns the response codes which are accepted as successful by tracer. + * + * @return codes + * + * @since 2.10.0 + */ + public int[] getAcceptedStatus() { + return acceptedStatusCodes; + } + /** * Returns true if the request decodes gzip compression. * @@ -434,4 +462,7 @@ public abstract class BaseHttpRequest /** kind of span for trace api */ private String spanKind = "HTTP Request"; + + /** codes which will be marked as successful by tracer */ + private int[] acceptedStatusCodes = new int[]{}; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index d00c898233..4a2d3e77ae 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -57,5 +57,10 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY; + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY, + + /** + * @since 2.10.0 + */ + LOOKUP; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LookupCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LookupCommandBuilder.java new file mode 100644 index 0000000000..2c047b97eb --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/LookupCommandBuilder.java @@ -0,0 +1,51 @@ +/* + * 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 sonia.scm.repository.spi.LookupCommand; +import sonia.scm.repository.spi.LookupCommandRequest; + +import java.util.Optional; + +/** + * The lookup command executes a lookup for additional repository information. + * + * @since 2.10.0 + */ +public class LookupCommandBuilder { + + private final LookupCommand lookupCommand; + + public LookupCommandBuilder(LookupCommand lookupCommand) { + this.lookupCommand = lookupCommand; + } + + public Optional lookup(Class type, String... args) { + LookupCommandRequest request = new LookupCommandRequest<>(); + request.setType(type); + request.setArgs(args); + return lookupCommand.lookup(request); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index a6b4265239..15c2f3f523 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -430,6 +430,20 @@ public final class RepositoryService implements Closeable { return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, eMail); } + /** + * The lookup command executes a lookup which returns additional information for the repository. + * + * @return instance of {@link LookupCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + * @since 2.10.0 + */ + public LookupCommandBuilder getLookupCommand() { + LOG.debug("create lookup command for repository {}", + repository.getNamespaceAndName()); + return new LookupCommandBuilder(provider.getLookupCommand()); + } + /** * Returns true if the command is supported by the repository service. * diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java index 3259cc2094..f8dc0ba2d0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java @@ -21,27 +21,58 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; -import sonia.scm.ContextEntry; import sonia.scm.ExceptionWithContext; import sonia.scm.repository.Repository; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + public class IntegrateChangesFromWorkdirException extends ExceptionWithContext { - private static final String CODE = "CHRM7IQzo1"; + static final String CODE_WITH_ADDITIONAL_MESSAGES = "CHRM7IQzo1"; + static final String CODE_WITHOUT_ADDITIONAL_MESSAGES = "ASSG1ehZ01"; - public IntegrateChangesFromWorkdirException(Repository repository, String message) { - super(ContextEntry.ContextBuilder.entity(repository).build(), message); + private static final Pattern SCM_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\] (.*)"); + + public static MessageExtractor withPattern(Pattern pattern) { + return new MessageExtractor(pattern); } - public IntegrateChangesFromWorkdirException(Repository repository, String message, Exception cause) { - super(ContextEntry.ContextBuilder.entity(repository).build(), message, cause); + public static IntegrateChangesFromWorkdirException forMessage(Repository repository, String message) { + return new MessageExtractor(SCM_MESSAGE_PATTERN).forMessage(repository, message); + } + + private IntegrateChangesFromWorkdirException(Repository repository, List additionalMessages) { + super(entity(repository).build(), additionalMessages, "errors from hook"); } @Override public String getCode() { - return CODE; + return getAdditionalMessages().isEmpty()? CODE_WITHOUT_ADDITIONAL_MESSAGES : CODE_WITH_ADDITIONAL_MESSAGES; + } + + public static class MessageExtractor { + + private final Pattern extractorPattern; + + public MessageExtractor(Pattern extractorPattern) { + this.extractorPattern = extractorPattern; + } + + public IntegrateChangesFromWorkdirException forMessage(Repository repository, String message) { + return new IntegrateChangesFromWorkdirException(repository, stream(message.split("\\n")) + .map(extractorPattern::matcher) + .filter(Matcher::matches) + .map(matcher -> new AdditionalMessage(null, matcher.group(1))) + .collect(Collectors.toList())); + } } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommand.java new file mode 100644 index 0000000000..363d50ec4e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommand.java @@ -0,0 +1,38 @@ +/* + * 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 java.util.Optional; + +public interface LookupCommand { + + /** + * Executes lookup for given parameters. + * + * @param request Arguments provided for the lookup. + * @return Result of provided type. + */ + Optional lookup(LookupCommandRequest request); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommandRequest.java new file mode 100644 index 0000000000..0094351038 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LookupCommandRequest.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.spi; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LookupCommandRequest { + private Class type; + private String[] args; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index 6bde899617..091c9b46b3 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -274,4 +274,12 @@ public abstract class RepositoryServiceProvider implements Closeable { throw new CommandNotSupportedException(Command.MODIFY); } + + /** + * @since 2.10.0 + */ + public LookupCommand getLookupCommand() + { + throw new CommandNotSupportedException(Command.LOOKUP); + } } diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirExceptionTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirExceptionTest.java new file mode 100644 index 0000000000..d990a64ef7 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirExceptionTest.java @@ -0,0 +1,73 @@ +/* + * 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.junit.jupiter.api.Test; +import sonia.scm.repository.Repository; + +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.CODE_WITHOUT_ADDITIONAL_MESSAGES; +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.CODE_WITH_ADDITIONAL_MESSAGES; +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.forMessage; +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.withPattern; + +class IntegrateChangesFromWorkdirExceptionTest { + + private static final Repository REPOSITORY = new Repository("1", "git", "hitchhiker", "hog"); + + @Test + void shouldExtractMessagesWithDefaultPrefix() { + IntegrateChangesFromWorkdirException exception = + forMessage(REPOSITORY, "prefix [SCM] line 1\nprefix [SCM] line 2\nirrelevant line\n"); + + assertThat(exception.getAdditionalMessages()) + .extracting("message") + .containsExactly("line 1", "line 2"); + assertThat(exception.getCode()).isEqualTo(CODE_WITH_ADDITIONAL_MESSAGES); + } + + @Test + void shouldExtractMessagesWithCustomPattern() { + IntegrateChangesFromWorkdirException exception = + withPattern(Pattern.compile("-custom- (.*)")) + .forMessage(REPOSITORY, "to be ignored\n-custom- line\n"); + + assertThat(exception.getAdditionalMessages()) + .extracting("message") + .containsExactly("line"); + assertThat(exception.getCode()).isEqualTo(CODE_WITH_ADDITIONAL_MESSAGES); + } + + @Test + void shouldCreateSpecialMessageForMissingAdditionalMessages() { + IntegrateChangesFromWorkdirException exception = + forMessage(REPOSITORY, "There is no message"); + + assertThat(exception.getAdditionalMessages()).isEmpty(); + assertThat(exception.getCode()).isEqualTo(CODE_WITHOUT_ADDITIONAL_MESSAGES); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookMessageProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookMessageProvider.java index f5e451df84..b7ca70fac9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookMessageProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookMessageProvider.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 -------------------------------------------------------- @@ -71,7 +71,9 @@ public final class GitHookMessageProvider implements HookMessageProvider @Override public void sendMessage(String message) { - GitHooks.sendPrefixedMessage(receivePack, message); + if (!receivePack.isQuiet()) { + GitHooks.sendPrefixedMessage(receivePack, message); + } } //~--- fields --------------------------------------------------------------- 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 c083123242..8ad6e3ddf4 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 @@ -59,6 +59,7 @@ 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; +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.forMessage; //~--- JDK imports ------------------------------------------------------------ @@ -255,7 +256,7 @@ class AbstractGitCommand { .findAny() .ifPresent(remoteRefUpdate -> { logger.info("message for failed push: {}", pushResult.getMessages()); - throw new IntegrateChangesFromWorkdirException(repository, "could not push changes into central repository: " + remoteRefUpdate.getStatus()); + throw forMessage(repository, pushResult.getMessages()); }); } catch (GitAPIException e) { throw new InternalRepositoryException(repository, "could not push changes into central repository", e); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java index 3a291b4501..7319a312ff 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -37,6 +37,8 @@ import sonia.scm.repository.api.BranchRequest; import sonia.scm.repository.work.WorkingCopy; import sonia.scm.user.User; +import java.io.IOException; + /** * Mercurial implementation of the {@link BranchCommand}. * Note that this creates an empty commit to "persist" the new branch. @@ -106,9 +108,8 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workingCopyFactory.configure(pullCommand); pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); - } catch (Exception e) { - // TODO handle failed update - throw new IntegrateChangesFromWorkdirException(getRepository(), + } catch (IOException e) { + throw new InternalRepositoryException(getRepository(), String.format("Could not pull changes '%s' into central repository", branch), e); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java index 605ce5555a..08c42a023e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java @@ -24,71 +24,65 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.aragost.javahg.Repository; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; -import com.google.common.io.Closeables; import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.spi.javahg.HgDiffInternalCommand; import sonia.scm.web.HgUtil; +import javax.annotation.Nonnull; +import java.io.IOException; import java.io.InputStream; - -//~--- JDK imports ------------------------------------------------------------ +import java.io.OutputStream; /** * * @author Sebastian Sdorra */ -public class HgDiffCommand extends AbstractCommand implements DiffCommand -{ +public class HgDiffCommand extends AbstractCommand implements DiffCommand { - /** - * Constructs ... - * - * @param context - * - */ - HgDiffCommand(HgCommandContext context) - { + HgDiffCommand(HgCommandContext context) { super(context); } - //~--- get methods ---------------------------------------------------------- - @Override - public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) - { + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { return output -> { - com.aragost.javahg.Repository hgRepo = open(); - - HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); - DiffFormat format = request.getFormat(); - - if (format == DiffFormat.GIT) - { - cmd.git(); - } - - cmd.change(HgUtil.getRevision(request.getRevision())); - - InputStream inputStream = null; - + Repository hgRepo = open(); try { - - if (!Strings.isNullOrEmpty(request.getPath())) { - inputStream = cmd.stream(hgRepo.file(request.getPath())); - } else { - inputStream = cmd.stream(); - } - - ByteStreams.copy(inputStream, output); - + diff(hgRepo, request, output); } finally { - Closeables.close(inputStream, true); + getContext().close(); } }; } + + @SuppressWarnings("UnstableApiUsage") + private void diff(Repository hgRepo, DiffCommandRequest request, OutputStream output) throws IOException { + HgDiffInternalCommand cmd = createDiffCommand(hgRepo, request); + try (InputStream inputStream = streamDiff(hgRepo, cmd, request.getPath())) { + ByteStreams.copy(inputStream, output); + } + } + + @Nonnull + private HgDiffInternalCommand createDiffCommand(Repository hgRepo, DiffCommandRequest request) { + HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); + DiffFormat format = request.getFormat(); + if (format == DiffFormat.GIT) { + cmd.git(); + } + cmd.change(HgUtil.getRevision(request.getRevision())); + return cmd; + } + + private InputStream streamDiff(Repository hgRepo, HgDiffInternalCommand cmd, String path) { + if (!Strings.isNullOrEmpty(path)) { + return cmd.stream(hgRepo.file(path)); + } else { + return cmd.stream(); + } + } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java index 10e444b03a..aaea12eeac 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -41,11 +41,13 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.regex.Pattern; @SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype public class HgModifyCommand implements ModifyCommand { private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class); + static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)"); private final HgCommandContext context; private final HgWorkingCopyFactory workingCopyFactory; @@ -128,8 +130,12 @@ public class HgModifyCommand implements ModifyCommand { com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workingCopyFactory.configure(pullCommand); return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); - } catch (Exception e) { - throw new IntegrateChangesFromWorkdirException(context.getScmRepository(), + } catch (ExecutionException e) { + throw IntegrateChangesFromWorkdirException + .withPattern(HG_MESSAGE_PATTERN) + .forMessage(context.getScmRepository(), e.getMessage()); + } catch (IOException e) { + throw new InternalRepositoryException(context.getScmRepository(), String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()), e); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java new file mode 100644 index 0000000000..9775a5cd42 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java @@ -0,0 +1,78 @@ +/* + * 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.junit.Test; +import sonia.scm.repository.api.DiffCommandBuilder; +import sonia.scm.repository.api.DiffFormat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class HgDiffCommandTest extends AbstractHgCommandTestBase { + + @Test + public void shouldCreateDiff() throws IOException { + String content = diff(cmdContext, "3049df33fdbbded08b707bac3eccd0f7b453c58b"); + assertThat(content).contains("+e"); + } + + @Test + public void shouldCreateGitDiff() throws IOException { + DiffCommandRequest request = new DiffCommandRequest(); + request.setRevision("3049df33fdbbded08b707bac3eccd0f7b453c58b"); + request.setFormat(DiffFormat.GIT); + + String content = diff(cmdContext, request); + assertThat(content).contains("git"); + } + + @Test + public void shouldCloseContent() throws IOException { + HgCommandContext context = spy(cmdContext); + String content = diff(context, "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"); + assertThat(content).contains("+b"); + verify(context).close(); + } + + private String diff(HgCommandContext context, String revision) throws IOException { + DiffCommandRequest request = new DiffCommandRequest(); + request.setRevision(revision); + return diff(context, request); + } + + private String diff(HgCommandContext context, DiffCommandRequest request) throws IOException { + HgDiffCommand diff = new HgDiffCommand(context); + DiffCommandBuilder.OutputStreamConsumer consumer = diff.getDiffResult(request); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + consumer.accept(baos); + return baos.toString("UTF-8"); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java index 0ccc7d94c6..168d741bcb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -38,6 +38,7 @@ import sonia.scm.repository.work.WorkdirProvider; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.regex.Matcher; import static org.assertj.core.api.Assertions.assertThat; @@ -180,4 +181,18 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase { public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() { hgModifyCommand.execute(new ModifyCommandRequest()); } + + @Test + public void shouldExtractSimpleMessage() { + Matcher matcher = HgModifyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] This is a simple message"); + matcher.matches(); + assertThat(matcher.group(1)).isEqualTo("This is a simple message"); + } + + @Test + public void shouldExtractErrorMessage() { + Matcher matcher = HgModifyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] Error: This is an error message"); + matcher.matches(); + assertThat(matcher.group(1)).isEqualTo("This is an error message"); + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java index 81e40023f6..c7668c0520 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java @@ -565,6 +565,8 @@ public class SCMSvnDiffGenerator implements ISvnDiffGenerator { } if (!useGitFormat){ displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + } else { + displayBinaryGit(target, operation, outputStream); } return; @@ -590,6 +592,26 @@ public class SCMSvnDiffGenerator implements ISvnDiffGenerator { } } + private void displayBinaryGit(SvnTarget target, SvnDiffCallback.OperationKind operation, OutputStream outputStream) throws SVNException { + String path1 = operation == SvnDiffCallback.OperationKind.Added ? "/dev/null" : getRelativeToRootPath(target, originalTarget1); + String path2 = operation == SvnDiffCallback.OperationKind.Deleted ? "/dev/null" : getRelativeToRootPath(target, originalTarget2); + try { + displayString(outputStream, formatPath(path1, "---", "a")); + displayString(outputStream, formatPath(path2, "+++", "b")); + displayString(outputStream, "Binary files differ"); + } catch (IOException e) { + wrapException(e); + } + } + + private String formatPath(String path, String start, String aOrB) { + if (path.equals("/dev/null")) { + return String.format("%s %s\n", start, path); + } else { + return String.format("%s %s/%s\n", start, aOrB, path); + } + } + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { displayCannotDisplayFileMarkedBinary(outputStream); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLookupCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLookupCommand.java new file mode 100644 index 0000000000..ed79e40e25 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLookupCommand.java @@ -0,0 +1,64 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.io.SVNRepository; + +import java.util.Optional; + +@Slf4j +public class SvnLookupCommand extends AbstractSvnCommand implements LookupCommand { + + protected SvnLookupCommand(SvnContext context) { + super(context); + } + + @Override + public Optional lookup(LookupCommandRequest request) { + try { + if (request.getArgs().length > 1 && "propget".equalsIgnoreCase(request.getArgs()[0])) { + return lookupProps(request); + } + } catch (SVNException e) { + log.error("Lookup failed: ", e); + } + + return Optional.empty(); + } + + private Optional lookupProps(LookupCommandRequest request) throws SVNException { + if (request.getArgs()[1].equalsIgnoreCase("uuid")) { + if (!request.getType().equals(String.class)) { + throw new IllegalArgumentException("uuid can only be returned as String"); + } + SVNRepository repository = context.open(); + return Optional.of((T) repository.getRepositoryUUID(true)); + } + log.debug("No result found on lookup"); + return Optional.empty(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java index 849d5619a5..326b710874 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java @@ -27,6 +27,7 @@ package sonia.scm.repository.spi; import org.apache.shiro.SecurityUtils; import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNWCClient; @@ -40,9 +41,14 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.regex.Pattern; + +import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.withPattern; public class SvnModifyCommand implements ModifyCommand { + public static final Pattern SVN_ERROR_PATTERN = Pattern.compile(".*E" + SVNErrorCode.CANCELLED.getCode() + ": (.*)"); + private final SvnContext context; private final SvnWorkingCopyFactory workingCopyFactory; private final Repository repository; @@ -81,7 +87,7 @@ public class SvnModifyCommand implements ModifyCommand { ); return String.valueOf(svnCommitInfo.getNewRevision()); } catch (SVNException e) { - throw new InternalRepositoryException(repository, "could not commit changes on repository"); + throw withPattern(SVN_ERROR_PATTERN).forMessage(repository, e.getErrorMessage().getRootErrorMessage().getFullMessage()); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 1548fba869..f948a7168f 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -46,7 +46,7 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider //J- public static final Set COMMANDS = ImmutableSet.of( Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF, - Command.LOG, Command.BUNDLE, Command.UNBUNDLE, Command.MODIFY + Command.LOG, Command.BUNDLE, Command.UNBUNDLE, Command.MODIFY, Command.LOOKUP ); //J+ @@ -148,14 +148,21 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider return new SvnLogCommand(context); } + @Override public ModificationsCommand getModificationsCommand() { return new SvnModificationsCommand(context); } + @Override public ModifyCommand getModifyCommand() { return new SvnModifyCommand(context, workingCopyFactory); } + @Override + public LookupCommand getLookupCommand() { + return new SvnLookupCommand(context); + } + /** * Method description * diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLookupCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLookupCommandTest.java new file mode 100644 index 0000000000..df4fedae92 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLookupCommandTest.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.spi; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.io.SVNRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SvnLookupCommandTest { + + @Mock + SvnContext context; + + @Mock + SVNRepository svnRepository; + + @InjectMocks + SvnLookupCommand command; + + @Test + void shouldReturnEmptyOptional() { + LookupCommandRequest request = new LookupCommandRequest(); + request.setType(String.class); + request.setArgs(new String[]{"propget"}); + + Optional result = command.lookup(request); + + assertThat(result).isNotPresent(); + } + + @Test + void shouldReturnRepositoryUUID() throws SVNException { + String uuid = "trillian-hitchhiker-42"; + when(context.open()).thenReturn(svnRepository); + when(svnRepository.getRepositoryUUID(true)).thenReturn(uuid); + + LookupCommandRequest request = new LookupCommandRequest(); + request.setType(String.class); + request.setArgs(new String[]{"propget", "uuid", "/"}); + + Optional result = command.lookup(request); + + assertThat(result).isPresent(); + assertThat(result.get()) + .isInstanceOf(String.class) + .isEqualTo(uuid); + } +} diff --git a/scm-ui/jest-preset/package.json b/scm-ui/jest-preset/package.json index 2eeb3d2ece..de6e233d3c 100644 --- a/scm-ui/jest-preset/package.json +++ b/scm-ui/jest-preset/package.json @@ -10,7 +10,7 @@ "dependencies": { "babel-jest": "^24.9.0", "babel-plugin-require-context-hook": "^1.0.0", - "jest": "^24.9.0", + "jest": "^26.0.0", "jest-junit": "^8.0.0" }, "publishConfig": { diff --git a/scm-ui/ui-components/src/BackendErrorNotification.tsx b/scm-ui/ui-components/src/BackendErrorNotification.tsx index b61e25d777..a680304e73 100644 --- a/scm-ui/ui-components/src/BackendErrorNotification.tsx +++ b/scm-ui/ui-components/src/BackendErrorNotification.tsx @@ -42,6 +42,7 @@ class BackendErrorNotification extends React.Component {

{this.renderErrorName()}

{this.renderErrorDescription()}

+ {this.renderAdditionalMessages()}

{this.renderViolations()}

{this.renderMetadata()}
@@ -51,7 +52,7 @@ class BackendErrorNotification extends React.Component { renderErrorName = () => { const { error, t } = this.props; - const translation = t("errors." + error.errorCode + ".displayName"); + const translation = t(`errors.${error.errorCode}.displayName`); if (translation === error.errorCode) { return error.message; } @@ -60,13 +61,32 @@ class BackendErrorNotification extends React.Component { renderErrorDescription = () => { const { error, t } = this.props; - const translation = t("errors." + error.errorCode + ".description"); + const translation = t(`errors.${error.errorCode}.description`); if (translation === error.errorCode) { return ""; } return translation; }; + renderAdditionalMessages = () => { + const { error, t } = this.props; + if (error.additionalMessages) { + return ( + <> +
+ {error.additionalMessages + .map(additionalMessage => + additionalMessage.key ? t(`errors.${additionalMessage.key}.description`) : additionalMessage.message + ) + .map(message => ( +

{message}

+ ))} +
+ + ); + } + }; + renderViolations = () => { const { error, t } = this.props; if (error.violations) { diff --git a/scm-ui/ui-components/src/SplitAndReplace.tsx b/scm-ui/ui-components/src/SplitAndReplace.tsx index a3f72b553f..e1125b2265 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.tsx @@ -52,7 +52,13 @@ const SplitAndReplace: FC = ({ text, replacements }) => { if (parts.length === 0) { return <>{parts[0]}; } - return <>{parts}; + return ( + <> + {parts.map((part, index) => ( + {part} + ))} + + ); }; export default SplitAndReplace; diff --git a/scm-ui/ui-components/src/errors.ts b/scm-ui/ui-components/src/errors.ts index d1f7aef226..41e44f0702 100644 --- a/scm-ui/ui-components/src/errors.ts +++ b/scm-ui/ui-components/src/errors.ts @@ -31,6 +31,10 @@ export type Violation = { message: string; key?: string; }; +export type AdditionalMessage = { + key?: string; + message?: string; +}; export type BackendErrorContent = { transactionId: string; @@ -39,6 +43,7 @@ export type BackendErrorContent = { url?: string; context: Context; violations: Violation[]; + additionalMessages?: AdditionalMessage[]; }; export class BackendError extends Error { @@ -48,6 +53,7 @@ export class BackendError extends Error { context: Context = []; statusCode: number; violations: Violation[]; + additionalMessages?: AdditionalMessage[]; constructor(content: BackendErrorContent, name: string, statusCode: number) { super(content.message); @@ -58,6 +64,7 @@ export class BackendError extends Error { this.context = content.context; this.statusCode = statusCode; this.violations = content.violations; + this.additionalMessages = content.additionalMessages; } } diff --git a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx index 89f8f0a117..290f024b63 100644 --- a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx +++ b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx @@ -62,11 +62,11 @@ export const ConfirmAlert: FC = ({ title, message, buttons, close }) => { const footer = (
- {buttons.map((button, i) => ( -

+ {buttons.map((button, index) => ( +

handleClickButton(button)} > {button.label} diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx index b326cd0b21..1dd19da914 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx @@ -176,7 +176,13 @@ const ChangesetAuthor: FC = ({ changeset }) => { authorLine.push(...extensions); } - return {authorLine}; + return ( + + {authorLine.map((line, index) => ( + {line} + ))} + + ); }; export default ChangesetAuthor; diff --git a/scm-ui/ui-components/src/repos/diffs.ts b/scm-ui/ui-components/src/repos/diffs.ts index 029803a0a4..0f39f3a4dc 100644 --- a/scm-ui/ui-components/src/repos/diffs.ts +++ b/scm-ui/ui-components/src/repos/diffs.ts @@ -41,5 +41,5 @@ export function createHunkIdentifierFromContext(ctx: BaseContext) { } export function escapeWhitespace(path: string) { - return path.toLowerCase().replace(/\W/g, "-"); + return path?.toLowerCase().replace(/\W/g, "-"); } diff --git a/scm-ui/ui-scripts/package.json b/scm-ui/ui-scripts/package.json index 6132eb70bf..57ecfd1ac4 100644 --- a/scm-ui/ui-scripts/package.json +++ b/scm-ui/ui-scripts/package.json @@ -17,7 +17,7 @@ "mini-css-extract-plugin": "^0.12.0", "mustache": "^3.1.0", "optimize-css-assets-webpack-plugin": "^5.0.3", - "react-refresh": "^0.8.0", + "react-refresh": "^0.9.0", "sass": "^1.26.3", "sass-loader": "^8.0.0", "script-loader": "^0.7.2", diff --git a/scm-ui/ui-tests/package.json b/scm-ui/ui-tests/package.json index 706a6cb06c..fcbf2c137b 100644 --- a/scm-ui/ui-tests/package.json +++ b/scm-ui/ui-tests/package.json @@ -14,7 +14,7 @@ "enzyme-adapter-react-16": "^1.15.0", "enzyme-context": "^1.1.2", "enzyme-context-react-router-4": "^2.0.0", - "jest": "^24.9.0", + "jest": "^26.0.0", "raf": "^3.4.1", "react-test-renderer": "^16.10.2" }, diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index d3ec72b23f..d95ef44970 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -68,7 +68,19 @@ "name": "Name:", "commits": "Commits", "sources": "Sources", - "defaultTag": "Default" + "defaultTag": "Default", + "dangerZone": "Branch löschen", + "delete": { + "button": "Branch löschen", + "subtitle": "Branch löschen", + "description": "Gelöschte Branches können nicht wiederhergestellt werden.", + "confirmAlert": { + "title": "Branch löschen", + "message": "Möchten Sie den Branch \"{{branch}}\" wirklich löschen?", + "cancel": "Nein", + "submit": "Ja" + } + } }, "tags": { "overview": { @@ -279,4 +291,4 @@ "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen." } -}, +} diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 5f83ad7ba1..59981282df 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -68,7 +68,19 @@ "name": "Name:", "commits": "Commits", "sources": "Sources", - "defaultTag": "Default" + "defaultTag": "Default", + "dangerZone": "Delete branch", + "delete": { + "button": "Delete branch", + "subtitle": "Delete branch", + "description": "Deleted branches can not be restored.", + "confirmAlert": { + "title": "Delete branch", + "message": "Do you really want to delete the branch \"{{branch}}\"?", + "cancel": "No", + "submit": "Yes" + } + } }, "tags": { "overview": { diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx index baddb90afd..1acaa78159 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx @@ -22,25 +22,42 @@ * SOFTWARE. */ import React, { FC } from "react"; -import { Link } from "react-router-dom"; -import { Branch } from "@scm-manager/ui-types"; +import { Link as ReactLink } from "react-router-dom"; +import { Branch, Link } from "@scm-manager/ui-types"; import DefaultBranchTag from "./DefaultBranchTag"; +import { Icon } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; type Props = { baseUrl: string; branch: Branch; + onDelete: (branch: Branch) => void; }; -const BranchRow: FC = ({ baseUrl, branch }) => { +const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; + const [t] = useTranslation("repos"); + + let deleteButton; + if ((branch?._links?.delete as Link)?.href) { + deleteButton = ( + onDelete(branch)}> + + + + + ); + } + return ( - + {branch.name} - + + {deleteButton} ); }; diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx index db23ca7ad0..312275b663 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx @@ -21,41 +21,84 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; +import React, { FC, useState } from "react"; +import { useTranslation } from "react-i18next"; import BranchRow from "./BranchRow"; -import { Branch } from "@scm-manager/ui-types"; +import { Branch, Link } from "@scm-manager/ui-types"; +import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components"; -type Props = WithTranslation & { +type Props = { baseUrl: string; branches: Branch[]; + fetchBranches: () => void; }; -class BranchTable extends React.Component { - render() { - const { t } = this.props; - return ( +const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => { + const [t] = useTranslation("repos"); + const [showConfirmAlert, setShowConfirmAlert] = useState(false); + const [error, setError] = useState(); + const [branchToBeDeleted, setBranchToBeDeleted] = useState(); + + const onDelete = (branch: Branch) => { + setBranchToBeDeleted(branch); + setShowConfirmAlert(true); + }; + + const abortDelete = () => { + setBranchToBeDeleted(undefined); + setShowConfirmAlert(false); + }; + + const deleteBranch = () => { + apiClient + .delete((branchToBeDeleted?._links.delete as Link).href) + .then(() => fetchBranches()) + .catch(setError); + }; + + const renderRow = () => { + let rowContent = null; + if (branches) { + rowContent = branches.map((branch, index) => { + return ; + }); + } + return rowContent; + }; + + const confirmAlert = ( + deleteBranch() + }, + { + label: t("branch.delete.confirmAlert.cancel"), + onClick: () => abortDelete() + } + ]} + close={() => abortDelete()} + /> + ); + + return ( + <> + {showConfirmAlert && confirmAlert} + {error && } - {this.renderRow()} + {renderRow()}
{t("branches.table.branches")}
- ); - } + + ); +}; - renderRow() { - const { baseUrl, branches } = this.props; - let rowContent = null; - if (branches) { - rowContent = branches.map((branch, index) => { - return ; - }); - } - return rowContent; - } -} - -export default withTranslation("repos")(BranchTable); +export default BranchTable; diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx index 9a88c08c2d..32ff0d8f7a 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx @@ -25,6 +25,7 @@ import React from "react"; import BranchDetail from "./BranchDetail"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Branch, Repository } from "@scm-manager/ui-types"; +import BranchDangerZone from "../containers/BranchDangerZone"; type Props = { repository: Repository; @@ -34,7 +35,6 @@ type Props = { class BranchView extends React.Component { render() { const { repository, branch } = this.props; - return (

@@ -49,6 +49,7 @@ class BranchView extends React.Component { }} />
+
); } diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx new file mode 100644 index 0000000000..9e6c0e167a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx @@ -0,0 +1,59 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { Branch, Repository } from "@scm-manager/ui-types"; +import { Subtitle } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { DangerZoneContainer } from "../../containers/RepositoryDangerZone"; +import DeleteBranch from "./DeleteBranch"; + +type Props = { + repository: Repository; + branch: Branch; +}; + +const BranchDangerZone: FC = ({ repository, branch }) => { + const [t] = useTranslation("repos"); + + const dangerZone = []; + + if (branch?._links?.delete) { + dangerZone.push(); + } + + if (dangerZone.length === 0) { + return null; + } + + return ( + <> +
+ + {dangerZone} + + ); +}; + +export default BranchDangerZone; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx index cb2847a712..9a9aa02d24 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchRoot.tsx @@ -28,10 +28,9 @@ import { compose } from "redux"; import { Redirect, Route, Switch, withRouter } from "react-router-dom"; import { Branch, Repository } from "@scm-manager/ui-types"; import { fetchBranch, getBranch, getFetchBranchFailure, isFetchBranchPending } from "../modules/branches"; -import { ErrorNotification, Loading, NotFoundError } from "@scm-manager/ui-components"; +import { ErrorNotification, Loading, NotFoundError, urls } from "@scm-manager/ui-components"; import { History } from "history"; import queryString from "query-string"; -import { urls } from "@scm-manager/ui-components"; type Props = { repository: Repository; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx index 0e2a01622b..8bbcac53bf 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx @@ -81,10 +81,10 @@ class BranchesOverview extends React.Component { } renderBranchesTable() { - const { baseUrl, branches, t } = this.props; + const { baseUrl, branches, repository, fetchBranches, t } = this.props; if (branches && branches.length > 0) { orderBranches(branches); - return ; + return fetchBranches(repository)} />; } return {t("branches.overview.noBranches")}; } diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx new file mode 100644 index 0000000000..820c09740b --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/branches/containers/DeleteBranch.tsx @@ -0,0 +1,92 @@ +/* + * 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. + */ +import React, { FC, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Branch, Link, Repository } from "@scm-manager/ui-types"; +import { apiClient, ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components"; + +type Props = { + repository: Repository; + branch: Branch; +}; + +const DeleteBranch: FC = ({ repository, branch }: Props) => { + const [showConfirmAlert, setShowConfirmAlert] = useState(false); + const [error, setError] = useState(); + const [t] = useTranslation("repos"); + const history = useHistory(); + + const deleteBranch = () => { + apiClient + .delete((branch._links.delete as Link).href) + .then(() => history.push(`/repo/${repository.namespace}/${repository.name}/branches/`)) + .catch(setError); + }; + + if (!branch._links.delete) { + return null; + } + + let confirmAlert = null; + if (showConfirmAlert) { + confirmAlert = ( + deleteBranch() + }, + { + label: t("branch.delete.confirmAlert.cancel"), + onClick: () => null + } + ]} + close={() => setShowConfirmAlert(false)} + /> + ); + } + + return ( + <> + + {showConfirmAlert && confirmAlert} + + {t("branch.delete.subtitle")} +
+ {t("branch.delete.description")} +

+ } + right={ setShowConfirmAlert(true)} />} + /> + + ); +}; + +export default DeleteBranch; diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index f6caa08c9b..be9a70dfc8 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -174,8 +174,8 @@ class ChangesetDetails extends React.Component { const description = changesets.parseDescription(changeset.description); const id = ; const date = ; - const parents = changeset._embedded.parents.map((parent: ParentChangeset) => ( - + const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => ( + {parent.id.substring(0, 7)} )); diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index f2334c5d7e..2f91c8421b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -31,7 +31,7 @@ import { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { compose } from "redux"; -import DangerZone from "./DangerZone"; +import RepositoryDangerZone from "./RepositoryDangerZone"; import { getLinks } from "../../modules/indexResource"; import { urls } from "@scm-manager/ui-components"; @@ -80,7 +80,7 @@ class EditRepo extends React.Component { }} /> - + ); } diff --git a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx similarity index 93% rename from scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx rename to scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx index 0b2bda04b9..cb883a904f 100644 --- a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx @@ -35,7 +35,7 @@ type Props = { indexLinks: Links; }; -const DangerZoneContainer = styled.div` +export const DangerZoneContainer = styled.div` padding: 1.5rem 1rem; border: 1px solid #ff6a88; border-radius: 5px; @@ -56,7 +56,7 @@ const DangerZoneContainer = styled.div` } `; -const DangerZone: FC = ({ repository, indexLinks }) => { +const RepositoryDangerZone: FC = ({ repository, indexLinks }) => { const [t] = useTranslation("repos"); const dangerZone = []; @@ -81,4 +81,4 @@ const DangerZone: FC = ({ repository, indexLinks }) => { ); }; -export default DangerZone; +export default RepositoryDangerZone; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 467bdcbecf..f249579012 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -154,11 +154,11 @@ class RepositoryRoot extends React.Component { const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => { const baseUrl = `${url}/code/sources`; - const sourceLink = { + const sourceLink = file.newPath && { url: `${baseUrl}/${changeset.id}/${file.newPath}/`, label: t("diff.jumpToSource") }; - const targetLink = changeset._embedded?.parents?.length === 1 && { + const targetLink = file.oldPath && changeset._embedded?.parents?.length === 1 && { url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`, label: t("diff.jumpToTarget") }; @@ -166,7 +166,9 @@ class RepositoryRoot extends React.Component { const links = []; switch (file.type) { case "add": - links.push(sourceLink); + if (sourceLink) { + links.push(sourceLink); + } break; case "delete": if (targetLink) { @@ -174,17 +176,24 @@ class RepositoryRoot extends React.Component { } break; default: - if (targetLink) { + if (targetLink && sourceLink) { links.push(targetLink, sourceLink); // Target link first because its the previous file - } else { + } else if (sourceLink) { links.push(sourceLink); } } - return links.map(({ url, label }) => ); + return links ? links.map(({ url, label }) => ) : null; }; - const titleComponent = <>{repository.namespace}/{repository.name}; + const titleComponent = ( + <> + + {repository.namespace} + + /{repository.name} + + ); return ( diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java index 6cb1b1049b..96de0ce2dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java @@ -45,6 +45,6 @@ public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToD } private String createSelfLink(Repository repository, String branch) { - return resourceLinks.branch().history(repository.getNamespaceAndName(), branch); + return resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java index f640331381..bb0e93f27f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java @@ -30,7 +30,6 @@ import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import sonia.scm.repository.Branch; -import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; @@ -55,11 +54,11 @@ public class BranchCollectionToDtoMapper { public HalRepresentation map(Repository repository, Collection branches) { return new HalRepresentation( createLinks(repository), - embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches))); + embedDtos(getBranchDtoList(repository, branches))); } - public List getBranchDtoList(String namespace, String name, Collection branches) { - return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); + public List getBranchDtoList(Repository repository, Collection branches) { + return branches.stream().map(branch -> branchToDtoMapper.map(branch, repository)).collect(toList()); } private Links createLinks(Repository repository) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 58261b4d15..a5ce4d951e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -47,6 +47,7 @@ import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.validation.Valid; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -57,6 +58,7 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; import java.net.URI; +import java.util.Optional; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -132,7 +134,7 @@ public class BranchRootResource { .stream() .filter(branch -> branchName.equals(branch.getName())) .findFirst() - .map(branch -> branchToDtoMapper.map(branch, namespaceAndName)) + .map(branch -> branchToDtoMapper.map(branch, repositoryService.getRepository())) .map(Response::ok) .orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName))) .build(); @@ -247,7 +249,7 @@ public class BranchRootResource { branchCommand.from(parentName); } Branch newBranch = branchCommand.branch(branchName); - return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build(); + return Response.created(URI.create(resourceLinks.branch().self(namespace, name, newBranch.getName()))).build(); } } @@ -308,4 +310,50 @@ public class BranchRootResource { return Response.status(Response.Status.BAD_REQUEST).build(); } } + + /** + * Deletes a branch. + * + * Note: This method requires "repository" privilege. + * + * @param branch the name of the branch to delete. + */ + @DELETE + @Path("{branch}") + @Operation(summary = "Delete branch", description = "Deletes the given branch.", tags = "Repository") + @ApiResponse(responseCode = "204", description = "delete success or nothing to delete") + @ApiResponse(responseCode = "400", description = "the default branch cannot be deleted") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to modify the repository") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) + public Response delete(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("branch") String branch) { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + RepositoryPermissions.modify(repositoryService.getRepository()).check(); + + Optional branchToBeDeleted = repositoryService.getBranchesCommand().getBranches().getBranches().stream() + .filter(b -> b.getName().equalsIgnoreCase(branch)) + .findFirst(); + + if (branchToBeDeleted.isPresent()) { + if (branchToBeDeleted.get().isDefaultBranch()) { + return Response.status(400).build(); + } else { + repositoryService.getBranchCommand().delete(branch); + } + } + } catch (IOException e) { + return Response.serverError().build(); + } + return Response.noContent().build(); + } + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index f8088a79d5..f82c434b3d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -32,6 +32,8 @@ import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; @@ -46,16 +48,21 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { private ResourceLinks resourceLinks; @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); + public abstract BranchDto map(Branch branch, @Context Repository repository); @ObjectFactory - BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { + BranchDto createDto(@Context Repository repository, Branch branch) { + NamespaceAndName namespaceAndName = new NamespaceAndName(repository.getNamespace(), repository.getName()); Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.branch().self(namespaceAndName, branch.getName())) - .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build()) + .self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branch.getName())) + .single(linkBuilder("history", resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch.getName())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()) .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()); + if (!branch.isDefaultBranch() && RepositoryPermissions.modify(repository).isPermitted()) { + linksBuilder.single(linkBuilder("delete", resourceLinks.branch().delete(repository.getNamespace(), repository.getName(), branch.getName())).build()); + } + Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java index 0556b7a636..0e582309ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java @@ -56,7 +56,7 @@ class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper members; + private boolean external; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultBranchLinkProvider.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultBranchLinkProvider.java index 23e279148b..f523c31323 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultBranchLinkProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultBranchLinkProvider.java @@ -39,6 +39,6 @@ public class DefaultBranchLinkProvider implements BranchLinkProvider { @Override public String get(NamespaceAndName namespaceAndName, String branch) { - return resourceLinks.branch().self(namespaceAndName, branch); + return resourceLinks.branch().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index c05547b295..53e6d52c0a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -132,7 +132,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa } } if (repositoryService.isSupported(Command.BRANCHES)) { - embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, + embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository, getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId())))); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java index bdf14e5bd5..767a37522a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java @@ -34,20 +34,22 @@ import sonia.scm.ExceptionWithContext; import java.util.Optional; @Mapper -public abstract class ExceptionWithContextToErrorDtoMapper { +public interface ExceptionWithContextToErrorDtoMapper { @Mapping(target = "errorCode", source = "code") @Mapping(target = "transactionId", ignore = true) @Mapping(target = "violations", ignore = true) - public abstract ErrorDto map(ExceptionWithContext exception); + ErrorDto map(ExceptionWithContext exception); @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // is ok for mapping - public String mapOptional(Optional optionalString) { + default String mapOptional(Optional optionalString) { return optionalString.orElse(null); } @AfterMapping - void setTransactionId(@MappingTarget ErrorDto dto) { + default void setTransactionId(@MappingTarget ErrorDto dto) { dto.setTransactionId(MDC.get("transaction_id")); } + + ErrorDto.AdditionalMessageDto map(ExceptionWithContext.AdditionalMessage message); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index e7f4ce4e85..90ee95148d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -27,7 +27,9 @@ package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; @@ -121,7 +123,30 @@ public class GroupCollectionResource { @POST @Path("") @Consumes(VndMediaType.GROUP) - @Operation(summary = "Create group", description = "Creates a new group.", tags = "Group", operationId = "group_create") + @Operation( + summary = "Create group", + description = "Creates a new group.", + tags = "Group", + operationId = "group_create", + requestBody = @RequestBody( + content = @Content( + mediaType = VndMediaType.GROUP, + schema = @Schema(implementation = CreateGroupDto.class), + examples = { + @ExampleObject( + name = "Create an group with a description", + value = "{\n \"name\":\"manager\",\n \"description\":\"Manager group with full read access\"\n}", + summary = "Create a simple group" + ), + @ExampleObject( + name = "Create an internal group with two members", + value = "{\n \"name\":\"Admins\",\n \"description\":\"SCM-Manager admins\",\n \"external\":false,\n \"members\":[\"scmadmin\",\"c.body\"]\n}", + summary = "Create group with members" + ) + } + ) + ) + ) @ApiResponse( responseCode = "201", description = "create success", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index 9e31ecfecb..9847b0558e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -26,7 +26,9 @@ package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; @@ -104,7 +106,22 @@ public class GroupPermissionResource { @PUT @Path("") @Consumes(VndMediaType.PERMISSION_COLLECTION) - @Operation(summary = "Update Group permissions", description = "Sets permissions for a group. Overwrites all existing permissions.", tags = {"Group", "Permissions"}) + @Operation( + summary = "Update Group permissions", + description = "Sets permissions for a group. Overwrites all existing permissions.", + tags = {"Group", "Permissions"}, + requestBody = @RequestBody( + content = @Content( + mediaType = VndMediaType.PERMISSION_COLLECTION, + schema = @Schema(implementation = PermissionListDto.class), + examples = @ExampleObject( + name = "Add read permissions for all repositories and pull requests", + value = "{\n \"permissions\":[\"repository:read,pull:*\",\"repository:readPullRequest:*\"]\n}", + summary = "Simple update group permissions" + ) + ) + ) + ) @ApiResponse(responseCode = "204", description = "update success") @ApiResponse(responseCode = "400", description = "invalid body") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @@ -116,6 +133,7 @@ public class GroupPermissionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) + @ApiResponse(responseCode = "409", description = "conflict, group has been modified concurrently") @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 4d0cd1738b..f3fea99bff 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -26,7 +26,9 @@ package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; @@ -135,7 +137,22 @@ public class GroupResource { @PUT @Path("") @Consumes(VndMediaType.GROUP) - @Operation(summary = "Update group", description = "Modifies a group.", tags = "Group") + @Operation( + summary = "Update group", + description = "Modifies a group.", + tags = "Group", + requestBody = @RequestBody( + content = @Content( + mediaType = VndMediaType.GROUP, + schema = @Schema(implementation = UpdateGroupDto.class), + examples = @ExampleObject( + name = "Update a group description", + value = "{\n \"name\":\"manager\",\n \"description\":\"Group of managers with full read access\",\n \"lastModified\":\"2020-06-05T14:42:49.000Z\",\n \"type\":\"xml\"\n}", + summary = "Update a group" + ) + ) + ) + ) @ApiResponse(responseCode = "204", description = "update success") @ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/group name") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @@ -147,6 +164,7 @@ public class GroupResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) + @ApiResponse(responseCode = "409", description = "conflict, group has been modified concurrently") @ApiResponse( responseCode = "500", description = "internal server error", diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index c826ee8e5e..7bd6c353f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -24,14 +24,14 @@ package sonia.scm.api.v2.resources; -import sonia.scm.repository.NamespaceAndName; import sonia.scm.security.gpg.UserPublicKeyResource; import javax.inject.Inject; import java.net.URI; import java.net.URISyntaxException; -@SuppressWarnings("squid:S1192") // string literals should not be duplicated +@SuppressWarnings("squid:S1192") + // string literals should not be duplicated class ResourceLinks { private final ScmPathInfoStore scmPathInfoStore; @@ -273,13 +273,13 @@ class ResourceLinks { } AutoCompleteLinks autoComplete() { - return new AutoCompleteLinks (scmPathInfoStore.get()); + return new AutoCompleteLinks(scmPathInfoStore.get()); } - static class AutoCompleteLinks { + static class AutoCompleteLinks { private final LinkBuilder linkBuilder; - AutoCompleteLinks (ScmPathInfo pathInfo) { + AutoCompleteLinks(ScmPathInfo pathInfo) { linkBuilder = new LinkBuilder(pathInfo, AutoCompleteResource.class); } @@ -485,17 +485,21 @@ class ResourceLinks { branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class); } - String self(NamespaceAndName namespaceAndName, String branch) { - return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("get").parameters(branch).href(); + String self(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("get").parameters(branch).href(); } - public String history(NamespaceAndName namespaceAndName, String branch) { - return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href(); + public String history(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("history").parameters(branch).href(); } public String create(String namespace, String name) { return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href(); } + + public String delete(String namespace, String name, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("delete").parameters(branch).href(); + } } public IncomingLinks incoming() { @@ -510,11 +514,11 @@ class ResourceLinks { } public String changesets(String namespace, String name) { - return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters("source","target").href()); + return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters("source", "target").href()); } public String changesets(String namespace, String name, String source, String target) { - return incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters(source,target).href(); + return incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters(source, target).href(); } public String diff(String namespace, String name) { @@ -591,6 +595,7 @@ class ResourceLinks { ModificationsLinks(ScmPathInfo pathInfo) { modificationsLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, ModificationsRootResource.class); } + String self(String namespace, String name, String revision) { return modificationsLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("modifications").parameters().method("get").parameters(revision).href(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java index edf685ce87..0f9f267fb5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.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.api.v2.resources; import org.mapstruct.AfterMapping; @@ -32,46 +32,29 @@ import org.slf4j.MDC; import sonia.scm.ScmConstraintViolationException; import sonia.scm.ScmConstraintViolationException.ScmConstraintViolation; -import java.util.List; -import java.util.stream.Collectors; - @Mapper -public abstract class ScmViolationExceptionToErrorDtoMapper { +public interface ScmViolationExceptionToErrorDtoMapper { @Mapping(target = "errorCode", ignore = true) @Mapping(target = "transactionId", ignore = true) @Mapping(target = "context", ignore = true) - public abstract ErrorDto map(ScmConstraintViolationException exception); + ErrorDto map(ScmConstraintViolationException exception); @AfterMapping - void setTransactionId(@MappingTarget ErrorDto dto) { + default void setTransactionId(@MappingTarget ErrorDto dto) { dto.setTransactionId(MDC.get("transaction_id")); } - @AfterMapping - void mapViolations(ScmConstraintViolationException exception, @MappingTarget ErrorDto dto) { - List violations = - exception.getViolations() - .stream() - .map(this::createViolationDto) - .collect(Collectors.toList()); - dto.setViolations(violations); - } - - private ErrorDto.ConstraintViolationDto createViolationDto(ScmConstraintViolation violation) { - ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto(); - constraintViolationDto.setMessage(violation.getMessage()); - constraintViolationDto.setPath(violation.getPropertyPath()); - return constraintViolationDto; - } + @Mapping(source = "propertyPath", target = "path") + ErrorDto.ConstraintViolationDto map(ScmConstraintViolation violation); @AfterMapping - void setErrorCode(@MappingTarget ErrorDto dto) { + default void setErrorCode(@MappingTarget ErrorDto dto) { dto.setErrorCode("3zR9vPNIE1"); } @AfterMapping - void setMessage(@MappingTarget ErrorDto dto) { + default void setMessage(@MappingTarget ErrorDto dto) { dto.setMessage("input violates conditions (see violation list)"); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UpdateGroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UpdateGroupDto.java new file mode 100644 index 0000000000..d9f06eb88b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UpdateGroupDto.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.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sonia.scm.util.ValidationUtil; + +import javax.validation.constraints.Pattern; +import java.time.Instant; +import java.util.List; + +/** + * This class is currently only used in the openapi scheme + */ +@Getter +@Setter +@NoArgsConstructor +public class UpdateGroupDto { + + @Pattern(regexp = ValidationUtil.REGEX_NAME) + private String name; + private String description; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Instant lastModified; + private String type; + private List members; + private boolean external; +} diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java index ee3bdbb1e5..75acfc97d2 100644 --- a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java @@ -52,6 +52,7 @@ import java.io.OutputStream; import java.net.*; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -206,7 +207,7 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient try { DefaultAdvancedHttpResponse response = doRequest(request); span.label("status", response.getStatus()); - if (!response.isSuccessful()) { + if (isFailedRequest(request, response)) { span.failed(); } return response; @@ -219,6 +220,13 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient } } + private boolean isFailedRequest(BaseHttpRequest request, AdvancedHttpResponse responseStatus) { + if (Arrays.stream(request.getAcceptedStatus()).anyMatch(code -> code == responseStatus.getStatus())) { + return false; + } + return !responseStatus.isSuccessful(); + } + @Nonnull private DefaultAdvancedHttpResponse doRequest(BaseHttpRequest request) throws IOException { HttpURLConnection connection = openConnection(request, new URL(request.getUrl())); diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index cf2c29f21f..300fa3d17a 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -197,7 +197,11 @@ }, "CHRM7IQzo1": { "displayName": "Änderung des Repositories nicht möglich", - "description": "Die gewünschte Änderung am Repository konnte nicht durchgeführt werden. Höchst wahrscheinlich liegt dieses an installierten Plugins mit Hooks oder nativen Hooks." + "description": "Die gewünschte Änderung am Repository konnte nicht durchgeführt werden. Höchst wahrscheinlich liegt dieses an Prüfungen von Plugins. Bitte prüfen Sie die Einstellungen. Im Folgenden finden Sie weitere Meldungen zu dem Fehler:" + }, + "ASSG1ehZ01": { + "displayName": "Änderung des Repositories nicht möglich", + "description": "Die gewünschte Änderung am Repository konnte nicht durchgeführt werden. Es gab keine weiteren Meldungen." }, "thbsUFokjk": { "displayName": "Unerlaubte Änderung eines Schlüsselwerts", diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 4de2202976..8d6086ac19 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -197,7 +197,11 @@ }, "CHRM7IQzo1": { "displayName": "Could not modify repository", - "description": "The requested modification to the repository was rejected. Most probably this was due to plugins with repository hooks or native hooks." + "description": "The requested modification to the repository was rejected. The most likely reason for this are checks from plugins. Please check your settings. See the following messages for more details:" + }, + "ASSG1ehZ01": { + "displayName": "Could not modify repository", + "description": "The requested modification to the repository was rejected. There were no more messages." }, "thbsUFokjk": { "displayName": "Illegal change of an identifier", @@ -205,7 +209,7 @@ }, "40RaYIeeR1": { "displayName": "No changes were made", - "description": "No changes were made to the files of the repository. Therefor no new commit could be created. Possibly changes cannot be applied due to an .ignore-File definition." + "description": "No changes were made to the files of the repository. Therefore no new commit could be created. Possibly changes cannot be applied due to an .ignore-File definition." }, "ERS2vYb7U1": { "displayName": "Illegal change of namespace", @@ -213,7 +217,7 @@ }, "4iRct4avG1": { "displayName": "The revisions have unrelated histories", - "description": "The revisions have unrelated histories. Therefor there is no common commit to compare with." + "description": "The revisions have unrelated histories. Therefore there is no common commit to compare with." }, "65RdZ5atX1": { "displayName": "Error removing plugin files", diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 4178b0664f..7022422cea 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -24,8 +24,8 @@ package sonia.scm.api.v2.resources; -import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; @@ -56,11 +56,12 @@ import sonia.scm.web.RestDispatcher; import sonia.scm.web.VndMediaType; import javax.ws.rs.core.MediaType; +import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.time.Instant; import java.util.Date; import java.util.List; -import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -68,6 +69,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -271,6 +273,62 @@ public class BranchRootResourceTest extends RepositoryTestBase { verify(branchCommandBuilder, never()).branch(anyString()); } + @Test + public void shouldNotDeleteBranchIfNotPermitted() throws IOException, URISyntaxException { + doThrow(AuthorizationException.class).when(subject).checkPermission("repository:modify:repoId"); + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.normalBranch("suspicious", "0"))); + + MockHttpRequest request = MockHttpRequest + .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(403, response.getStatus()); + verify(branchCommandBuilder, never()).delete("suspicious"); + } + + @Test + public void shouldNotDeleteDefaultBranch() throws IOException, URISyntaxException { + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.defaultBranch("main", "0"))); + + MockHttpRequest request = MockHttpRequest + .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/main"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldDeleteBranch() throws IOException, URISyntaxException { + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.normalBranch("suspicious", "0"))); + + MockHttpRequest request = MockHttpRequest + .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(204, response.getStatus()); + verify(branchCommandBuilder).delete("suspicious"); + } + + @Test + public void shouldAnswer204IfNothingWasDeleted() throws IOException, URISyntaxException { + when(branchesCommandBuilder.getBranches()).thenReturn(new Branches()); + + MockHttpRequest request = MockHttpRequest + .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/suspicious"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(204, response.getStatus()); + verify(branchCommandBuilder, never()).delete(anyString()); + } + private Branch createBranch(String existing_branch) { return Branch.normalBranch(existing_branch, REVISION); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index f1b44bee08..e46dc6eea3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -24,28 +24,49 @@ package sonia.scm.api.v2.resources; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class BranchToBranchDtoMapperTest { - private final URI baseUri = URI.create("https://hitchhiker.com"); + private final URI baseUri = URI.create("https://hitchhiker.com/api/"); @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private Subject subject; + @InjectMocks private BranchToBranchDtoMapperImpl mapper; + @BeforeEach + void setupSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void tearDown() { + ThreadContext.unbindSubject(); + } + @Test void shouldAppendLinks() { HalEnricherRegistry registry = new HalEnricherRegistry(); @@ -59,7 +80,37 @@ class BranchToBranchDtoMapperTest { Branch branch = Branch.normalBranch("master", "42"); - BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold")); - assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master"); + BranchDto dto = mapper.map(branch, RepositoryTestData.createHeartOfGold()); + assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/HeartOfGold/master"); } + + @Test + void shouldAppendDeleteLink() { + Repository repository = RepositoryTestData.createHeartOfGold(); + when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(true); + Branch branch = Branch.normalBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://hitchhiker.com/api/v2/repositories/hitchhiker/HeartOfGold/branches/master"); + } + + @Test + void shouldNotAppendDeleteLinkIfDefaultBranch() { + Repository repository = RepositoryTestData.createHeartOfGold(); + Branch branch = Branch.defaultBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent(); + } + + @Test + void shouldNotAppendDeleteLinkIfNotPermitted() { + Repository repository = RepositoryTestData.createHeartOfGold(); + when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(false); + Branch branch = Branch.normalBranch("master", "42"); + + BranchDto dto = mapper.map(branch, repository); + assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent(); + } + } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java index 649493b7e8..e7d764f0ae 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java @@ -31,6 +31,7 @@ import sonia.scm.ExceptionWithContext; import java.util.Optional; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; class ExceptionWithContextToErrorDtoMapperTest { @@ -51,9 +52,27 @@ class ExceptionWithContextToErrorDtoMapperTest { assertThat(dto.getUrl()).isNull(); } + @Test + void shouldMapAdditionalMessages() { + ExceptionWithUrl exception = new ExceptionWithUrl(); + ErrorDto dto = mapper.map(exception); + assertThat(dto.getAdditionalMessages()) + .extracting("message") + .containsExactly("line 1", "line 2", null); + assertThat(dto.getAdditionalMessages()) + .extracting("key") + .containsExactly(null, null, "KEY"); + } + private static class ExceptionWithUrl extends ExceptionWithContext { public ExceptionWithUrl() { - super(ContextEntry.ContextBuilder.noContext(), "With Url"); + super( + ContextEntry.ContextBuilder.noContext(), + asList( + new AdditionalMessage(null, "line 1"), + new AdditionalMessage(null, "line 2"), + new AdditionalMessage("KEY", null)), + "With Url"); } @Override diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index a0b1c9b7cb..9889d4df33 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -140,13 +140,13 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectBranchUrl() { - String url = resourceLinks.branch().self(new NamespaceAndName("space", "name"), "master"); + String url = resourceLinks.branch().self("space", "name", "master"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master", url); } @Test public void shouldCreateCorrectBranchHiostoryUrl() { - String url = resourceLinks.branch().history(new NamespaceAndName("space", "name"), "master"); + String url = resourceLinks.branch().history("space", "name", "master"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master/changesets/", url); } diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java index 89004ece27..d81c806471 100644 --- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java @@ -317,6 +317,28 @@ public class DefaultAdvancedHttpClientTest verify(tracer, never()).span(anyString()); } + @Test + public void shouldNotTraceRequestIfAcceptedResponseCode() throws IOException { + when(connection.getResponseCode()).thenReturn(400); + + new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").acceptStatusCodes(400).request(); + verify(tracer).span("HTTP Request"); + verify(span).label("status", 400); + verify(span, never()).failed(); + verify(span).close(); + } + + @Test + public void shouldTraceRequestAsFailedIfAcceptedResponseCodeDoesntMatch() throws IOException { + when(connection.getResponseCode()).thenReturn(401); + + new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").acceptStatusCodes(400).request(); + verify(tracer).span("HTTP Request"); + verify(span).label("status", 401); + verify(span).failed(); + verify(span).close(); + } + //~--- set methods ---------------------------------------------------------- @@ -328,7 +350,7 @@ public class DefaultAdvancedHttpClientTest public void setUp() { configuration = new ScmConfiguration(); - transformers = new HashSet(); + transformers = new HashSet<>(); client = new TestingAdvacedHttpClient(configuration, transformers); when(tracer.span(anyString())).thenReturn(span); } diff --git a/yarn.lock b/yarn.lock index 90a978e77d..9b752f6242 100644 --- a/yarn.lock +++ b/yarn.lock @@ -542,6 +542,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" @@ -1446,6 +1453,18 @@ jest-util "^26.3.0" slash "^3.0.0" +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^26.6.2" + jest-util "^26.6.2" + slash "^3.0.0" + "@jest/core@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" @@ -1514,6 +1533,40 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" + micromatch "^4.0.2" + p-each-series "^2.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/environment@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" @@ -1534,6 +1587,16 @@ "@types/node" "*" jest-mock "^26.3.0" +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + "@jest/fake-timers@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" @@ -1555,6 +1618,18 @@ jest-mock "^26.3.0" jest-util "^26.3.0" +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + "@jest/globals@^26.4.2": version "26.4.2" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.4.2.tgz#73c2a862ac691d998889a241beb3dc9cada40d4a" @@ -1564,6 +1639,15 @@ "@jest/types" "^26.3.0" expect "^26.4.2" +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" + "@jest/reporters@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" @@ -1623,6 +1707,38 @@ optionalDependencies: node-notifier "^8.0.0" +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.4" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^4.0.3" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^7.0.0" + optionalDependencies: + node-notifier "^8.0.0" + "@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" @@ -1641,6 +1757,15 @@ graceful-fs "^4.2.4" source-map "^0.6.0" +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + "@jest/test-result@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" @@ -1660,6 +1785,16 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-sequencer@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" @@ -1681,6 +1816,17 @@ jest-runner "^26.4.2" jest-runtime "^26.4.2" +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== + dependencies: + "@jest/test-result" "^26.6.2" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + "@jest/transform@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" @@ -1724,6 +1870,27 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^26.6.2" + babel-plugin-istanbul "^6.0.0" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-regex-util "^26.0.0" + jest-util "^26.6.2" + micromatch "^4.0.2" + pirates "^4.0.1" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + "@jest/types@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" @@ -1754,6 +1921,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@lerna/add@3.21.0": version "3.21.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" @@ -3450,7 +3628,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== @@ -3888,6 +4066,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stack-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" + integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + "@types/storybook__addon-storyshots@^5.1.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@types/storybook__addon-storyshots/-/storybook__addon-storyshots-5.3.1.tgz#3bf921bcf34265d239c7417e4bbd3d66aa3d41d3" @@ -4854,6 +5037,20 @@ babel-jest@^26.3.0: graceful-fs "^4.2.4" slash "^3.0.0" +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + babel-loader@^8.0.6: version "8.1.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" @@ -4949,6 +5146,16 @@ babel-plugin-jest-hoist@^26.2.0: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.7.0, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" @@ -5177,6 +5384,24 @@ babel-preset-current-node-syntax@^0.1.3: "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +babel-preset-current-node-syntax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + babel-preset-jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" @@ -5193,6 +5418,14 @@ babel-preset-jest@^26.3.0: babel-plugin-jest-hoist "^26.2.0" babel-preset-current-node-syntax "^0.1.3" +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + "babel-preset-minify@^0.5.0 || 0.6.0-alpha.5": version "0.5.1" resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz#25f5d0bce36ec818be80338d0e594106e21eaa9f" @@ -5950,6 +6183,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -7230,6 +7468,11 @@ diff-sequences@^26.3.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -8236,6 +8479,18 @@ expect@^26.4.2: jest-message-util "^26.3.0" jest-regex-util "^26.0.0" +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -10067,6 +10322,13 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" +is-core-module@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -10606,6 +10868,15 @@ jest-changed-files@^26.3.0: execa "^4.0.0" throat "^5.0.0" +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + dependencies: + "@jest/types" "^26.6.2" + execa "^4.0.0" + throat "^5.0.0" + jest-cli@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" @@ -10644,6 +10915,25 @@ jest-cli@^26.4.2: prompts "^2.0.1" yargs "^15.3.1" +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== + dependencies: + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + is-ci "^2.0.0" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" + prompts "^2.0.1" + yargs "^15.4.1" + jest-config@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" @@ -10691,6 +10981,30 @@ jest-config@^26.4.2: micromatch "^4.0.2" pretty-format "^26.4.2" +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" + chalk "^4.0.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.6.3" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + micromatch "^4.0.2" + pretty-format "^26.6.2" + jest-diff@^24.3.0, jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" @@ -10721,6 +11035,16 @@ jest-diff@^26.4.2: jest-get-type "^26.3.0" pretty-format "^26.4.2" +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -10757,6 +11081,17 @@ jest-each@^26.4.2: jest-util "^26.3.0" pretty-format "^26.4.2" +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + jest-get-type "^26.3.0" + jest-util "^26.6.2" + pretty-format "^26.6.2" + jest-environment-jsdom@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" @@ -10782,6 +11117,19 @@ jest-environment-jsdom@^26.3.0: jest-util "^26.3.0" jsdom "^16.2.2" +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + jest-environment-node@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" @@ -10805,6 +11153,18 @@ jest-environment-node@^26.3.0: jest-mock "^26.3.0" jest-util "^26.3.0" +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" @@ -10860,6 +11220,27 @@ jest-haste-map@^26.3.0: optionalDependencies: fsevents "^2.1.2" +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== + dependencies: + "@jest/types" "^26.6.2" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + jest-regex-util "^26.0.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + micromatch "^4.0.2" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" + jest-jasmine2@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" @@ -10906,6 +11287,30 @@ jest-jasmine2@^26.4.2: pretty-format "^26.4.2" throat "^5.0.0" +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^26.6.2" + is-generator-fn "^2.0.0" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" + throat "^5.0.0" + jest-junit@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-8.0.0.tgz#d4f7ff67e292a5426dc60bc38694c9f77cb94178" @@ -10932,6 +11337,14 @@ jest-leak-detector@^26.4.2: jest-get-type "^26.3.0" pretty-format "^26.4.2" +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== + dependencies: + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-matcher-utils@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" @@ -10952,6 +11365,16 @@ jest-matcher-utils@^26.4.2: jest-get-type "^26.3.0" pretty-format "^26.4.2" +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-message-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" @@ -10980,6 +11403,21 @@ jest-message-util@^26.3.0: slash "^3.0.0" stack-utils "^2.0.2" +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + jest-mock@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" @@ -10995,6 +11433,14 @@ jest-mock@^26.3.0: "@jest/types" "^26.3.0" "@types/node" "*" +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" @@ -11028,6 +11474,15 @@ jest-resolve-dependencies@^26.4.2: jest-regex-util "^26.0.0" jest-snapshot "^26.4.2" +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== + dependencies: + "@jest/types" "^26.6.2" + jest-regex-util "^26.0.0" + jest-snapshot "^26.6.2" + jest-resolve@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" @@ -11053,6 +11508,20 @@ jest-resolve@^26.4.0: resolve "^1.17.0" slash "^3.0.0" +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-pnp-resolver "^1.2.2" + jest-util "^26.6.2" + read-pkg-up "^7.0.1" + resolve "^1.18.1" + slash "^3.0.0" + jest-runner@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" @@ -11104,6 +11573,32 @@ jest-runner@^26.4.2: source-map-support "^0.5.6" throat "^5.0.0" +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.7.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-docblock "^26.0.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" + source-map-support "^0.5.6" + throat "^5.0.0" + jest-runtime@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" @@ -11165,6 +11660,39 @@ jest-runtime@^26.4.2: strip-bom "^4.0.0" yargs "^15.3.1" +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + cjs-module-lexer "^0.6.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + yargs "^15.4.1" + jest-serializer@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" @@ -11178,6 +11706,14 @@ jest-serializer@^26.3.0: "@types/node" "*" graceful-fs "^4.2.4" +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + jest-snapshot@^24.1.0, jest-snapshot@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" @@ -11218,6 +11754,28 @@ jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: pretty-format "^26.4.2" semver "^7.3.2" +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" + jest-specific-snapshot@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-2.0.0.tgz#425fe524b25df154aa39f97fa6fe9726faaac273" @@ -11262,6 +11820,18 @@ jest-util@^26.3.0: is-ci "^2.0.0" micromatch "^4.0.2" +jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-validate@^24.0.0, jest-validate@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" @@ -11286,6 +11856,18 @@ jest-validate@^26.4.2: leven "^3.1.0" pretty-format "^26.4.2" +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.3.0" + leven "^3.1.0" + pretty-format "^26.6.2" + jest-watcher@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" @@ -11312,6 +11894,19 @@ jest-watcher@^26.3.0: jest-util "^26.3.0" string-length "^4.0.1" +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== + dependencies: + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^26.6.2" + string-length "^4.0.1" + jest-worker@^24.6.0, jest-worker@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" @@ -11337,6 +11932,15 @@ jest-worker@^26.2.1, jest-worker@^26.3.0: merge-stream "^2.0.0" supports-color "^7.0.0" +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" @@ -11345,6 +11949,15 @@ jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +jest@^26.0.0: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== + dependencies: + "@jest/core" "^26.6.3" + import-local "^3.0.2" + jest-cli "^26.6.3" + jest@^26.0.1: version "26.4.2" resolved "https://registry.yarnpkg.com/jest/-/jest-26.4.2.tgz#7e8bfb348ec33f5459adeaffc1a25d5752d9d312" @@ -11409,7 +12022,7 @@ jsdom@^11.5.1: ws "^5.2.0" xml-name-validator "^3.0.0" -jsdom@^16.2.2: +jsdom@^16.2.2, jsdom@^16.4.0: version "16.4.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== @@ -14218,6 +14831,16 @@ pretty-format@^26.4.0, pretty-format@^26.4.2: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -14757,6 +15380,11 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -14810,10 +15438,10 @@ react-redux@^5.0.7: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react-refresh@^0.8.0: - version "0.8.3" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" - integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-refresh@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" + integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== react-router-dom@^5.1.2: version "5.2.0" @@ -15494,6 +16122,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13 dependencies: path-parse "^1.0.6" +resolve@^1.18.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -17632,6 +18268,15 @@ v8-to-istanbul@^5.0.1: convert-source-map "^1.6.0" source-map "^0.7.3" +v8-to-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -18267,7 +18912,7 @@ yargs@^14.2.2: y18n "^4.0.0" yargs-parser "^15.0.1" -yargs@^15.3.1: +yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==