Merge with develop branch

This commit is contained in:
Sebastian Sdorra
2020-11-18 10:07:28 +01:00
80 changed files with 2147 additions and 220 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

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

View File

@@ -903,7 +903,7 @@
<properties>
<!-- test libraries -->
<mockito.version>3.5.13</mockito.version>
<mockito.version>3.5.15</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<junit.version>5.7.0</junit.version>
@@ -925,8 +925,8 @@
<legman.version>1.6.2</legman.version>
<!-- webserver -->
<jetty.version>9.4.33.v20201020</jetty.version>
<jetty.maven.version>9.4.30.v20200611</jetty.maven.version>
<jetty.version>9.4.34.v20201102</jetty.version>
<jetty.maven.version>9.4.34.v20201102</jetty.maven.version>
<!-- security libraries -->
<ssp.version>1.2.0</ssp.version>

View File

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

View File

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

View File

@@ -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<ContextEntry> context;
private final List<AdditionalMessage> additionalMessages;
public ExceptionWithContext(List<ContextEntry> context, String message) {
super(message);
this.context = context;
protected ExceptionWithContext(List<ContextEntry> context, String message) {
this(context, null, message);
}
public ExceptionWithContext(List<ContextEntry> context, String message, Exception cause) {
protected ExceptionWithContext(List<ContextEntry> context, List<AdditionalMessage> additionalMessages, String message) {
super(message);
this.context = context;
this.additionalMessages = additionalMessages;
}
protected ExceptionWithContext(List<ContextEntry> context, String message, Exception cause) {
this(context, null, message, cause);
}
protected ExceptionWithContext(List<ContextEntry> context, List<AdditionalMessage> additionalMessages, String message, Exception cause) {
super(message, cause);
this.context = context;
this.additionalMessages = additionalMessages;
}
public List<ContextEntry> getContext() {
@@ -61,4 +73,26 @@ public abstract class ExceptionWithContext extends RuntimeException {
public Optional<String> getUrl() {
return Optional.empty();
}
public List<AdditionalMessage> 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;
}
}
}

View File

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

View File

@@ -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<ContextEntry> context;
private String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<AdditionalMessageDto> additionalMessages;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@XmlElementWrapper(name = "violations")
private List<ConstraintViolationDto> 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;
}
}

View File

@@ -257,6 +257,22 @@ public abstract class BaseHttpRequest<T extends 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<T extends 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<T extends 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[]{};
}

View File

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

View File

@@ -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 <T> Optional<T> lookup(Class<T> type, String... args) {
LookupCommandRequest<T> request = new LookupCommandRequest<>();
request.setType(type);
request.setArgs(args);
return lookupCommand.lookup(request);
}
}

View File

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

View File

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

View File

@@ -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.
*/
<T> Optional<T> lookup(LookupCommandRequest<T> request);
}

View File

@@ -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<T> {
private Class<T> type;
private String[] args;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <T> Optional<T> lookup(LookupCommandRequest<T> 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 <T> Optional<T> lookupProps(LookupCommandRequest<T> 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();
}
}

View File

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

View File

@@ -46,7 +46,7 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
//J-
public static final Set<Command> 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
*

View File

@@ -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<Object> 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<Object> result = command.lookup(request);
assertThat(result).isPresent();
assertThat(result.get())
.isInstanceOf(String.class)
.isEqualTo(uuid);
}
}

View File

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

View File

