view mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java @ 423:010d8663ac4d

TW-21384 better error message
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Thu, 10 May 2012 15:10:35 +0400
parents 45f25ca68312
children 3600b68a4c0c
line wrap: on
line source
package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;

import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.ExecResult;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.ConnectionRefusedException;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnknownFileException;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnknownRevisionException;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.vcs.VcsException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashSet;
import java.util.Set;

import static com.intellij.openapi.util.text.StringUtil.isEmpty;
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 final Logger myLogger;
  private final String myCommand;
  private final ExecResult myDelegate;
  private final Set<String> myPrivateData;

  public CommandResult(@NotNull Logger logger,
                       @NotNull String command,
                       @NotNull ExecResult execResult,
                       @NotNull Set<String> privateData) {
    myLogger = logger;
    myCommand = command;
    myDelegate = execResult;
    myPrivateData = privateData;
  }

  @NotNull
  public String getStdout() {
    return removePrivateData(myDelegate.getStdout(), myPrivateData);
  }

  public void checkCommandFailed() throws VcsException {
    checkFailure(false);
  }

  public void checkFailure(boolean failWhenStderrIsNonEmpty) throws VcsException {
    rethrowDetectedError();
    if (isFailure())
      logAndThrowError();
    String stderr = getStderr();
    if (!isEmpty(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());
    throw new VcsException(message);
  }

  private void logStderr(String stderr) {
    myLogger.warn("Error output produced by: " + getCommand());
    myLogger.warn(stderr);
  }

  @NotNull
  private String getStderr() {
    return removePrivateData(myDelegate.getStderr(), myPrivateData);
  }

  @Nullable
  private Throwable getException() {
    return myDelegate.getException();
  }

  private boolean isFailure() {
    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() {
    Throwable exception = getException();
    return exception instanceof NullPointerException;
  }

  private String createCommandLogMessage() {
    String stderr = getStderr();
    String stdout = getStdout();
    String exceptionMessage = getExceptionMessage();
    return "'" + getCommand() + "' command failed.\n" +
            (!StringUtil.isEmpty(stdout) ? "stdout: " + stdout + "\n" : "") +
            (!StringUtil.isEmpty(stderr) ? "stderr: " + stderr + "\n" : "") +
            (exceptionMessage != null ? "exception: " + exceptionMessage : "");
  }

  @Nullable
  private String getExceptionMessage() {
    Throwable exception = getException();
    if (exception == null)
      return null;
    String message = exception.getMessage();
    if (message == null)
      message = exception.getClass().getName();
    return message;
  }

  private void rethrowDetectedError() throws VcsException {
    if (!shouldDetectErrors())
      return;
    String stderr = getStderr().trim();
    checkUnrelatedRepository(stderr);
    checkUnknownRevision(stderr);
    checkFileNotUnderTheRoot(stderr);
    checkConnectionRefused(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 static Set<Integer> setOf(Integer... ints) {
    return new HashSet<Integer>(asList(ints));
  }
}