Mercurial > hg > mercurial
view mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java @ 1124:a14a2f7e74d8 development/2024.03.x tip
2024.03.x branch is created
author | Nadia Burnasheva <nadezhda.burnasheva@jetbrains.com> |
---|---|
date | Thu, 15 Feb 2024 11:33:35 +0100 |
parents | 7b244aaead2e |
children |
line wrap: on
line source
/* * Copyright 2000-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.buildServer.buildTriggers.vcs.mercurial.command; import com.intellij.execution.process.ProcessNotCreatedException; import com.intellij.openapi.diagnostic.Logger; import jetbrains.buildServer.ExecResult; import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.*; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.vcs.VcsException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.HashSet; import java.util.Set; import static com.intellij.openapi.util.text.StringUtil.isEmpty; import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces; import static java.util.Arrays.asList; import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData; /** * Mercurial command result. Filters out private data from stdout and detects errors. * * @author dmitry.neverov */ public class CommandResult { //Mercurial returns -1 in the case of errors (see dispatch.py) //and on some shells (e.g. windows cmd) it is truncated to 255. //A non-zero exit code is not always an error: //http://mercurial.selenic.com/bts/issue186 //http://mercurial.selenic.com/bts/issue2189 //e.g. pull command in hg 2.1 exits with 1 if no new changes were pulled. private static final Set<Integer> ERROR_EXIT_CODES = setOf(-1, 255); private static final String MERCURIAL_NOT_FOUND_MESSAGE_PREFIX = "Cannot run program \""; private static final String MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX1 = "No such file or directory"; private static final String MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX2 = "The system cannot find the file specified"; private final Logger myLogger; private final String myCommand; private final ExecResult myDelegate; private final Set<String> myPrivateData; private final CommandSettings mySettings; private final String myCommandWorkingDir; public CommandResult(@NotNull Logger logger, @NotNull String command, @NotNull ExecResult execResult, @NotNull Set<String> privateData, @NotNull CommandSettings settings, @Nullable String commandWorkingDir) { myLogger = logger; myCommand = command; myDelegate = execResult; myPrivateData = privateData; mySettings = settings; myCommandWorkingDir = commandWorkingDir; } public int getExitCode() { return myDelegate.getExitCode(); } @NotNull public String getSecureStdout() { return removePrivateData(myDelegate.getStdout(), myPrivateData); } @NotNull public String getRawStdout() { return myDelegate.getStdout(); } public void checkCommandFailed() throws VcsException { checkFailure(false); } public void checkFailure(boolean failWhenStderrIsNonEmpty) throws VcsException { rethrowDetectedError(); if (isFailure()) logAndThrowError(); String stderr = getSecureStderr(); if (!isEmptyOrSpaces(stderr)) { if (failWhenStderrIsNonEmpty) logAndThrowError(); else logStderr(stderr); } } private void logAndThrowError() throws VcsException { String message = createCommandLogMessage(); myLogger.warn(message); if (hasImportantException()) myLogger.error("Error during executing '" + getCommand() + "'", getException()); throwVcsException(getExceptionMessage()); } private void throwVcsException(@NotNull String message) throws VcsException { //noinspection ThrowableResultOfMethodCallIgnored Throwable e = getException(); if (isMercurialNotFoundException(e)) { assert e != null; throw new MercurialNotFoundException(getCommand(), e); } if (e != null) throw new VcsException(message, e); throw new VcsException(message); } private boolean isMercurialNotFoundException(@Nullable Throwable e) { return e instanceof ProcessNotCreatedException && e.getCause() instanceof IOException && isMercurialNotFoundErrorMessage(e.getMessage()); } private boolean isMercurialNotFoundErrorMessage(@Nullable String message) { return message != null && (message.startsWith(MERCURIAL_NOT_FOUND_MESSAGE_PREFIX) && (message.endsWith(MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX1) || message.endsWith(MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX2)) || message.startsWith("CreateProcess") && message.endsWith("error=2")); } private void logStderr(String stderr) { myLogger.warn("Error output produced by: " + getCommand()); myLogger.warn(stderr); } @NotNull private String getSecureStderr() { return removePrivateData(myDelegate.getStderr(), myPrivateData); } @NotNull private String getRawStderr() { return myDelegate.getStderr(); } @Nullable private Throwable getException() { return myDelegate.getException(); } private boolean isFailure() { //noinspection ThrowableResultOfMethodCallIgnored return getException() != null || isErrorExitCode(); } private boolean isErrorExitCode() { int exitCode = myDelegate.getExitCode(); return ERROR_EXIT_CODES.contains(exitCode); } private boolean shouldDetectErrors() { return isFailure() || myDelegate.getExitCode() != 0; } @NotNull private String getCommand() { return removePrivateData(myCommand, myPrivateData); } private boolean hasImportantException() { //noinspection ThrowableResultOfMethodCallIgnored Throwable exception = getException(); return exception instanceof NullPointerException; } @NotNull private String createCommandLogMessage() { StringBuilder message = new StringBuilder(); message.append("'"); if (!isEmpty(myCommandWorkingDir)) message.append("[").append(myCommandWorkingDir).append("] "); message.append(getCommand()).append("' command failed."); String stderr = getSecureStderr(); if (!isEmptyOrSpaces(stderr)) { int logOutputLimit = mySettings.getLogOutputLimit(); if (logOutputLimit == -1) { message.append("\nstderr:\n").append(stderr); } else { message.append(StringUtil.truncateStringValueWithDotsAtEnd(stderr, logOutputLimit)); } } return message.toString(); } private String getExceptionMessage() { StringBuilder message = new StringBuilder(); message.append("'").append(getCommand()).append("' command failed."); String stderr = getSecureStderr(); if (!isEmptyOrSpaces(stderr)) { message.append("\n"); // for security reasons, see https://youtrack.jetbrains.com/issue/TW-60220 // we don't want to add response of a web server to the message as it can be shown to a user and can expose some data int webServerOutputStart = stderr.indexOf("---%<---"); if (webServerOutputStart > 0) { stderr = stderr.substring(0, webServerOutputStart); stderr += "<see complete output of the command in teamcity-vcs.log file>"; } int limit = mySettings.getExceptionOutputLimit(); if (stderr.length() < limit || limit == -1) { message.append("stderr: ").append(stderr); } else { if (limit > 4) message.append("stderr: ").append(StringUtil.truncateStringValueWithDotsAtEnd(stderr, limit)); message.append("\nSee details in teamcity-vcs.log"); } } return message.toString(); } private void rethrowDetectedError() throws VcsException { if (!shouldDetectErrors()) return; String stderr = getRawStderr().trim(); checkUnrelatedRepository(stderr); checkUnknownRevision(stderr); checkFileNotUnderTheRoot(stderr); checkConnectionRefused(stderr); checkAbandonedTransaction(stderr); checkMergeWithWorkDirAncestor(stderr); checkNothingToMerge(stderr); checkUnknownException(stderr); checkPathIllegalComponent(stderr); } private void checkUnrelatedRepository(@NotNull final String stderr) throws UnrelatedRepositoryException { if (stderr.contains("abort: repository is unrelated")) throw new UnrelatedRepositoryException(); } private void checkUnknownRevision(@NotNull final String stderr) throws UnknownRevisionException { final String message = "abort: unknown revision '"; int idx = stderr.indexOf(message); if (idx != -1) { int startIdx = idx + message.length(); int endIdx = stderr.indexOf("'", startIdx); String revision = stderr.substring(startIdx, endIdx); throw new UnknownRevisionException(revision); } } private void checkFileNotUnderTheRoot(@NotNull final String stderr) throws VcsException { final String prefix = "abort: "; int idx = stderr.indexOf("abort: "); if (idx != -1) { int startIdx = idx + prefix.length(); int endIdx = stderr.indexOf(" not under root"); if (endIdx != -1) { String path = stderr.substring(startIdx, endIdx); throw new UnknownFileException(path); } } } private void checkConnectionRefused(@NotNull final String stderr) throws ConnectionRefusedException { if (stderr.equals("abort: error: Connection refused")) throw new ConnectionRefusedException(); } private void checkAbandonedTransaction(@NotNull final String stderr) throws AbandonedTransactionFound { if (stderr.contains("abort: abandoned transaction found") && stderr.contains("hg recover")) throw new AbandonedTransactionFound(); } private void checkMergeWithWorkDirAncestor(@NotNull final String stderr) throws MergeWithWorkingDirAncestor { if (stderr.equals("abort: merging with a working directory ancestor has no effect")) throw new MergeWithWorkingDirAncestor(); } private void checkNothingToMerge(@NotNull final String stderr) throws NothingToMergeException { if (stderr.startsWith("abort: nothing to merge")) throw new NothingToMergeException(); } private void checkUnknownException(@NotNull final String stderr) throws UnknownMercurialException { if (stderr.contains("unknown exception encountered")) throw new UnknownMercurialException(stderr); } private void checkPathIllegalComponent(@NotNull final String stderr) throws PathIllegalComponentException { final String prefix = "abort: path contains illegal component: "; int idx = stderr.indexOf(prefix); if (idx != -1) { int startIdx = idx + prefix.length(); final String component = stderr.substring(startIdx); throw new PathIllegalComponentException(component); } } private static Set<Integer> setOf(Integer... ints) { return new HashSet<>(asList(ints)); } }