]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5643 Provide a default SVN SCM Provider
authorJulien HENRY <julien.henry@sonarsource.com>
Tue, 30 Sep 2014 14:49:55 +0000 (16:49 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Thu, 2 Oct 2014 15:52:23 +0000 (17:52 +0200)
21 files changed:
plugins/sonar-git-plugin/pom.xml
plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameCommand.java
plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameConsumer.java
plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java
plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitBlameCommandTest.java
plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java
plugins/sonar-svn-plugin/pom.xml [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameConsumer.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java [new file with mode: 0644]
plugins/sonar-svn-plugin/src/test/resources/blame.xml [new file with mode: 0644]
plugins/sonar-svn-plugin/test-repos/dummy-git.zip [new file with mode: 0644]
pom.xml
sonar-application/pom.xml
sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java

index 98cac3424e4a94ebbdfd38e20da13d9100d588a2..0cbd8ff467e4be329ac17258474406dd2d865d24 100644 (file)
       <artifactId>mockito-core</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.codehaus.sonar</groupId>
-      <artifactId>sonar-batch</artifactId>
-      <version>${project.version}</version>
-      <scope>test</scope>
-      <exclusions>
-        <exclusion>
-          <artifactId>sonar-deprecated</artifactId>
-          <groupId>org.codehaus.sonar</groupId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-    <dependency>
-      <groupId>org.codehaus.sonar.plugins</groupId>
-      <artifactId>sonar-xoo-plugin</artifactId>
-      <scope>test</scope>
-    </dependency>
   </dependencies>
 
   <build>
index a8c56b7ab9c208bf54c69a6f095a3ec6247b1869..9e9bb3cb8e7bd974434ad7413fcaee49ecb2b742 100644 (file)
@@ -48,10 +48,11 @@ public class GitBlameCommand implements BlameCommand, BatchComponent {
 
   @Override
   public void blame(FileSystem fs, Iterable<InputFile> files, BlameResult result) {
+    LOG.info("Working directory: " + fs.baseDir().getAbsolutePath());
     for (InputFile inputFile : files) {
       String filename = inputFile.relativePath();
       Command cl = createCommandLine(fs.baseDir(), filename);
-      GitBlameConsumer consumer = new GitBlameConsumer(LOG);
+      GitBlameConsumer consumer = new GitBlameConsumer();
       StringStreamConsumer stderr = new StringStreamConsumer();
 
       int exitCode = execute(cl, consumer, stderr);
@@ -64,9 +65,7 @@ public class GitBlameCommand implements BlameCommand, BatchComponent {
 
   public int execute(Command cl, StreamConsumer consumer, StreamConsumer stderr) {
     LOG.info("Executing: " + cl);
-    LOG.info("Working directory: " + cl.getDirectory().getAbsolutePath());
-
-    return commandExecutor.execute(cl, consumer, stderr, 10 * 1000);
+    return commandExecutor.execute(cl, consumer, stderr, 0);
   }
 
   private Command createCommandLine(File workingDirectory, String filename) {
index 1c7ef86cb98e339a407ff2104bb67a76db661738..50069a0640f0fe5fa98f457ba0acf66d9d78ca42 100644 (file)
 package org.sonar.plugins.scm.git;
 
 import com.google.common.annotations.VisibleForTesting;
-import org.slf4j.Logger;
 import org.sonar.api.batch.scm.BlameLine;
 import org.sonar.api.utils.command.StreamConsumer;
 
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-/**
- * Plain copy of package org.apache.maven.scm.provider.git.gitexe.command.blame.GitBlameConsumer
- * Patched to allow user email retrieval when parsing Git blame results.
- *
- * @Todo: hack - to be submitted as an update in maven-scm-api for a future release
- *
- * <p/>
- * For more information, see:
- * <a href="http://jira.sonarsource.com/browse/DEVACT-103">DEVACT-103</a>
- *
- * @since 1.5.1
- */
 public class GitBlameConsumer implements StreamConsumer {
 
   private static final String GIT_COMMITTER_PREFIX = "committer";
@@ -71,15 +56,6 @@ public class GitBlameConsumer implements StreamConsumer {
   private String author = null;
   private String committer = null;
   private Date time = null;
-  private Logger logger;
-
-  public Logger getLogger() {
-    return logger;
-  }
-
-  public GitBlameConsumer(Logger logger) {
-    this.logger = logger;
-  }
 
   public void consumeLine(String line) {
     if (line == null) {
@@ -123,21 +99,6 @@ public class GitBlameConsumer implements StreamConsumer {
     return false;
   }
 
-  @VisibleForTesting
-  protected String getAuthor() {
-    return author;
-  }
-
-  @VisibleForTesting
-  protected String getCommitter() {
-    return committer;
-  }
-
-  @VisibleForTesting
-  protected Date getTime() {
-    return time;
-  }
-
   private String extractEmail(String line) {
 
     int emailStartIndex = line.indexOf(OPENING_EMAIL_FIELD);
@@ -156,11 +117,6 @@ public class GitBlameConsumer implements StreamConsumer {
     // keep commitinfo for this sha-1
     commitInfo.put(revision, blameLine);
 
-    if (getLogger().isDebugEnabled()) {
-      DateFormat df = SimpleDateFormat.getDateTimeInstance();
-      getLogger().debug(author + " " + df.format(time));
-    }
-
     expectRevisionLine = true;
   }
 
index 958f838a9b048bfdb6475219470b29f6d07c395c..262fa7ecebd35acdfcdf7440f4e40709d13d94cf 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.plugins.scm.git;
 
 import com.google.common.collect.ImmutableList;
+import org.sonar.api.CoreProperties;
 import org.sonar.api.PropertyType;
 import org.sonar.api.SonarPlugin;
 import org.sonar.api.config.PropertyDefinition;
@@ -28,6 +29,7 @@ import java.util.List;
 
 public final class GitPlugin extends SonarPlugin {
 
+  static final String CATEGORY_GIT = "Git";
   static final String GIT_IMPLEMENTATION_PROP_KEY = "sonar.git.implementation";
   static final String JGIT = "jgit";
   static final String EXE = "exe";
@@ -40,10 +42,12 @@ public final class GitPlugin extends SonarPlugin {
 
       PropertyDefinition.builder(GIT_IMPLEMENTATION_PROP_KEY)
         .name("Git implementation")
-        .description("Use pure Java implementation by default but you can use command line git executable in case of issue.")
+        .description("By default pure Java implementation is used. You can force use of command line git executable in case of issue.")
         .defaultValue(JGIT)
         .type(PropertyType.SINGLE_SELECT_LIST)
         .options(EXE, JGIT)
+        .category(CoreProperties.CATEGORY_SCM)
+        .subCategory(CATEGORY_GIT)
         .build());
   }
 
index 805b0222af3894dd837dad4392667a477f036a3b..1005ac05723bbb408c907e493e11f769a6d53296 100644 (file)
@@ -105,4 +105,30 @@ public class GitBlameCommandTest {
         new BlameLine(DateUtils.parseDateTime("2011-08-05T10:49:31+0200"), "2c68c473da7fc293e12ca50f19380c5118be7ead", "simon.brandhof@gmail.com")));
   }
 
+  @Test
+  public void testExecutionError() throws IOException {
+    File source = new File(baseDir, "src/foo.xoo");
+    FileUtils.write(source, "sample content");
+    DefaultInputFile inputFile = new DefaultInputFile("foo", "src/foo.xoo").setAbsolutePath(new File(baseDir, "src/foo.xoo").getAbsolutePath());
+    fs.add(inputFile);
+
+    BlameResult result = mock(BlameResult.class);
+    CommandExecutor commandExecutor = mock(CommandExecutor.class);
+
+    when(commandExecutor.execute(any(Command.class), any(StreamConsumer.class), any(StreamConsumer.class), anyLong())).thenAnswer(new Answer<Integer>() {
+
+      @Override
+      public Integer answer(InvocationOnMock invocation) throws Throwable {
+        StreamConsumer errConsumer = (StreamConsumer) invocation.getArguments()[2];
+        errConsumer.consumeLine("My error");
+        return 1;
+      }
+    });
+
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("The git blame command [git blame --porcelain src/foo.xoo -w] failed: My error");
+
+    new GitBlameCommand(commandExecutor).blame(fs, Arrays.<InputFile>asList(inputFile), result);
+  }
+
 }
index 58aad97d84494a6aea8809ac802678d701fc547d..a750ae3ae153b3a003a77caaa0571bf56e950ffe 100644 (file)
@@ -1,3 +1,22 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
 package org.sonar.plugins.scm.git;
 
 import com.google.common.io.Closeables;
diff --git a/plugins/sonar-svn-plugin/pom.xml b/plugins/sonar-svn-plugin/pom.xml
new file mode 100644 (file)
index 0000000..6c217e8
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.codehaus.sonar</groupId>
+    <artifactId>sonar</artifactId>
+    <version>5.0-SNAPSHOT</version>
+    <relativePath>../..</relativePath>
+  </parent>
+  <groupId>org.codehaus.sonar.plugins</groupId>
+  <artifactId>sonar-svn-plugin</artifactId>
+  <name>SonarQube :: Plugins :: SVN</name>
+  <packaging>sonar-plugin</packaging>
+  <description>SVN SCM Provider.</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-plugin-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- unit tests -->
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-testing-harness</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.sonar</groupId>
+        <artifactId>sonar-packaging-maven-plugin</artifactId>
+        <configuration>
+          <pluginKey>svn</pluginKey>
+          <pluginName>SVN</pluginName>
+          <pluginClass>org.sonar.plugins.scm.svn.SvnPlugin</pluginClass>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java
new file mode 100644 (file)
index 0000000..147d964
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.scm.BlameCommand;
+import org.sonar.api.utils.command.Command;
+import org.sonar.api.utils.command.CommandExecutor;
+import org.sonar.api.utils.command.StreamConsumer;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class SvnBlameCommand implements BlameCommand, BatchComponent {
+
+  private static final Logger LOG = LoggerFactory.getLogger(SvnBlameCommand.class);
+  private final CommandExecutor commandExecutor;
+  private final SvnConfiguration configuration;
+
+  public SvnBlameCommand(SvnConfiguration configuration) {
+    this(CommandExecutor.create(), configuration);
+  }
+
+  SvnBlameCommand(CommandExecutor commandExecutor, SvnConfiguration configuration) {
+    this.commandExecutor = commandExecutor;
+    this.configuration = configuration;
+  }
+
+  @Override
+  public void blame(final FileSystem fs, Iterable<InputFile> files, final BlameResult result) {
+    LOG.info("Working directory: " + fs.baseDir().getAbsolutePath());
+    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);
+    List<Future<Void>> tasks = new ArrayList<Future<Void>>();
+    for (InputFile inputFile : files) {
+      tasks.add(submitTask(fs, result, executorService, inputFile));
+    }
+
+    for (Future<Void> task : tasks) {
+      try {
+        task.get();
+      } catch (ExecutionException e) {
+        throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause() : new IllegalStateException(e.getCause());
+      } catch (InterruptedException e) {
+        // Ignore
+      }
+    }
+  }
+
+  private Future<Void> submitTask(final FileSystem fs, final BlameResult result, ExecutorService executorService, final InputFile inputFile) {
+    return executorService.submit(new Callable<Void>() {
+      public Void call() {
+        String filename = inputFile.relativePath();
+        Command cl = createCommandLine(fs.baseDir(), filename);
+        SvnBlameConsumer consumer = new SvnBlameConsumer();
+        StringStreamConsumer stderr = new StringStreamConsumer();
+
+        int exitCode = execute(cl, consumer, stderr);
+        if (exitCode != 0) {
+          throw new IllegalStateException("The svn blame command [" + cl.toString() + "] failed: " + stderr.getOutput());
+        }
+        result.add(inputFile, consumer.getLines());
+        return null;
+      }
+    });
+  }
+
+  public int execute(Command cl, StreamConsumer consumer, StreamConsumer stderr) {
+    LOG.info("Executing: " + cl);
+    return commandExecutor.execute(cl, consumer, stderr, 0);
+  }
+
+  public Command createCommandLine(File workingDirectory, String filename) {
+    Command cl = Command.create("svn");
+    for (Entry<String, String> env : System.getenv().entrySet()) {
+      cl.setEnvironmentVariable(env.getKey(), env.getValue());
+    }
+    cl.setEnvironmentVariable("LC_MESSAGES", "en");
+
+    if (workingDirectory != null) {
+      cl.setDirectory(workingDirectory);
+    }
+    cl.addArgument("blame");
+    cl.addArgument("--xml");
+    cl.addArgument(filename);
+    cl.addArgument("--non-interactive");
+    String configDir = configuration.configDir();
+    if (configDir != null) {
+      cl.addArgument("--config-dir");
+      cl.addArgument(configDir);
+    }
+    String username = configuration.username();
+    if (username != null) {
+      cl.addArgument("--username");
+      cl.addArgument(username);
+      String password = configuration.password();
+      if (password != null) {
+        cl.addArgument("--password");
+        cl.addArgument(password);
+      }
+    }
+    if (configuration.trustServerCert()) {
+      cl.addArgument("--trust-server-cert");
+    }
+    return cl;
+  }
+
+  private static class StringStreamConsumer implements StreamConsumer {
+    private StringBuffer string = new StringBuffer();
+
+    private String ls = System.getProperty("line.separator");
+
+    @Override
+    public void consumeLine(String line) {
+      string.append(line + ls);
+    }
+
+    public String getOutput() {
+      return string.toString();
+    }
+  }
+
+}
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameConsumer.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameConsumer.java
new file mode 100644 (file)
index 0000000..525f365
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.utils.command.StreamConsumer;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SvnBlameConsumer implements StreamConsumer {
+
+  private static final Logger LOG = LoggerFactory.getLogger(SvnBlameConsumer.class);
+
+  private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+  private static final Pattern LINE_PATTERN = Pattern.compile("line-number=\"(.*)\"");
+
+  private static final Pattern REVISION_PATTERN = Pattern.compile("revision=\"(.*)\"");
+
+  private static final Pattern AUTHOR_PATTERN = Pattern.compile("<author>(.*)</author>");
+
+  private static final Pattern DATE_PATTERN = Pattern.compile("<date>(.*)T(.*)\\.(.*)Z</date>");
+
+  private SimpleDateFormat dateFormat;
+
+  private List<BlameLine> lines = new ArrayList<BlameLine>();
+
+  public SvnBlameConsumer() {
+    dateFormat = new SimpleDateFormat(SVN_TIMESTAMP_PATTERN);
+    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+  }
+
+  private int lineNumber;
+
+  private String revision;
+
+  private String author;
+
+  @Override
+  public void consumeLine(String line) {
+    Matcher matcher;
+    if ((matcher = LINE_PATTERN.matcher(line)).find()) {
+      String lineNumberStr = matcher.group(1);
+      lineNumber = Integer.parseInt(lineNumberStr);
+    }
+    else if ((matcher = REVISION_PATTERN.matcher(line)).find()) {
+      revision = matcher.group(1);
+    }
+    else if ((matcher = AUTHOR_PATTERN.matcher(line)).find()) {
+      author = matcher.group(1);
+    }
+    else if ((matcher = DATE_PATTERN.matcher(line)).find()) {
+      String date = matcher.group(1);
+      String time = matcher.group(2);
+      Date dateTime = parseDateTime(date + " " + time);
+      lines.add(new BlameLine(dateTime, revision, author));
+    }
+  }
+
+  protected Date parseDateTime(String dateTimeStr) {
+    try {
+      return dateFormat.parse(dateTimeStr);
+    } catch (ParseException e) {
+      LOG.error("skip ParseException: " + e.getMessage() + " during parsing date " + dateTimeStr, e);
+      return null;
+    }
+  }
+
+  public List<BlameLine> getLines() {
+    return lines;
+  }
+}
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java
new file mode 100644 (file)
index 0000000..79da661
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.PropertyType;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Qualifiers;
+
+import javax.annotation.CheckForNull;
+
+import java.util.List;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class SvnConfiguration implements BatchComponent {
+
+  private static final String CATEGORY_SVN = "SVN";
+  private static final String USER_PROP_KEY = "sonar.svn.username";
+  private static final String PASSWORD_PROP_KEY = "sonar.svn.password";
+  private static final String CONFIG_DIR_PROP_KEY = "sonar.svn.config_dir";
+  private static final String TRUST_SERVER_PROP_KEY = "sonar.svn.trust_server_cert";
+  private final Settings settings;
+
+  public SvnConfiguration(Settings settings) {
+    this.settings = settings;
+  }
+
+  public static List<PropertyDefinition> getProperties() {
+    return ImmutableList.of(
+      PropertyDefinition.builder(USER_PROP_KEY)
+        .name("Username")
+        .description("Username to be used for SVN authentication")
+        .type(PropertyType.STRING)
+        .onQualifiers(Qualifiers.PROJECT)
+        .category(CoreProperties.CATEGORY_SCM)
+        .subCategory(CATEGORY_SVN)
+        .index(0)
+        .build(),
+      PropertyDefinition.builder(PASSWORD_PROP_KEY)
+        .name("Password")
+        .description("Password to be used for SVN authentication")
+        .type(PropertyType.STRING)
+        .onQualifiers(Qualifiers.PROJECT)
+        .category(CoreProperties.CATEGORY_SCM)
+        .subCategory(CATEGORY_SVN)
+        .index(1)
+        .build(),
+      PropertyDefinition.builder(CONFIG_DIR_PROP_KEY)
+        .name("Configuration directory")
+        .description("Folder containing configuration files (see --config-dir)")
+        .type(PropertyType.STRING)
+        .onQualifiers(Qualifiers.PROJECT)
+        .category(CoreProperties.CATEGORY_SCM)
+        .subCategory(CATEGORY_SVN)
+        .index(2)
+        .build(),
+      PropertyDefinition.builder(TRUST_SERVER_PROP_KEY)
+        .name("Trust server certificate")
+        .description("Accept unknown SSL certificates (like self-signed)")
+        .type(PropertyType.BOOLEAN)
+        .defaultValue("false")
+        .onQualifiers(Qualifiers.PROJECT)
+        .category(CoreProperties.CATEGORY_SCM)
+        .subCategory(CATEGORY_SVN)
+        .index(3)
+        .build());
+  }
+
+  @CheckForNull
+  public String username() {
+    return settings.getString(USER_PROP_KEY);
+  }
+
+  @CheckForNull
+  public String password() {
+    return settings.getString(PASSWORD_PROP_KEY);
+  }
+
+  @CheckForNull
+  public String configDir() {
+    return settings.getString(CONFIG_DIR_PROP_KEY);
+  }
+
+  public boolean trustServerCert() {
+    return settings.getBoolean(TRUST_SERVER_PROP_KEY);
+  }
+
+}
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java
new file mode 100644 (file)
index 0000000..bf62868
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.SonarPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class SvnPlugin extends SonarPlugin {
+
+  public List getExtensions() {
+    ArrayList result = new ArrayList();
+    result.addAll(ImmutableList.of(
+      SvnScmProvider.class,
+      SvnBlameCommand.class,
+      SvnConfiguration.class));
+    result.addAll(SvnConfiguration.getProperties());
+    return result;
+  }
+}
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java
new file mode 100644 (file)
index 0000000..6e7fb3a
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import org.sonar.api.batch.scm.BlameCommand;
+import org.sonar.api.batch.scm.ScmProvider;
+
+import java.io.File;
+
+public class SvnScmProvider extends ScmProvider {
+
+  private final SvnBlameCommand blameCommand;
+
+  public SvnScmProvider(SvnBlameCommand blameCommand) {
+    this.blameCommand = blameCommand;
+  }
+
+  @Override
+  public String key() {
+    return "svn";
+  }
+
+  @Override
+  public boolean supports(File baseDir) {
+    return new File(baseDir, ".svn").exists();
+  }
+
+  @Override
+  public BlameCommand blameCommand() {
+    return blameCommand;
+  }
+}
diff --git a/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java
new file mode 100644 (file)
index 0000000..e68270d
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.plugins.scm.svn;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java
new file mode 100644 (file)
index 0000000..7b07dac
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.scm.BlameCommand.BlameResult;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.command.Command;
+import org.sonar.api.utils.command.CommandExecutor;
+import org.sonar.api.utils.command.StreamConsumer;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SvnBlameCommandTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private DefaultFileSystem fs;
+  private File baseDir;
+
+  @Before
+  public void prepare() throws IOException {
+    baseDir = temp.newFolder();
+    fs = new DefaultFileSystem();
+    fs.setBaseDir(baseDir);
+  }
+
+  @Test
+  public void testParsingOfOutput() throws IOException {
+    File source = new File(baseDir, "src/foo.xoo");
+    FileUtils.write(source, "sample content");
+    DefaultInputFile inputFile = new DefaultInputFile("foo", "src/foo.xoo").setAbsolutePath(new File(baseDir, "src/foo.xoo").getAbsolutePath());
+    fs.add(inputFile);
+
+    BlameResult result = mock(BlameResult.class);
+    CommandExecutor commandExecutor = mock(CommandExecutor.class);
+
+    when(commandExecutor.execute(any(Command.class), any(StreamConsumer.class), any(StreamConsumer.class), anyLong())).thenAnswer(new Answer<Integer>() {
+
+      @Override
+      public Integer answer(InvocationOnMock invocation) throws Throwable {
+        StreamConsumer outConsumer = (StreamConsumer) invocation.getArguments()[1];
+        List<String> lines = FileUtils.readLines(new File("src/test/resources/blame.xml"), "UTF-8");
+        for (String line : lines) {
+          outConsumer.consumeLine(line);
+        }
+        return 0;
+      }
+    });
+
+    new SvnBlameCommand(commandExecutor, mock(SvnConfiguration.class)).blame(fs, Arrays.<InputFile>asList(inputFile), result);
+    verify(result).add(inputFile,
+      Arrays.asList(
+        new BlameLine(DateUtils.parseDateTime("2009-04-18T10:29:59+0000"), "9491", "simon.brandhof"),
+        new BlameLine(DateUtils.parseDateTime("2009-04-18T10:29:59+0000"), "9491", "simon.brandhof"),
+        new BlameLine(DateUtils.parseDateTime("2009-08-31T22:32:17+0000"), "10558", "david")));
+  }
+
+  @Test
+  public void testExecutionError() throws IOException {
+    File source = new File(baseDir, "src/foo.xoo");
+    FileUtils.write(source, "sample content");
+    DefaultInputFile inputFile = new DefaultInputFile("foo", "src/foo.xoo").setAbsolutePath(new File(baseDir, "src/foo.xoo").getAbsolutePath());
+    fs.add(inputFile);
+
+    BlameResult result = mock(BlameResult.class);
+    CommandExecutor commandExecutor = mock(CommandExecutor.class);
+
+    when(commandExecutor.execute(any(Command.class), any(StreamConsumer.class), any(StreamConsumer.class), anyLong())).thenAnswer(new Answer<Integer>() {
+
+      @Override
+      public Integer answer(InvocationOnMock invocation) throws Throwable {
+        StreamConsumer errConsumer = (StreamConsumer) invocation.getArguments()[2];
+        errConsumer.consumeLine("My error");
+        return 1;
+      }
+    });
+
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("The svn blame command [svn blame --xml src/foo.xoo --non-interactive] failed: My error");
+
+    new SvnBlameCommand(commandExecutor, mock(SvnConfiguration.class)).blame(fs, Arrays.<InputFile>asList(inputFile), result);
+  }
+
+}
diff --git a/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java
new file mode 100644 (file)
index 0000000..7c8ea23
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class SvnPluginTest {
+
+  @Test
+  public void getExtensions() {
+    assertThat(new SvnPlugin().getExtensions()).hasSize(7);
+  }
+}
diff --git a/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java
new file mode 100644 (file)
index 0000000..36359e6
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.scm.svn;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class SvnScmProviderTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void sanityCheck() {
+    SvnBlameCommand blameCommand = new SvnBlameCommand(mock(SvnConfiguration.class));
+    SvnScmProvider svnScmProvider = new SvnScmProvider(blameCommand);
+    assertThat(svnScmProvider.key()).isEqualTo("svn");
+    assertThat(svnScmProvider.blameCommand()).isEqualTo(blameCommand);
+  }
+
+  @Test
+  public void testAutodetection() throws IOException {
+    File baseDirEmpty = temp.newFolder();
+    assertThat(new SvnScmProvider(null).supports(baseDirEmpty)).isFalse();
+
+    File svnBaseDir = temp.newFolder();
+    new File(svnBaseDir, ".svn").mkdir();
+    assertThat(new SvnScmProvider(null).supports(svnBaseDir)).isTrue();
+  }
+
+}
diff --git a/plugins/sonar-svn-plugin/src/test/resources/blame.xml b/plugins/sonar-svn-plugin/src/test/resources/blame.xml
new file mode 100644 (file)
index 0000000..479dee9
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<blame>
+<target
+   path="pom.xml">
+<entry
+   line-number="1">
+<commit
+   revision="9491">
+<author>simon.brandhof</author>
+<date>2009-04-18T10:29:59.077093Z</date>
+</commit>
+</entry>
+<entry
+   line-number="2">
+<commit
+   revision="9491">
+<author>simon.brandhof</author>
+<date>2009-04-18T10:29:59.077093Z</date>
+</commit>
+</entry>
+<entry
+   line-number="3">
+<commit
+   revision="10558">
+<author>david</author>
+<date>2009-08-31T22:32:17.361675Z</date>
+</commit>
+</entry>
+</target>
+</blame>
diff --git a/plugins/sonar-svn-plugin/test-repos/dummy-git.zip b/plugins/sonar-svn-plugin/test-repos/dummy-git.zip
new file mode 100644 (file)
index 0000000..e019a80
Binary files /dev/null and b/plugins/sonar-svn-plugin/test-repos/dummy-git.zip differ
diff --git a/pom.xml b/pom.xml
index 4217be58420d4879852e9a885738f88820337c05..438095f7f91598b913485cad4818663aa10a3f6a 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
     <module>plugins/sonar-l10n-en-plugin</module>
     <module>plugins/sonar-email-notifications-plugin</module>
     <module>plugins/sonar-git-plugin</module>
+    <module>plugins/sonar-svn-plugin</module>
     <module>plugins/sonar-xoo-plugin</module>
   </modules>
 
         <version>${project.version}</version>
         <type>sonar-plugin</type>
       </dependency>
+      <dependency>
+        <groupId>org.codehaus.sonar.plugins</groupId>
+        <artifactId>sonar-svn-plugin</artifactId>
+        <version>${project.version}</version>
+        <type>sonar-plugin</type>
+      </dependency>
       <dependency>
         <groupId>org.codehaus.sonar</groupId>
         <artifactId>sonar-squid</artifactId>
index 77451cd99224c513fbf23849508ba512d3ee0e07..9fc60b2fe0b582514a0c38c385646c6767560607 100644 (file)
       <type>sonar-plugin</type>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar.plugins</groupId>
+      <artifactId>sonar-svn-plugin</artifactId>
+      <type>sonar-plugin</type>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.sonatype.jsw-binaries</groupId>
       <artifactId>jsw-binaries</artifactId>
index bdb9cd4b3ef98a9bd2c099a522a037f7885435ab..4221e1abdfa9c55934fc1024a9b55f7d90760954 100644 (file)
@@ -27,7 +27,11 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.util.concurrent.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library.
@@ -52,6 +56,7 @@ public class CommandExecutor {
   /**
    * @throws org.sonar.api.utils.command.TimeoutException on timeout, since 4.4
    * @throws CommandException on any other error
+   * @param timeoutMilliseconds set it to 0 for no timeout.
    * @since 3.0
    */
   public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) {
@@ -80,7 +85,12 @@ public class CommandExecutor {
           return finalProcess.waitFor();
         }
       });
-      int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+      int exitCode;
+      if (timeoutMilliseconds == 0) {
+        exitCode = ft.get();
+      } else {
+        exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+      }
       waitUntilFinish(outputGobbler);
       waitUntilFinish(errorGobbler);
       verifyGobbler(command, outputGobbler, "stdOut");