mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
Merge branch 'develop' into feature/delete_branches
This commit is contained in:
@@ -8,9 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Delete branches directly in the UI ([#1422](https://github.com/scm-manager/scm-manager/pull/1422))
|
- 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))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Error on repository initialization with least-privilege user ([#1414](https://github.com/scm-manager/scm-manager/pull/1414))
|
- 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))
|
||||||
|
|
||||||
|
## [2.9.1] - 2020-11-11
|
||||||
|
### Fixed
|
||||||
|
- German translation for repositories view
|
||||||
|
|
||||||
|
|
||||||
## [2.9.0] - 2020-11-06
|
## [2.9.0] - 2020-11-06
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -903,7 +903,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- test libraries -->
|
<!-- test libraries -->
|
||||||
<mockito.version>3.5.13</mockito.version>
|
<mockito.version>3.5.15</mockito.version>
|
||||||
<hamcrest.version>2.1</hamcrest.version>
|
<hamcrest.version>2.1</hamcrest.version>
|
||||||
<junit.version>5.7.0</junit.version>
|
<junit.version>5.7.0</junit.version>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class ConcurrentModificationException extends ExceptionWithContext {
|
|||||||
|
|
||||||
private static final String CODE = "2wR7UzpPG1";
|
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)));
|
this(Collections.singletonList(new ContextEntry(type, id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,16 @@ import sonia.scm.repository.NamespaceAndName;
|
|||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.util.AssertUtil;
|
import sonia.scm.util.AssertUtil;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ContextEntry {
|
public class ContextEntry implements Serializable {
|
||||||
private final String type;
|
private final String type;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
ContextEntry(Class type, String id) {
|
ContextEntry(Class<?> type, String id) {
|
||||||
this(type.getSimpleName(), id);
|
this(type.getSimpleName(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ public class ContextEntry {
|
|||||||
return this.in(Repository.class, namespaceAndName.logString());
|
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));
|
context.add(new ContextEntry(type, id));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm;
|
package sonia.scm;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -34,15 +35,26 @@ public abstract class ExceptionWithContext extends RuntimeException {
|
|||||||
private static final long serialVersionUID = 4327413456580409224L;
|
private static final long serialVersionUID = 4327413456580409224L;
|
||||||
|
|
||||||
private final List<ContextEntry> context;
|
private final List<ContextEntry> context;
|
||||||
|
private final List<AdditionalMessage> additionalMessages;
|
||||||
|
|
||||||
public ExceptionWithContext(List<ContextEntry> context, String message) {
|
protected ExceptionWithContext(List<ContextEntry> context, String message) {
|
||||||
super(message);
|
this(context, null, message);
|
||||||
this.context = context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
super(message, cause);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.additionalMessages = additionalMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ContextEntry> getContext() {
|
public List<ContextEntry> getContext() {
|
||||||
@@ -61,4 +73,26 @@ public abstract class ExceptionWithContext extends RuntimeException {
|
|||||||
public Optional<String> getUrl() {
|
public Optional<String> getUrl() {
|
||||||
return Optional.empty();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class NotFoundException extends ExceptionWithContext {
|
|||||||
|
|
||||||
private static final String CODE = "AGR7UzkhA1";
|
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)));
|
this(Collections.singletonList(new ContextEntry(type, id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ public class ErrorDto {
|
|||||||
private List<ContextEntry> context;
|
private List<ContextEntry> context;
|
||||||
private String message;
|
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")
|
@XmlElementWrapper(name = "violations")
|
||||||
private List<ConstraintViolationDto> violations;
|
private List<ConstraintViolationDto> violations;
|
||||||
|
|
||||||
@@ -53,4 +56,10 @@ public class ErrorDto {
|
|||||||
private String path;
|
private String path;
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
public static class AdditionalMessageDto {
|
||||||
|
private String key;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,5 +57,10 @@ public enum Command
|
|||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY;
|
MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.10.0
|
||||||
|
*/
|
||||||
|
LOOKUP;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -430,6 +430,20 @@ public final class RepositoryService implements Closeable {
|
|||||||
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, eMail);
|
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.
|
* Returns true if the command is supported by the repository service.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,24 +24,55 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
import sonia.scm.ContextEntry;
|
|
||||||
import sonia.scm.ExceptionWithContext;
|
import sonia.scm.ExceptionWithContext;
|
||||||
import sonia.scm.repository.Repository;
|
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 {
|
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) {
|
private static final Pattern SCM_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\] (.*)");
|
||||||
super(ContextEntry.ContextBuilder.entity(repository).build(), message);
|
|
||||||
|
public static MessageExtractor withPattern(Pattern pattern) {
|
||||||
|
return new MessageExtractor(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IntegrateChangesFromWorkdirException(Repository repository, String message, Exception cause) {
|
public static IntegrateChangesFromWorkdirException forMessage(Repository repository, String message) {
|
||||||
super(ContextEntry.ContextBuilder.entity(repository).build(), message, cause);
|
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
|
@Override
|
||||||
public String getCode() {
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -274,4 +274,12 @@ public abstract class RepositoryServiceProvider implements Closeable
|
|||||||
{
|
{
|
||||||
throw new CommandNotSupportedException(Command.MODIFY);
|
throw new CommandNotSupportedException(Command.MODIFY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.10.0
|
||||||
|
*/
|
||||||
|
public LookupCommand getLookupCommand()
|
||||||
|
{
|
||||||
|
throw new CommandNotSupportedException(Command.LOOKUP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,8 +71,10 @@ public final class GitHookMessageProvider implements HookMessageProvider
|
|||||||
@Override
|
@Override
|
||||||
public void sendMessage(String message)
|
public void sendMessage(String message)
|
||||||
{
|
{
|
||||||
|
if (!receivePack.isQuiet()) {
|
||||||
GitHooks.sendPrefixedMessage(receivePack, message);
|
GitHooks.sendPrefixedMessage(receivePack, message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import static java.util.Optional.of;
|
|||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
import static sonia.scm.NotFoundException.notFound;
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
import static sonia.scm.repository.GitUtil.getBranchIdOrCurrentHead;
|
import static sonia.scm.repository.GitUtil.getBranchIdOrCurrentHead;
|
||||||
|
import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.forMessage;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ class AbstractGitCommand {
|
|||||||
.findAny()
|
.findAny()
|
||||||
.ifPresent(remoteRefUpdate -> {
|
.ifPresent(remoteRefUpdate -> {
|
||||||
logger.info("message for failed push: {}", pushResult.getMessages());
|
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) {
|
} catch (GitAPIException e) {
|
||||||
throw new InternalRepositoryException(repository, "could not push changes into central repository", e);
|
throw new InternalRepositoryException(repository, "could not push changes into central repository", e);
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import sonia.scm.repository.api.BranchRequest;
|
|||||||
import sonia.scm.repository.work.WorkingCopy;
|
import sonia.scm.repository.work.WorkingCopy;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mercurial implementation of the {@link BranchCommand}.
|
* Mercurial implementation of the {@link BranchCommand}.
|
||||||
* Note that this creates an empty commit to "persist" the new branch.
|
* 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());
|
PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
||||||
workingCopyFactory.configure(pullCommand);
|
workingCopyFactory.configure(pullCommand);
|
||||||
pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
// TODO handle failed update
|
throw new InternalRepositoryException(getRepository(),
|
||||||
throw new IntegrateChangesFromWorkdirException(getRepository(),
|
|
||||||
String.format("Could not pull changes '%s' into central repository", branch),
|
String.format("Could not pull changes '%s' into central repository", branch),
|
||||||
e);
|
e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class HgModifyCommand implements ModifyCommand {
|
public class HgModifyCommand implements ModifyCommand {
|
||||||
|
|
||||||
|
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
|
||||||
|
|
||||||
private HgCommandContext context;
|
private HgCommandContext context;
|
||||||
private final HgWorkingCopyFactory workingCopyFactory;
|
private final HgWorkingCopyFactory workingCopyFactory;
|
||||||
|
|
||||||
@@ -114,8 +117,12 @@ public class HgModifyCommand implements ModifyCommand {
|
|||||||
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
||||||
workingCopyFactory.configure(pullCommand);
|
workingCopyFactory.configure(pullCommand);
|
||||||
return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
||||||
} catch (Exception e) {
|
} catch (ExecutionException e) {
|
||||||
throw new IntegrateChangesFromWorkdirException(context.getScmRepository(),
|
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()),
|
String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()),
|
||||||
e);
|
e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -186,4 +187,18 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase {
|
|||||||
public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() {
|
public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() {
|
||||||
hgModifyCommand.execute(new ModifyCommandRequest());
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
|
|||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.tmatesoft.svn.core.SVNCommitInfo;
|
import org.tmatesoft.svn.core.SVNCommitInfo;
|
||||||
import org.tmatesoft.svn.core.SVNDepth;
|
import org.tmatesoft.svn.core.SVNDepth;
|
||||||
|
import org.tmatesoft.svn.core.SVNErrorCode;
|
||||||
import org.tmatesoft.svn.core.SVNException;
|
import org.tmatesoft.svn.core.SVNException;
|
||||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||||
import org.tmatesoft.svn.core.wc.SVNWCClient;
|
import org.tmatesoft.svn.core.wc.SVNWCClient;
|
||||||
@@ -40,9 +41,14 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static sonia.scm.repository.spi.IntegrateChangesFromWorkdirException.withPattern;
|
||||||
|
|
||||||
public class SvnModifyCommand implements ModifyCommand {
|
public class SvnModifyCommand implements ModifyCommand {
|
||||||
|
|
||||||
|
public static final Pattern SVN_ERROR_PATTERN = Pattern.compile(".*E" + SVNErrorCode.CANCELLED.getCode() + ": (.*)");
|
||||||
|
|
||||||
private final SvnContext context;
|
private final SvnContext context;
|
||||||
private final SvnWorkingCopyFactory workingCopyFactory;
|
private final SvnWorkingCopyFactory workingCopyFactory;
|
||||||
private final Repository repository;
|
private final Repository repository;
|
||||||
@@ -81,7 +87,7 @@ public class SvnModifyCommand implements ModifyCommand {
|
|||||||
);
|
);
|
||||||
return String.valueOf(svnCommitInfo.getNewRevision());
|
return String.valueOf(svnCommitInfo.getNewRevision());
|
||||||
} catch (SVNException e) {
|
} catch (SVNException e) {
|
||||||
throw new InternalRepositoryException(repository, "could not commit changes on repository");
|
throw withPattern(SVN_ERROR_PATTERN).forMessage(repository, e.getErrorMessage().getRootErrorMessage().getFullMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
//J-
|
//J-
|
||||||
public static final Set<Command> COMMANDS = ImmutableSet.of(
|
public static final Set<Command> COMMANDS = ImmutableSet.of(
|
||||||
Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF,
|
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+
|
//J+
|
||||||
|
|
||||||
@@ -148,14 +148,21 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
return new SvnLogCommand(context);
|
return new SvnLogCommand(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ModificationsCommand getModificationsCommand() {
|
public ModificationsCommand getModificationsCommand() {
|
||||||
return new SvnModificationsCommand(context);
|
return new SvnModificationsCommand(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ModifyCommand getModifyCommand() {
|
public ModifyCommand getModifyCommand() {
|
||||||
return new SvnModifyCommand(context, workingCopyFactory);
|
return new SvnModifyCommand(context, workingCopyFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LookupCommand getLookupCommand() {
|
||||||
|
return new SvnLookupCommand(context);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
|||||||
<div className="content">
|
<div className="content">
|
||||||
<p className="subtitle">{this.renderErrorName()}</p>
|
<p className="subtitle">{this.renderErrorName()}</p>
|
||||||
<p>{this.renderErrorDescription()}</p>
|
<p>{this.renderErrorDescription()}</p>
|
||||||
|
{this.renderAdditionalMessages()}
|
||||||
<p>{this.renderViolations()}</p>
|
<p>{this.renderViolations()}</p>
|
||||||
{this.renderMetadata()}
|
{this.renderMetadata()}
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +52,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
|||||||
|
|
||||||
renderErrorName = () => {
|
renderErrorName = () => {
|
||||||
const { error, t } = this.props;
|
const { error, t } = this.props;
|
||||||
const translation = t("errors." + error.errorCode + ".displayName");
|
const translation = t(`errors.${error.errorCode}.displayName`);
|
||||||
if (translation === error.errorCode) {
|
if (translation === error.errorCode) {
|
||||||
return error.message;
|
return error.message;
|
||||||
}
|
}
|
||||||
@@ -60,13 +61,32 @@ class BackendErrorNotification extends React.Component<Props> {
|
|||||||
|
|
||||||
renderErrorDescription = () => {
|
renderErrorDescription = () => {
|
||||||
const { error, t } = this.props;
|
const { error, t } = this.props;
|
||||||
const translation = t("errors." + error.errorCode + ".description");
|
const translation = t(`errors.${error.errorCode}.description`);
|
||||||
if (translation === error.errorCode) {
|
if (translation === error.errorCode) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return translation;
|
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 = () => {
|
renderViolations = () => {
|
||||||
const { error, t } = this.props;
|
const { error, t } = this.props;
|
||||||
if (error.violations) {
|
if (error.violations) {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export type Violation = {
|
|||||||
message: string;
|
message: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
};
|
};
|
||||||
|
export type AdditionalMessage = {
|
||||||
|
key?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BackendErrorContent = {
|
export type BackendErrorContent = {
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
@@ -39,6 +43,7 @@ export type BackendErrorContent = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
context: Context;
|
context: Context;
|
||||||
violations: Violation[];
|
violations: Violation[];
|
||||||
|
additionalMessages?: AdditionalMessage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BackendError extends Error {
|
export class BackendError extends Error {
|
||||||
@@ -48,6 +53,7 @@ export class BackendError extends Error {
|
|||||||
context: Context = [];
|
context: Context = [];
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
violations: Violation[];
|
violations: Violation[];
|
||||||
|
additionalMessages?: AdditionalMessage[];
|
||||||
|
|
||||||
constructor(content: BackendErrorContent, name: string, statusCode: number) {
|
constructor(content: BackendErrorContent, name: string, statusCode: number) {
|
||||||
super(content.message);
|
super(content.message);
|
||||||
@@ -58,6 +64,7 @@ export class BackendError extends Error {
|
|||||||
this.context = content.context;
|
this.context = content.context;
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
this.violations = content.violations;
|
this.violations = content.violations;
|
||||||
|
this.additionalMessages = content.additionalMessages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,20 +34,22 @@ import sonia.scm.ExceptionWithContext;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public abstract class ExceptionWithContextToErrorDtoMapper {
|
public interface ExceptionWithContextToErrorDtoMapper {
|
||||||
|
|
||||||
@Mapping(target = "errorCode", source = "code")
|
@Mapping(target = "errorCode", source = "code")
|
||||||
@Mapping(target = "transactionId", ignore = true)
|
@Mapping(target = "transactionId", ignore = true)
|
||||||
@Mapping(target = "violations", ignore = true)
|
@Mapping(target = "violations", ignore = true)
|
||||||
public abstract ErrorDto map(ExceptionWithContext exception);
|
ErrorDto map(ExceptionWithContext exception);
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") // is ok for mapping
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") // is ok for mapping
|
||||||
public String mapOptional(Optional<String> optionalString) {
|
default String mapOptional(Optional<String> optionalString) {
|
||||||
return optionalString.orElse(null);
|
return optionalString.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterMapping
|
@AfterMapping
|
||||||
void setTransactionId(@MappingTarget ErrorDto dto) {
|
default void setTransactionId(@MappingTarget ErrorDto dto) {
|
||||||
dto.setTransactionId(MDC.get("transaction_id"));
|
dto.setTransactionId(MDC.get("transaction_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorDto.AdditionalMessageDto map(ExceptionWithContext.AdditionalMessage message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,46 +32,29 @@ import org.slf4j.MDC;
|
|||||||
import sonia.scm.ScmConstraintViolationException;
|
import sonia.scm.ScmConstraintViolationException;
|
||||||
import sonia.scm.ScmConstraintViolationException.ScmConstraintViolation;
|
import sonia.scm.ScmConstraintViolationException.ScmConstraintViolation;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public abstract class ScmViolationExceptionToErrorDtoMapper {
|
public interface ScmViolationExceptionToErrorDtoMapper {
|
||||||
|
|
||||||
@Mapping(target = "errorCode", ignore = true)
|
@Mapping(target = "errorCode", ignore = true)
|
||||||
@Mapping(target = "transactionId", ignore = true)
|
@Mapping(target = "transactionId", ignore = true)
|
||||||
@Mapping(target = "context", ignore = true)
|
@Mapping(target = "context", ignore = true)
|
||||||
public abstract ErrorDto map(ScmConstraintViolationException exception);
|
ErrorDto map(ScmConstraintViolationException exception);
|
||||||
|
|
||||||
@AfterMapping
|
@AfterMapping
|
||||||
void setTransactionId(@MappingTarget ErrorDto dto) {
|
default void setTransactionId(@MappingTarget ErrorDto dto) {
|
||||||
dto.setTransactionId(MDC.get("transaction_id"));
|
dto.setTransactionId(MDC.get("transaction_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterMapping
|
@Mapping(source = "propertyPath", target = "path")
|
||||||
void mapViolations(ScmConstraintViolationException exception, @MappingTarget ErrorDto dto) {
|
ErrorDto.ConstraintViolationDto map(ScmConstraintViolation violation);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterMapping
|
@AfterMapping
|
||||||
void setErrorCode(@MappingTarget ErrorDto dto) {
|
default void setErrorCode(@MappingTarget ErrorDto dto) {
|
||||||
dto.setErrorCode("3zR9vPNIE1");
|
dto.setErrorCode("3zR9vPNIE1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterMapping
|
@AfterMapping
|
||||||
void setMessage(@MappingTarget ErrorDto dto) {
|
default void setMessage(@MappingTarget ErrorDto dto) {
|
||||||
dto.setMessage("input violates conditions (see violation list)");
|
dto.setMessage("input violates conditions (see violation list)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,11 @@
|
|||||||
},
|
},
|
||||||
"CHRM7IQzo1": {
|
"CHRM7IQzo1": {
|
||||||
"displayName": "Änderung des Repositories nicht möglich",
|
"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": {
|
"thbsUFokjk": {
|
||||||
"displayName": "Unerlaubte Änderung eines Schlüsselwerts",
|
"displayName": "Unerlaubte Änderung eines Schlüsselwerts",
|
||||||
|
|||||||
@@ -197,7 +197,11 @@
|
|||||||
},
|
},
|
||||||
"CHRM7IQzo1": {
|
"CHRM7IQzo1": {
|
||||||
"displayName": "Could not modify repository",
|
"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": {
|
"thbsUFokjk": {
|
||||||
"displayName": "Illegal change of an identifier",
|
"displayName": "Illegal change of an identifier",
|
||||||
@@ -205,7 +209,7 @@
|
|||||||
},
|
},
|
||||||
"40RaYIeeR1": {
|
"40RaYIeeR1": {
|
||||||
"displayName": "No changes were made",
|
"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": {
|
"ERS2vYb7U1": {
|
||||||
"displayName": "Illegal change of namespace",
|
"displayName": "Illegal change of namespace",
|
||||||
@@ -213,7 +217,7 @@
|
|||||||
},
|
},
|
||||||
"4iRct4avG1": {
|
"4iRct4avG1": {
|
||||||
"displayName": "The revisions have unrelated histories",
|
"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": {
|
"65RdZ5atX1": {
|
||||||
"displayName": "Error removing plugin files",
|
"displayName": "Error removing plugin files",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import sonia.scm.ExceptionWithContext;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class ExceptionWithContextToErrorDtoMapperTest {
|
class ExceptionWithContextToErrorDtoMapperTest {
|
||||||
@@ -51,9 +52,27 @@ class ExceptionWithContextToErrorDtoMapperTest {
|
|||||||
assertThat(dto.getUrl()).isNull();
|
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 {
|
private static class ExceptionWithUrl extends ExceptionWithContext {
|
||||||
public ExceptionWithUrl() {
|
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
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user