Show messages from native scm protocol

This commit is contained in:
René Pfeuffer
2020-11-10 08:33:22 +01:00
parent b2533db684
commit 01a5dbd091
14 changed files with 202 additions and 24 deletions

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) {
this(context, null, message);
}
public ExceptionWithContext(List<ContextEntry> context, List<AdditionalMessage> additionalMessages, String message) {
super(message);
this.context = context;
this.additionalMessages = additionalMessages;
}
public ExceptionWithContext(List<ContextEntry> context, String message, Exception cause) {
this(context, null, message, cause);
}
public 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

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

@@ -24,24 +24,54 @@
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";
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;
}
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,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.
*/
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.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");
}
@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");
}
}

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

@@ -114,8 +114,10 @@ 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.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

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

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,20 @@ 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 error.additionalMessages.map(a => a.key ? t(`errors.${a.key}.description`) : a.message).map(m => <p>{m}</p>);
}
};
renderViolations = () => {
const { error, t } = this.props;
if (error.violations) {

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

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

@@ -197,7 +197,7 @@
},
"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 installierten Plugins mit Hooks oder nativen Hooks. Folgend sind eventuelle weitere Meldungen."
},
"thbsUFokjk": {
"displayName": "Unerlaubte Änderung eines Schlüsselwerts",

View File

@@ -197,7 +197,7 @@
},
"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. Most probably this was due to plugins with repository hooks or native hooks. Following are potential additional messages."
},
"thbsUFokjk": {
"displayName": "Illegal change of an identifier",

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