@@ -42,6 +42,7 @@ class BackendErrorNotification extends React.Component<Props> {
<div className="content">
<p className="subtitle">{this.renderErrorName()}</p>
<p>{this.renderErrorDescription()}</p>
{this.renderAdditionalMessages()}
<p>{this.renderViolations()}</p>
{this.renderMetadata()}
</div>
@@ -51,7 +52,7 @@ class BackendErrorNotification extends React.Component<Props> {
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<Props> {
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 (
<>
<hr />
{error.additionalMessages
.map(additionalMessage =>
additionalMessage.key ? t(`errors.${additionalMessage.key}.description`) : additionalMessage.message
)
.map(message => (
<p>{message}</p>
))}
<hr />
</>
);
}
};
renderViolations = () => {
const { error, t } = this.props;
if (error.violations) {

View File

@@ -52,7 +52,13 @@ const SplitAndReplace: FC<Props> = ({ text, replacements }) => {
if (parts.length === 0) {
return <>{parts[0]}</>;
}
return <>{parts}</>;
return (
<>
{parts.map((part, index) => (
<React.Fragment key={index}>{part}</React.Fragment>
))}
</>
);
};
export default SplitAndReplace;

View File

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

View File

@@ -62,11 +62,11 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
const footer = (
<div className="field is-grouped">
{buttons.map((button, i) => (
<p className="control">
{buttons.map((button, index) => (
<p className="control" key={index}>
<a
className={classNames("button", "is-info", button.className)}
key={i}
key={index}
onClick={() => handleClickButton(button)}
>
{button.label}

View File

@@ -176,7 +176,13 @@ const ChangesetAuthor: FC<Props> = ({ changeset }) => {
authorLine.push(...extensions);
}
return <CommaSeparatedList>{authorLine}</CommaSeparatedList>;
return (
<CommaSeparatedList>
{authorLine.map((line, index) => (
<React.Fragment key={index}>{line}</React.Fragment>
))}
</CommaSeparatedList>
);
};
export default ChangesetAuthor;

View File

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

View File

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

View File

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

View File

@@ -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."
}
},
}

View File

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

View File

@@ -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<Props> = ({ baseUrl, branch }) => {
const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
const [t] = useTranslation("repos");
let deleteButton;
if ((branch?._links?.delete as Link)?.href) {
deleteButton = (
<a className="level-item" onClick={() => onDelete(branch)}>
<span className="icon is-small">
<Icon name="trash" className="fas" title={t("branch.delete.button")} />
</span>
</a>
);
}
return (
<tr>
<td>
<Link to={to} title={branch.name}>
<ReactLink to={to} title={branch.name}>
{branch.name}
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
</Link>
</ReactLink>
</td>
<td className="is-darker">{deleteButton}</td>
</tr>
);
};

View File

@@ -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<Props> {
render() {
const { t } = this.props;
return (
const BranchTable: FC<Props> = ({ baseUrl, branches, fetchBranches }) => {
const [t] = useTranslation("repos");
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [error, setError] = useState<Error | undefined>();
const [branchToBeDeleted, setBranchToBeDeleted] = useState<Branch | undefined>();
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 <BranchRow key={index} baseUrl={baseUrl} branch={branch} onDelete={onDelete} />;
});
}
return rowContent;
};
const confirmAlert = (
<ConfirmAlert
title={t("branch.delete.confirmAlert.title")}
message={t("branch.delete.confirmAlert.message", { branch: branchToBeDeleted?.name })}
buttons={[
{
className: "is-outlined",
label: t("branch.delete.confirmAlert.submit"),
onClick: () => deleteBranch()
},
{
label: t("branch.delete.confirmAlert.cancel"),
onClick: () => abortDelete()
}
]}
close={() => abortDelete()}
/>
);
return (
<>
{showConfirmAlert && confirmAlert}
{error && <ErrorNotification error={error} />}
<table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead>
<tr>
<th>{t("branches.table.branches")}</th>
</tr>
</thead>
<tbody>{this.renderRow()}</tbody>
<tbody>{renderRow()}</tbody>
</table>
);
}
</>
);
};
renderRow() {
const { baseUrl, branches } = this.props;
let rowContent = null;
if (branches) {
rowContent = branches.map((branch, index) => {
return <BranchRow key={index} baseUrl={baseUrl} branch={branch} />;
});
}
return rowContent;
}
}
export default withTranslation("repos")(BranchTable);
export default BranchTable;

View File

@@ -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<Props> {
render() {
const { repository, branch } = this.props;
return (
<div>
<BranchDetail repository={repository} branch={branch} />
@@ -49,6 +49,7 @@ class BranchView extends React.Component<Props> {
}}
/>
</div>
<BranchDangerZone repository={repository} branch={branch} />
</div>
);
}

View File

@@ -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<Props> = ({ repository, branch }) => {
const [t] = useTranslation("repos");
const dangerZone = [];
if (branch?._links?.delete) {
dangerZone.push(<DeleteBranch repository={repository} branch={branch} key={dangerZone.length} />);
}
if (dangerZone.length === 0) {
return null;
}
return (
<>
<hr />
<Subtitle subtitle={t("branch.dangerZone")} />
<DangerZoneContainer>{dangerZone}</DangerZoneContainer>
</>
);
};
export default BranchDangerZone;

View File

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

View File

@@ -81,10 +81,10 @@ class BranchesOverview extends React.Component<Props> {
}
renderBranchesTable() {
const { baseUrl, branches, t } = this.props;
const { baseUrl, branches, repository, fetchBranches, t } = this.props;
if (branches && branches.length > 0) {
orderBranches(branches);
return <BranchTable baseUrl={baseUrl} branches={branches} />;
return <BranchTable baseUrl={baseUrl} branches={branches} fetchBranches={() => fetchBranches(repository)} />;
}
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
}

View File

@@ -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<Props> = ({ repository, branch }: Props) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [error, setError] = useState<Error | undefined>();
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 = (
<ConfirmAlert
title={t("branch.delete.confirmAlert.title")}
message={t("branch.delete.confirmAlert.message", { branch: branch.name })}
buttons={[
{
className: "is-outlined",
label: t("branch.delete.confirmAlert.submit"),
onClick: () => deleteBranch()
},
{
label: t("branch.delete.confirmAlert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
<>
<ErrorNotification error={error} />
{showConfirmAlert && confirmAlert}
<Level
left={
<p>
<strong>{t("branch.delete.subtitle")}</strong>
<br />
{t("branch.delete.description")}
</p>
}
right={<DeleteButton label={t("branch.delete.button")} action={() => setShowConfirmAlert(true)} />}
/>
</>
);
};
export default DeleteBranch;

View File

@@ -174,8 +174,8 @@ class ChangesetDetails extends React.Component<Props, State> {
const description = changesets.parseDescription(changeset.description);
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
const date = <DateFromNow date={changeset.date} />;
const parents = changeset._embedded.parents.map((parent: ParentChangeset) => (
<ReactLink title={parent.id} to={parent.id}>
const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => (
<ReactLink title={parent.id} to={parent.id} key={index}>
{parent.id.substring(0, 7)}
</ReactLink>
));

View File

@@ -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<Props> {
}}
/>
<ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} />
<DangerZone repository={repository} indexLinks={indexLinks} />
<RepositoryDangerZone repository={repository} indexLinks={indexLinks} />
</>
);
}

View File

@@ -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<Props> = ({ repository, indexLinks }) => {
const RepositoryDangerZone: FC<Props> = ({ repository, indexLinks }) => {
const [t] = useTranslation("repos");
const dangerZone = [];
@@ -81,4 +81,4 @@ const DangerZone: FC<Props> = ({ repository, indexLinks }) => {
);
};
export default DangerZone;
export default RepositoryDangerZone;

View File

@@ -154,11 +154,11 @@ class RepositoryRoot extends React.Component<Props> {
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<Props> {
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<Props> {
}
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 }) => <JumpToFileButton tooltip={label} link={url} />);
return links ? links.map(({ url, label }) => <JumpToFileButton tooltip={label} link={url} />) : null;
};
const titleComponent = <><Link to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>{repository.namespace}</Link>/{repository.name}</>;
const titleComponent = (
<>
<Link to={`/repos/${repository.namespace}/`} className={"has-text-dark"}>
{repository.namespace}
</Link>
/{repository.name}
</>
);
return (
<StateMenuContextProvider>

View File

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

View File

@@ -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<Branch> branches) {
return new HalRepresentation(
createLinks(repository),
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches)));
embedDtos(getBranchDtoList(repository, branches)));
}
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
public List<BranchDto> getBranchDtoList(Repository repository, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, repository)).collect(toList());
}
private Links createLinks(Repository repository) {

View File

@@ -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.
*
* <strong>Note:</strong> 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<Branch> 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();
}
}

View File

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

View File

@@ -56,7 +56,7 @@ class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper<Chan
private BranchReferenceDto createBranchReferenceDto(Repository repository, String branchName) {
BranchReferenceDto branchReferenceDto = new BranchReferenceDto();
branchReferenceDto.setName(branchName);
branchReferenceDto.add(Links.linkingTo().self(resourceLinks.branch().self(repository.getNamespaceAndName(), branchName)).build());
branchReferenceDto.add(Links.linkingTo().self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branchName)).build());
return branchReferenceDto;
}
}

View File

@@ -0,0 +1,49 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;
import java.util.List;
/**
* This class is currently only used in the openapi scheme
*/
@Getter
@Setter
@NoArgsConstructor
public class CreateGroupDto {
@Pattern(regexp = ValidationUtil.REGEX_NAME)
private String name;
private String description;
private String type;
private List<String> members;
private boolean external;
}

View File

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

View File

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

View File

@@ -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<String> optionalString) {
default String mapOptional(Optional<String> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String> members;
private boolean external;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ContentTransformer>();
transformers = new HashSet<>();
client = new TestingAdvacedHttpClient(configuration, transformers);
when(tracer.span(anyString())).thenReturn(span);
}

659
yarn.lock
View File

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