From 2d60412e140a950436349f80f928c0e3a073c287 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Tue, 30 Sep 2014 16:49:55 +0200 Subject: [PATCH] SONAR-5643 Provide a default SVN SCM Provider --- plugins/sonar-git-plugin/pom.xml | 17 -- .../plugins/scm/git/GitBlameCommand.java | 7 +- .../plugins/scm/git/GitBlameConsumer.java | 44 ----- .../org/sonar/plugins/scm/git/GitPlugin.java | 6 +- .../plugins/scm/git/GitBlameCommandTest.java | 26 +++ .../plugins/scm/git/JGitBlameCommandTest.java | 19 +++ plugins/sonar-svn-plugin/pom.xml | 60 +++++++ .../plugins/scm/svn/SvnBlameCommand.java | 152 ++++++++++++++++++ .../plugins/scm/svn/SvnBlameConsumer.java | 117 ++++++++++++++ .../plugins/scm/svn/SvnConfiguration.java | 109 +++++++++++++ .../org/sonar/plugins/scm/svn/SvnPlugin.java | 39 +++++ .../sonar/plugins/scm/svn/SvnScmProvider.java | 49 ++++++ .../sonar/plugins/scm/svn/package-info.java | 25 +++ .../plugins/scm/svn/SvnBlameCommandTest.java | 126 +++++++++++++++ .../sonar/plugins/scm/svn/SvnPluginTest.java | 32 ++++ .../plugins/scm/svn/SvnScmProviderTest.java | 58 +++++++ .../src/test/resources/blame.xml | 30 ++++ .../sonar-svn-plugin/test-repos/dummy-git.zip | Bin 0 -> 51878 bytes pom.xml | 7 + sonar-application/pom.xml | 6 + .../api/utils/command/CommandExecutor.java | 14 +- 21 files changed, 875 insertions(+), 68 deletions(-) create mode 100644 plugins/sonar-svn-plugin/pom.xml create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameConsumer.java create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java create mode 100644 plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java create mode 100644 plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java create mode 100644 plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java create mode 100644 plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java create mode 100644 plugins/sonar-svn-plugin/src/test/resources/blame.xml create mode 100644 plugins/sonar-svn-plugin/test-repos/dummy-git.zip diff --git a/plugins/sonar-git-plugin/pom.xml b/plugins/sonar-git-plugin/pom.xml index 98cac3424e4..0cbd8ff467e 100644 --- a/plugins/sonar-git-plugin/pom.xml +++ b/plugins/sonar-git-plugin/pom.xml @@ -47,23 +47,6 @@ mockito-core test - - org.codehaus.sonar - sonar-batch - ${project.version} - test - - - sonar-deprecated - org.codehaus.sonar - - - - - org.codehaus.sonar.plugins - sonar-xoo-plugin - test - diff --git a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameCommand.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameCommand.java index a8c56b7ab9c..9e9bb3cb8e7 100644 --- a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameCommand.java +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameCommand.java @@ -48,10 +48,11 @@ public class GitBlameCommand implements BlameCommand, BatchComponent { @Override public void blame(FileSystem fs, Iterable 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) { diff --git a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameConsumer.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameConsumer.java index 1c7ef86cb98..50069a0640f 100644 --- a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameConsumer.java +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitBlameConsumer.java @@ -20,30 +20,15 @@ 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 - * - *

- * For more information, see: - * DEVACT-103 - * - * @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; } diff --git a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java index 958f838a9b0..262fa7ecebd 100644 --- a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java @@ -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()); } diff --git a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitBlameCommandTest.java b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitBlameCommandTest.java index 805b0222af3..1005ac05723 100644 --- a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitBlameCommandTest.java +++ b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitBlameCommandTest.java @@ -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() { + + @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.asList(inputFile), result); + } + } diff --git a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java index 58aad97d844..a750ae3ae15 100644 --- a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java +++ b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java @@ -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 index 00000000000..6c217e89640 --- /dev/null +++ b/plugins/sonar-svn-plugin/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.codehaus.sonar + sonar + 5.0-SNAPSHOT + ../.. + + org.codehaus.sonar.plugins + sonar-svn-plugin + SonarQube :: Plugins :: SVN + sonar-plugin + SVN SCM Provider. + + + + com.google.code.findbugs + jsr305 + provided + + + org.codehaus.sonar + sonar-plugin-api + provided + + + + + org.codehaus.sonar + sonar-testing-harness + test + + + org.hamcrest + hamcrest-all + test + + + + org.mockito + mockito-core + test + + + + + + + org.codehaus.sonar + sonar-packaging-maven-plugin + + svn + SVN + org.sonar.plugins.scm.svn.SvnPlugin + + + + + 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 index 00000000000..147d9644621 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameCommand.java @@ -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 files, final BlameResult result) { + LOG.info("Working directory: " + fs.baseDir().getAbsolutePath()); + ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1); + List> tasks = new ArrayList>(); + for (InputFile inputFile : files) { + tasks.add(submitTask(fs, result, executorService, inputFile)); + } + + for (Future 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 submitTask(final FileSystem fs, final BlameResult result, ExecutorService executorService, final InputFile inputFile) { + return executorService.submit(new Callable() { + 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 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 index 00000000000..525f3651048 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnBlameConsumer.java @@ -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("(.*)"); + + private static final Pattern DATE_PATTERN = Pattern.compile("(.*)T(.*)\\.(.*)Z"); + + private SimpleDateFormat dateFormat; + + private List lines = new ArrayList(); + + 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 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 index 00000000000..79da661fe83 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnConfiguration.java @@ -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 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 index 00000000000..bf628688eb7 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnPlugin.java @@ -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 index 00000000000..6e7fb3ac8a4 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/SvnScmProvider.java @@ -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 index 00000000000..e68270ddbe0 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/main/java/org/sonar/plugins/scm/svn/package-info.java @@ -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 index 00000000000..7b07dac96ce --- /dev/null +++ b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnBlameCommandTest.java @@ -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() { + + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StreamConsumer outConsumer = (StreamConsumer) invocation.getArguments()[1]; + List 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.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() { + + @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.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 index 00000000000..7c8ea23a91a --- /dev/null +++ b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnPluginTest.java @@ -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 index 00000000000..36359e68f41 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/test/java/org/sonar/plugins/scm/svn/SvnScmProviderTest.java @@ -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 index 00000000000..479dee9c692 --- /dev/null +++ b/plugins/sonar-svn-plugin/src/test/resources/blame.xml @@ -0,0 +1,30 @@ + + + + + +simon.brandhof +2009-04-18T10:29:59.077093Z + + + + +simon.brandhof +2009-04-18T10:29:59.077093Z + + + + +david +2009-08-31T22:32:17.361675Z + + + + 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 index 0000000000000000000000000000000000000000..e019a80dee2eda254083a3516e862b28c59fefe7 GIT binary patch literal 51878 zcmeFZbx>7%)INRyK}02#6j4A@iPM1+iqc58#7RPs?o>*VmX?rIT2dqh1Ox#UkS;+) zB?Sc~zZ-p#o8Eih_pjf~kD1NjIA?wKT6?W$JuCK6RixNQ1KRm7u-*vWD#Lee8-lsL-L{f+Mo3;aTkio`2@o+tkn ze%;|#1t`KK`PB8)Ej?k+8rhfL-^h0iws&i&+xt&S0=Fd(T*v-=YXqV_-p<^{!QA@V z&i#GV-@Ctq9nS%UmDL0kgrA9a3bl&nlxoW`1Ay)Tr7MC64hen|e|NjtU&1pfT_3)`DnTVJ*R?o>N|v1|L^+sI@!LYIaN1bPZw zr~iC&KSW?{Y%E=Eunu@LVJmx65qqqajV1AqHuDS@S80?w6ks~96Ic3?T>fdBCJ#>! zlcQ{3PpjTz(Ik#C%1>jDuO66tYEk2r+-EfW==nmu9c&o1 zdE$9%LPB)(IP$1_vHigDCAZiV?trgKu6|Ln^UmsNlv;*`g&cHR>6#Q2@*P>ED8bX` znID9y3XdU&!umSg4a|m(hNpRT^Lsc>6s@C5H{9GF#LRp&5YIjNdkcT~+D5+$Sl5RF z1mgQU3*)V=tjryLW!*?)>uxY(Q_5WxTe&k`+REumH%k?Kx%&^^(Otdx;VmEPR@u`| z%fL_fJ)8?Ta+vr&9l6xEy45M(a-!}TtvazhwD}clEX`--QbXwnWwn7FA+yI5WsEeu z=yZ-zKA^}5yRCaj1sgRr#I{=6dV;6b4PyjbhUJ)8Yd_7NKn~ue zJsp`O$@b(tV>smj<3hOzh7R&TukeqD92->EO6f~3m*gY&UG6?iW`nMO*YV}L z6&>v2mW8{sYt^RB=7)KU<@rfwS>1URrXzRfoV%wyYVHb8-V9X74A?waszy5-^=Y}oWJpP zn9F`qdd_ItE{DkR@wxo>)AOCxR-%n@OD2kD(zgw$PL@XV;~Dg@cd{mI&Z~)J9J-Kd zKE;N(&CB9>DhP)PsMb9LT^OR)Y16(qvZhgc5Z$edHI7VLD?1in?tQe|`6xX4%QaCL z2IWcZvFImjw~^DSJEI(H7qv+Cd-aa<8eld7)k1%TMK*Rs;XOS18x1|fAS#VudVkZ6 zGbhYnQ+C~qKJWvJF1ux`gm}hyrV8HN4%N^vDypqouXVpYlr{B|V9HFoJ`GC93!|vN zbv(T4saom{I)&jB8wc%rw({stiI4m)SDR`LGD*4L2(d+9%1x3^dD_TKniHm5j4B!F zUdtP-%x!E%&j!UZ$!Z63rfy~MJhN~VDligcGCzTtJy>v@MK&_7|FmKD_z74&RmU=3 zkN3jJmzvv}QuXDT$(3EWm9bON&x|9Jr{nJh-TGL^_j)i6v6AQL(1uizS!spr{q{Kn|b{q`^=rR8At}8=K<9(;Ah9E zKOpGu4612pJtRZ#f%I;YLC&($3+kyJ8uC9(ea&YdcrE=vpH@>iBayCvo;HzG#VWXzO`o&ti%7ctTR{iE>=~lwz z<|=8Wo@+)>t#T_zYN3Og6)eh)k!>8xy<)-Je5J2b`S9`;j$_3KYB}BVRt+tNP)Axf zf}PJ zYcib|nYOP<)mQ!^m^V;m?(3xwUte@(*e8bV3li!M;y%@KxB6%&Z0Z|2if%!tu4_bT zi9}k`6Wi#xi2~C=Bl|Rk*06!kWClO@y863 zq0aM++&g-b=d2E4qn*icI7tLITvIZt(RcG+-qi!_A(lmANa&jpUE%G63!*ot4m~!PE8)EvrU)sbF?+|1TXU}~z3&iKo)F;w?0;D$9pqJVoi0B+ zFeWeUNOTI?$)Q}<@%pzg)rTzo8Kf2sx7^0~uuI(Di|d2<9y^6NPRHKs*RMH5g=LOe z`C7SE#-+F>d>Q4<-R~^HTc{je8du>t=RYY9azRNkAdUx638X&>ER|1UJeu$7?`;6+SSXamm?o(zY}}=w5}d(`KDmMmJh$-vPIZ1iSRj zQNzGHjTD_}UmrKWALXa$&T3>Io5;!txe#U@;XB;7FPlQ&@}Ap`QhJwV4ZXDOM>b2F zikGWfSwvTPy59NW46e>fe7iAudc$E+HH{E`L!7(4@rVaBT(bekjZ zeSK(YT9;?{{~kt##zbNV_Jcq=NB<$>?T9$6{of^tOx>&YL(NQ^?VC&}MuP~ElSd>S z^sf@agiMd{TR|r+5EASuHWU@Z|St z4e4rEd#otHe(}SAJ)LjIK=F$OC04KaY9uI~Q#RByjS|`j(c;sHL3Z8f8_qWAieh_rxUu0GRIkxAhP~RbOO;*y z(o}h=UM}?9yVcRELDm&GIP_QvwlMlqwDgR=wNPSzc(>IRhiKl2{d|wh9C#QA@r{F1 z%-l3YZPmlclj@#!Eg?jSpfgifTAn~=l?hhyk^wyS;YF_;YxhOu+so>;EItUxaf>i@ zIxG(EH!n$|VMs|T(XOBHomk!)FBL70N+jo7^G%mX>b}CcFY~>tbLYBDrc-$U;VgUm z`h_WN#KJW;aVG;Ihh*L-`U+#-?w{6T<`*u7U6wnq~v*6CU-ykOy98@&C7h2 zXY7OEN4c*%f}5_s_r2tB$mN^eiY1Smsp*YvPecCz7OIdVfSpt5O)%Q9y}X&C3Ps#KA{0b;HLdaKNrH*=%ZT2*G)kR^OPNwq*NT_; zh660ga*lG=r@}!7)8*5Zn$&)2bk{9SyXI(F?u;!iM&x<<$z8QJnFx}wY-hXiQMX#n zf9qL&!hH2DHl=e9#rr=UTP*aFx^l^5JT#Iv5@&_}#Pu)c~_2Pu`%6DcZ zy3NfO$|`)lQlERcHy%RWcds94nvlMeQa`PGfzE;MRr82b|Fp4*wdDT(p`4li`Y(N+ zU~6uU#*MrLe9S1!>V zIHZz>oqfM>qvYrl=6f}IRv{@7(NK&sBZss~QOMK{U0RV*uhkdM{C%dMT(LUG-D8}(=gks?Ii(#9U+Qg7_*ZX~2OwBAsA*XdH4?A|VGprX>0v2qCw$}{rtf{OkcYm!|I~D%))%G`rw6k5CGB{$6b{SmJ z-6o-sSHJH3fNZQ>gLOBHSz22zK;TmcXAdTLw6hFLsYG#ZWtoj!B-tR=*_t7>NO01i}C z-S3E)4aUA!-M_|b_p!(Bqk-`1fVzKzUJqNsg|iUY=xxsahfM8-8LhDS0jacV4d!*; zIFyQzf@;XiCyKl+P1a~5pOpM~md@A^TX56;?$r}YdZy5me(Jn!`sU@e=P%LG^tQ9m zOoE!9JUyFvdgUqn>d}GV>kJJm2P?BqA`Fc^!!CuNVkLJjj+qJ~OZQdGD?0xQ?H4^K zf-|ESr8vJYR9Q*eR`ScehKYO$*PEf&B}an z>l}1G9v&Az$MMQRjJGsGJj1r`3-&GdM~6WZu=-a~#+O0T<;LC~pifh#hK9uF93EoY ztgVZy?eYau#yTAs{s5X!hg#TLVn@*Br0&cJ>YjFDOAj;Xafms4e4_qDD;moG(cn<2%v-N9{-T^$$?7oE-{5{FLluhlIz z!7j|(6a=+Es%g-N^0N+Se!Xkg5@VOl(&e9alZj?_)ouLMoz}O`bqt@A@yx4F-)-Ny znl-WiNaBX!%ck>b1EVDr@TB`2%A8iKnK~+hrb*W8uG@YO6(23~8A+rCJHXZ|pT&o* zc^`d(4DX2HeA>h+V&iYTrZGCncSQf2e<6!yOcW{Q^FU*JSt^ z&!(jwd*cU{&xQ_JA$gtfE4Qt`nX(#lr|O{Y^vpc|tp1o{c2y?en6}lmCX2$EfWsTm zwxLZApHqC>ZZ-^J$)}47*dMD;ygu5bbMD^l92rTe84~@;bDbX4uRhmt9nZRXQib6% z*G;iiO3jtH!RV?<&J|`p>yepbFmDT`bJtZGEkhI<`d91T!Ij6K=PyeBo-DAC*VM+6 zgFt=zKp>I7Eh+7YHrSsf!Y{fMjv9vc-C%kTzPgEUn6jkM-3EiRb4r4TKKpmRT#PZ# z?Kd^hjJP7771bIx&4c0M;JmYcDr#hCNSs*sJhLZrhHrq!_u%2L8hLL1FWHts1;xdy z}>kCo43q~~@i+S-xzf##7x5(qkqNblmuXOZ9h~{+# zm-l`LnHs|KPT^@o)Y{JRHv42lvLj@y&5rmzJZ1LoNqoJwE~vcpFOhZe)q=?GbhV_eaSFzS~w%f@Dh(V`pUb~onx&(Gq_^I9rz8g*tzGe1WQj8Ik{ z7m{Je@y46@gp4j-e5zAGY~lf1+Hg8%QbB!um~HPwu(w!dIJVmKj9zEkFcYkTSE>~+ z&HIs+E!LyYFAqAMa_>>tjI2^Twg2vN`|;!NHpsO?VgT%-6de%FhTOpY?wJ5x#<+!+-);>n%D8rxc_2Bbo9%Mlhv9*j7`z0x?H>$j1_IOO}Q;Y zxSm@uGV1cmq3+gp++SeDK(7-VyrlSj>}-${Whe5lH=i!pjt_f zOAS??nA^o{C$AIF@BE>Q!lYiE?*zCzJJ1j3{yUfdWdkK&N6mT&N$*we)ue@-7<@Rz zCGkmkS1^%8!mXJCBXL8gagp z9ey;4YUG~cN#0L|mPPI8Oa<}CQ2Qb{fl(R8Bx#{9&zA{pUu0uL_C>w5IDh3>@!K8|{5ce|xOJG=xRPqnhvuFLxEIj{D|O=5w?5i8=ScjXReI26TzAx^R!Vm&1Z) zBn=-(_DkAV)ZCXF=6D*C1!;7ivvK-I9IaA=GS~|>&-?mE_b~WmJ9dyArytQ1*T{r+ z@|a(P=WAS3PhP03WY&|>xO|xo>F;NL#nH{KR#TrfnuXz&C}oFBgLZt;G-ZDDEc;Cv zWg%FyZDZ;>^$};fH=7K>I@fg{#LERua>kWiKB%obbXo2RS~1);$i6dJ6`I55_)*Ho zw^qfTZ}Riw`Sqa2FJPGfzbpJq}T6c$xtY$zNA!%k8}yflVy$zT^AsW{U; zO4*|R@I3Ppk1uVQMpQ@sZQ0t$6N-9s1sTGN6;!8aKCHd1K9aGjYV?39KOTPSQPAtQ z-UW-t7hM#5JL6uDF=mYldiogjeXd{k^>=>fraH1g^D&X~7PZB-s0eEFyp4cZfW9d> z4OjPBc8b#Kjv(}>>K!AUCM!PHJjxe#x?f?Iz2tiWM(xz^rBvzVOkaAyO{&tf49niM zey*`Xm*$#SBc#5g>H6WsQzv;(^JhPCR5=X~G>OlxbG4jhhFkQ^#c6EMrfkKe-WlAq zp?|7mf4p~}G@};%sN!>9z5L<^u1#64i)~SKfzoTUO&?h|p323qc=}W=T_e@myRzAI zOdQ^ycs5JdE`%ezh!CGYtl^O+P`oYk-qEwC_{_04GFf5#WkGemk7)vmuB`_Gb1Dc^_V*rlF_Njs$x|b~JWyrgY?HL3n;Rd!nky!13%|J$k#=cRuu1 zj?uFpICD6jr^*a_xOogK=zqQWTDmB^F8z8%K403&HqpheoC-Si%(%0Kj!}1U*7q{* z99?=*!+-9JonOfWqv6q7vq`=lWG*`|>|tf@8Qm=%wwf-)NfBMHG0BuEeB-!uTJ3g8jltE+ z^DQCl6)f+)!_@OL+2t~hEY*2fYFvmazZ7`XIenm0yz+2GZ^pWoT&hO3;fR0txc6S- z-Wlj^au1qc01`I`(A#7DGm4pCBM@D7Ml?G!yxlpT{ij2ABoh-8a$GN{Gnpurz~xi+ zp8O_h->n73(UvDKEA~BGr(rxohRfD`PU~M4X(#D@N&W~PyGtEbO_ra7mhWFz% zOXke|sCVzQKc0Oz6-&rqe(ZyG^_nWR)>+3WX?BIJwN~p!CurFQhn#02sK{H` z9+24f15Gh#2kd)XN!}4n1Z#ocR(hsAcd36{8_nWex4b|c*;>$TSg4XZ#TLvITroTE zXjRfLG53ydp6=7oM@=*->guX*^;465J!bmVF8V!bwnjU;ynp1*s_7FYM$L(?z`j4{>149 z&qA-}P&=y5@x~apXkV=LxEo!smleXeaN%scy zJDbCvCC>V=M6WzM#~JD4sMl`OVdX zSeXCbq5Cnnjrzmf_Al&aX>GbUw(a|$k@@eV{|lEuP%1DcF{K27SpRId?-F(g%R7VZ zT?ELF>Il+=(986#Xb?o@A{DeE&p*A+>^Q@&sjk)0RO^}PO*Q;=mc)n(3D%3#qKWhf z)R3=EVGrfbKL2VT!^0*%M4wEyt58uaJx@SS$lamkZtnS0CmpIPA1z&}k=)1`P5ivp zNh!y`MW($vD7#?%#mX>t#&_LAzu@4>kp(vthuq;fc1~aD(-YLsG$Zn~)!X9f-A=c4 z6Ex?atvqtvoL`F&jU92&m+!spXdIz;A>#N2Dc_=zgRwmL82kMRPaEjx{l$aOlZ78e zJRKh`+oCwkitlgTU^ip>J!{uXCm`mgh_W1GE;lx4U2oYaLNTiCNRm-Y`< ziVNTOt-3bf9oVuN1|I85G{{TUQ+NQXRd}FgH;vW9tXhu7#_?QEog#1DRvxNMay&== zYQ!P=aI{v-d6JooKRiQs2ApunEi~JoHKTo`?20&Ho4em3Sx=g9C@PXg1;N9ZovN)o zEZApD!7UZPj-cufUxD^pj-nr|VYL(}c+W&15F^o-UVA%W$JTZC@^vOUvJbSgx07A8 zoYB^gtgBLX9`j#23ftd2W;-J3uf653#hm<=7&9W4@I#m%urct@uE+b&!mP0N4n(`% zWr6?S5C1>shksl>V)^qkx5ilz@eX@lxcij%ocXlx@3&R{VjX8!2n6;sXS{Iyu`aZS zeLHfvm8)7@fqdZrTvC63Gk_cn1P(#sVMH*3Lu#`G9-D0hB#EHNqe)w-GdioD)XKdz3Jqn%nQ!#C~Ni>`!xz13>@#&95CfrLkHAcz`Gx z0oSk0g%w3(&`>O%gohAuP&gEiK;Q{bEE12ykuXR+ibx_M;V3wC$6Q_jI_#cCW<;L4 z=|X0uVW=_*rmvpf2x3s7{G#yTLq^q%h5+N~=yC=1R0*+L-MZ+}ot#LCSKDk6C$_hoTp8Yw3$kGY@A<-QdIm2n=}e{gM}Hln$@d79VJ(UcfuBg0 z3%f8DOq-+yOb@|lTL|a2zIZ}w6RIaN#Z@bMsWZhl1)sU7xjbb{Uw-mP9yN1_65;VF zi6%;#IJN(_pqPakHN_dlscJ3P8Q!Qs@D%TW7fp1|#|FKt3g&OF9uT#fl+xOszE;+x zF4j_Z);xph*j;2_4)utlmfR$dUnAG?O8uB;RkgM!9%S&JfA{oj!x^ZCT$9Gy2U&BD zlW$tg2iZAuEgCfyHUE;62lwy!MtadZrem?^pl=>$dxq5 zVv8@}&&510R|u;!Rsk+$jrc^KPp5~JzZDnW%3XR!wck+F{$&sgAuu02E22gA<_$7_ z#9q;(MpvZR@ScP*A$JJgxtiC>$E;DUu-uy;T1Oz3;|-HeHM+-W#rJ!GF=z4b0|WOn zF!Ck;7#KTpWDZY{LI4aA`3D@qiINa#A{;^>Ay5Pe97@2GFc2IVK_KF>5E21`KqBEd z6b!eEBfF>Nn)LL3UOJ2bD?-2o6AMFo!X3rCI%)4)XTnOCZ^z_{_c6FK_J z%9s55H{|+1s0l^>gPMmfg%VW&6%~H1CLo7ML_jfUGzyFdeq$g+EF2B+0UQ>JLE;cd z7#Kx@q2Z)Is!65*D*FkSFdP^HBKIGD+I}t|cFMhQ{);hB`MqjyBA&V38;=0!JVakys#i5a4JW6o!T2kq9&ih1xa3?rCJApqD?5 zfc#;EsIK8*g@C`Tu0WLGbPE3~O<~Y~(DXn=F_AxUTw7K6gz za3~@k21f#c3kApk42{A7+3x!z-N|dar~RL4Kvm{aXyIFE>Fcp$h8|Ds8$TSmW3Quh z$VV!*LLi=t6TOZfLy4IB7h0xtG3R8~RJzr0fE;;F7wnl!$^BoW9Srf)FT4RIeoq5- z359P#6>mkHf=Z1SAFx1Oa}>FLqAaJ?$^h z?%JPs`yZ@pl>L1kX(NDVy1>7IlQ~wp) zfuTe(cpQR+ha%xf0QB&1C>#ccLa}H(3M#2^phbT=*xut*{11;4{Zr2!pZxvf+>uk^4XLvL7#;D~tPX}2B_a@T5*&`lVStdv z;_(nL5_pbKIE09S65v=I7U09A?btB?L|dPs&1c2o=P(5W0HL7pWYr8 zHNSpAk2hoKoeHUxC0;fPDn?hWRB3SknNpMgf+i;ijp9k6EHkALB{SW&%w_HQt196q zE=;4E%QcilZu2F+7UMr?eB|6fTRr1kO5kf|MZ3_26LNQ$Xzscd5|}Gvp{M4k8{V+e z8p>lwrUK1;4cmhTU-{KBY&2o|Jjf3psqIh^Otx>3f?#l z5zq6TDl^`e@zNh-WLmct>6#}vUZ@Ms9wtI|%{i{T3@!lMFG%*Uc+$)$8RdONU$LdXGme7ujqT3C2B|QB6Tr`urv-{xyaa_tv2lG`ua(XwA=TNSyou6J zHPHy?H?-SgE(fnLQ^~!XyVyOneDKs|cX@?)F9W}a7Z&T1$8D6SUX$#paWXo)O=hC5%D5LF{{;v368?eAjaMTj}Q4Cq>>RTio~Fa)Vk^ zfV!ADh4-uw{qJ)m9P$$-2LF^JcjTx|c1_9v2yy<`C;`ZULf~kC03gv&C=8&wNGt-1 zMnDj73;}{9!XOARkS)>QYrh?o*gcKxJbCROYd;^i%+K=qHK>H zY#Ay^N_+p*yUy_*Cr@%@UJa}Sb~isu*votS+tkFl1`HJ1Kr7{LKK`K0|E=s)ir-%8 zcjtTQM<+cNwUVS&H8|PP8EIgsqHpyim_o1m`$Rlh-T2(9|sdsw75 zPI3C{toAW;|K^+#3oXC-$EP!Fk2GLU1{K8cmK%=6?Gqe$74FAs>~e`jo*kEIDk(Qp z;b$#dt|R62dP4In*oPP>J;s(he~&V^@v8B|<;JfI8ZoL_v1;}b8x6YWChv3|Gq}~; zn)4b!iI{P0%$RT=W}=CT&NiX>wg>FZ^IATM=>4?luW1Q2+UGd=E&bm(WYbWuqCR{M zHJHd9@QAU#@tF`Kdad|SEA&#ZR^q#o4{j&!>*jaAhE}nWvsE3H8W;mDWj9%GX$KF` zFK7vh7y6&2t$pLcal41t!Ztk7p6m5>UK*oNPkNWPUlZ?{N{}A3&aR~zBW=mG>cdZt zx1Kg)R}t?G)Ro?LFQ#Z$f*c=j;XQ(UuV%k5ei~sCd{L`bChgYO*vH({t%qz*QlvwC zuUvFjJGYe5@NO=EI0PTz30jLjt!nn>c!4y-Hg~n9K0`RuL8r8;JNn%p^t`E8o;>Ou z&nQe*s(6EO3*Dl)HH!I7&+|VZ0DO-h{xt$ToYe3B7nC26g9iFSI~*TIBw!FI5*9~9 zVG(Ey0!tzykO;^Q{+9CV7_)ag>fBVHvFi#hBE?*v-o$5-Kqq_XO7^1s zk7IyeFk$!}*8RC7yCWx0K+EqefOE)SV;vv|O#)OzV+bfT2|#2e zBqA2D1PTG(rTn|6ks(2?Ke4Xd@0P}>wp(H#4MGV(1Tz0_zbhYfMk&tdAg z+r&*wy9;lA$b5X3*=ZQL%U)a8=dfqmTHULEgxE?}U?0@g8B zxBfMtRNo3y(k-o_Xn)qdwA>Io{E{Q~W_gsjU50~yowvaxZ<{KT;_XM1rwlb?3$M1C zCzuUMce%at2_tzN#2i7slOK1CN)e2~d5is3kF+%9r6 zGdf(WC^KOkd+?e(yOGC@;w|qZ4W$;3&$J&-e44^He0b0yO(a4siL_Bd$eHclUi(Je zs%P?9uL{0CEdDO;xRL~3KYRZ8G$Oim;&7~!4H?72%54n0BhTy zo~D76pZ=F22l#=hf`rQ`jGFc~n?W z|J6`hwC+NK%c{J+On0Ue?cs=fo)jS~DqZQR{>EM0euCdPb1tu6M=dEx6>`hYJ2h-i zO`BvFR8X3H-KLXUgz=_>DI&}cxX~Y{qENaO`RGc_mE!Iq2e{mjUU(#cfb5eV7m2!;pwSyaoLW&_w)b zi|u&R&h`5OYDW&!=<&zYfZtO5gYN=uF&ur%d_*>zJ$W{=P)J98iw-Awx0-ACG{0q`$ZV61l%X&J{<>b3$ zv{N0bkD%lXtW^k5ztKo_<#KslDPwW_s^-yi*Xo7MJC;=p&2MbF^#+>1w$X`WM~6tN zc}tr01?5P|lqGz65t*H)xIHr+`_}gi$oVUj%Fa2C3lrzjT%rD2tkdAMdFxe?x%cGU zqa128c_XsN7BGRn&oQS)#=CTzvXwaB>G#sJZ1D(GUA!7)NX;QNGL{WKKUub+IdU^- z#z3Ip<&v;~1+AyawKre4gjvb!4Bit_a;)zt<0R^tlt)7^JBZUPqA=+{icbJpxR=<^Bamz z5;0WE%W=<+9CG(5O9IjOs4faUpMXZz1L%pBR|w(+ZI;@ z#%O<&U;ay;-y=4=nQc$Y9XZmu?rgGvmKT0aY!F0ID4vJ~Ltsc4kmG??9~y%p;lT(9 z1PR#*S|pwb%=w_mT`hM{Bf9{q`AKYw2>(NDY}%iTw#o_H33hbkACeaqdG%aDNRjZh zSHOkfEH!)MDaDIqE$h2XW$)Hptw870SZekF>c1#Y`uT`=Z)Z>W9g;K9sm%W`SRH~S zib4<(a4?(*Xt~q4Lqm`lV77^d;ZZme35x>y*+c^B`*`Aa{ms8*bv8R%_k7pbM)R|Y z##MEv>T=CfU_wDZUak2d9|&H4cwz5GcNAW)H@DmAIPdu5F9wV|H;Mro>m(=<3d6vV zP&~i|&`1oF1S7%`2m}(2z`-yi%+KEXu37ec@i%OzyiNMu)*7Vs&Wxqb7ICxIv!x7o zcYQ7R5OjsS@Hg)GA9yx$ueH9*eD@UHk)vYDs_hFX>Gf+x0Xaw@VE`i|7!-!UL2-ng zrZX@j0wd5+JQ@Na0L3;QC^L55arZPbFVJT5*D76R=OYuuA42a z3G#>gsoMINZ-1c@8JnmI0lZ#03&-i41q^Mb_}=EhS)ug>@?^#U^vCLRuy$H6ff7--aUOT&|I?$~k#maG%PtLgj7k6SC~=}NEEWetU|~co42p*! zVMr37D3XN1Ac;s|`vL*O?~E6B9_3C|y?Yv25(u{w1D)Svzz5rhX_}=pC-H^dxOw}= z^fSEr!SZM%Q*}+Ltj#x3iLc$~LZvA$dgk1wrhnXTx7f*Sd|_sgHHin88*&M$G+Gwi zn(eGh$T4}Pz$L0A$A@b_TXdX#65^T!1Z>9a-Zns%lF*Ftyi9%7^$ zg>rB}Twb8PuX1`9M0biVr_$5k-%Uy6i)cwko=vR zcR_^j?xXx2LVvhVeCOLj&NNP!P5u@^U%=kDpR2aJr~M4dvoz!odLQgeBF^nR-wS1_rxzZsQr&4MjNnfCP5w3f0CGrpG*CeRJzy*W7=xoA0Pv7t zI2aa80Ggx(;4c&k5BphA?4CxJ48rcfPw38SIz32ymC2ojfVyjW_@)4LE&i^hW@&y; z&#HmvjTX!ynEC9w0p&D7iyy`r`d0o3djRfcfDz8zbCE-Bh$ga8$ZP1FV{z&YhrB^s zRU|Q1PB%y|{78D7(1r20kFr8KzldG7=~@+T)DuN&_fLs(h)50|(q2^@t9o)>py3z? z?v9u5>n+v0_)gW&*A^AT+q)+zx_mC>>_rMR;9oO5G~s8Yc)R~`Cv!(m)%Dh={~~<> za^OT1Fuw%Q912Asus8w~i2*i?5Ew8RM+9n7pdE_EBM7@bw0qkBIeoEv8qclZ9^3Jk zUNmiq8H!I4`5>x;+}Tx{NFxOJjL(L7j#VTw)fRgy7(9qs`S|gR6R50Jh-%MV{{dJq zdm*U(~;BS%iJ{h9&bgWA9LLqHA$MgSHSel(nrC;|!zMWX;a;YdgX5sknT!FUqt zFVdhkD0?^L9{${5{cU0Uqr|H?hcBB?a+;R3OBqU!CNCBp9d7F0rxl#hruNELIbVL@ z)1Z3Dlkms5uuE)>37{?!&d5E3{09ue?gi#9D(o3#N6x+%r&3e_WBkLo7LbEO5x^)Y z1d2i7aReZX;So?^F$)8SfJrc*LkE-&L@4PeFW5cpZ^yN#vaJrS#ZL=0^hedLQeMql zcUZ0iU##RQtMJ2qxhp7cttdgbviSx#pY60ZUC6%-%vkgfa{Q(z$^Y*+o3VSVx~+fO zY~GR6+#k^(pJgZI;D=Pu9|Lk+jbmRT#PtmlK)D2J2U(4`8LUY_xS~~Z6_k2&M!@m_*mln zjxg=JzyE)7gL1E|fbqtoJwXaa-hL0?NZcaHtPbyg#|%9%@O1Rv7tTs5J#p4% zNx`hUuQp$tOqpKr0KFe`J@oYi#C@8xSczmy)&1H?y{thimFL+Q^@*le(oY)2!&`&b zuPuHkc#tL>d*!h&jUDwzpS}EZV$CLos=aHlWS^ zO(pDz**Q-#ih7M>qEq*YqGHzyel*%&*|2iC|7^4uR;T%8uMXJoy?SgLN)eIT2r^7e zv6Pvlt|MHe?*6D&>U*GS$>E%|jJ;jjqRgQrgu_2@;Gto z(A#9?7aQFRjurHKr89U4Ew4PR{Mc%Il(Ea^e;2fSwcu_T{XS^b)<&NdRW(U|`}01qRG` z1e^%L16wy(fF)y5z&0WHJCWMy=d5R8*$~rW`8ij6^asE~WWI-V3_l z#zXt*brEKW?9%z_V+EcR98(T5zBTBx}3x~vt= z;eQs~_~|}Ly@HBK!}9r=g5!z3#S1gdZhfpp!W@;8CjxxyS6?*Ne_3P#7SKHOoBSNs zOtYf#@zQZe#28M*Ut1nEhw>HC?%#~)|^Y6~ISClz8w=kiUaC+7m&pf^;5w7Ubl8Tnk8!R2an>GZBJ#=s>E#`V5ulyofd4VOD@r_4F3pljVweo~o@fpO(lDKgH z40lcvBb?K!AAD)(wZE)O+|x<-(+8}xO3W))@@2anB;=1=yv@oLgH62LlB>sxe;gEh zo6xoYhxHfB3-$MPJz(8_$GE8Kphx?TMtcNdu zXxIV$cips^+Cxt7b@)R$X$0oTj|X!Op+|5v2Dycj*2pKC#7&TKAwz`Ji%9KobTJwB zVww#vcQ!&ip4}=*o9x5|6EIJBZf7X*#bdx*~r=NXHe#=zjZo9A`LN4It5;!vVCA)M+sl&a70bA!_B(@D=cQH@f3m(Y7C(F3K ztt_uUk>wpZ8Bb**Hh~oX_19@0kOLDPDV92ulMW2|=meGCte9vAE^2J6eCzd)V1hPl z8x^ykZ^RqOypZ)n=!APeeP~aGb2s#zqo4S86k&^RF<_jE2t{)BTzx8EXYD52^eWZ!)H zmRfbC#f1Q;E6Yb1&AbB^2N+K9rj?c7PMzfCNCk!F_|0*h9(N0o2o4>8!&YNmU0paC zU@7mW)q9XT>CPv0Pnp-naDWHEcbVz#X=DRr zd_P>)dI)@I1laU=CLWnYYNUT4;YdjXH@og>^5{Z@o=y@kTVx_(+pE}&x09L9jP^^@ zk+Do+W9TpM=dC*hP$dSPfpa1R@M0g8`0Q zuyXlCf7M9nYyr1*1QE0OxOV&4ai$c-xpRHwH)8Lc`5&K^s0ib~8`^qHWO;5$(7o;1 zXmkkhO<8B(<1b6`2fd%}XU?!(KZI#z zRc7(K(CYO`;bV@=RLrYMCTs^2BPtG=%wKaCaoF$a_@d%OAMo{UceQ(YZL`YAYt682 zA{(pLkCtC>k*zG8+mClnl(vC4-E35HWQP|%e#a#6a*H}OoSH7fv3DM;eDU%JYDybb zlx5t5>ob@&u$WJJ&fC~gAuXr#F+AJ3FK_kKNHfV0s-x zY7SdSrT=2cEn0UEpr5{Pl(Ot6hv}aBaO^j8@S=hZhAkS%_#H@3A-*20Sd`g1lBW}K z=Vj8C$CmTru=aRFf66u&bS)(+8!XG!+STX#$_p|cBaljFc!N2x?CJY#;jfchf_&Z} zM!p8oJ)is!H6woS#iqYzWt387m2LndbNn?c1LWY~z|IlS;Uod{11P7VNT34>#sM=3 zG~lH$fRzC}1p9sN=n1rrc26U71O@G~vj5e4pucXJE0!Az97*t6TPjH(Y}42$q0?vN z@&CH}@^GrU_J2i%WT+H{gb)p;Oqq_%no&{cI2@FDN|d1tl_67uLMc;GibiBe(MTy3 z6%VD9PSL0VzqQYH?7h!9dv|)j|NVNeb6wB-JXfE4eb#WVwb#1u&;6B;>zDANmq>s8 zL$sDF5)gTsZM4>4y4!6la9%SdKC=NF9S{+%2A-j`!E%MtLBV6D7I?^115Z{iuBza| zkE}vLo){R`#+i<Tu>MF}89hq&J_t!m| zQBxWbQK`M(FF7&s9QOmKS=w8?=l)#t)15qT_>!@5XRe6Z2Uppoc!iew2U__%-CNIo z*Uru%>&+VaWgz94>^x8=rMPKpOiRx2bk{0Hz3!}snf*!csF^2^$1Se5jt(d)`LkM; zWWJD}yxHwk*5xhF!}xmUF5gbNggZ(X-|(qo@5oGAvd>Src-LBkrMS?n^24HqOQhN+LRQ> zi`t?F0-M`F8fe35kOul5q=A0;qcPl=0n$L-9%l*FsLW^{EV|r!Z{u ztm@@CC=JwPP0iW~{0p;|-CG;F)#tkSuxY5{6&Yd0Cp437i!T;DDjyd1iUrA^md^T_ ze)ZO0rw&cB($@1q=KnvulDNWq82&3so#*c<9NbX?9E!F!@C#%W9q@<`?(CQ+PtNZBm zR5OA0$j7I9Sh(iQ%2N8zVT#qyjkL zJ_xQEjQ*HYlXs^SK6DzsG9s2bW-?XgoT!x~;D)ioc3GlTZ;O2R+6 zsaAb^=IWN-?kvdF7AAm-p8kisShv4D@~p}+TEM|A2EftM0;3L6LN%ylHC2!mss+NR z!DY27c*q6`lS@F7u%??D%Oel&EyjA!H&dH=!xWPO5Bi}L<6ePEL9sT;#(&KR7o1c%y4IU99BNGndSrwa5$iXcO zXdj4LQw5j3OLSbR8d|O(w+)07siW8kfPD^cSOYk4!-!{|)v|lbBl8{$=XFlZWRAP1i zw06TpE|p$URQ%gNM4GrE{)c@$jY@dRc@FY70FH(l@X8=VT?GgOgODp%9aX9($l%bR z>S(wufg*`O55Ph6=y)3Zl1LArs7SI7n9b`3d%LYw+3{$*& zRDthJ3FEH*XnUjI>T>w8b7;$trq2Cb--m^Q5u*R_N=QZGz!>j#$O?yW;wJFEIR#GZ zdBi7nfTO9Vr3tu_!FQw!$fk#$U^QKU-vz;}K(j>$Bo9zfFU<^VWTt8=>7Fe!cFcge-b7cuBi7@j#50Tc9fKUEw<^%9`RKB z=ZRBqPe^XM_~c5)t*zag^n)**kGhc8K2yQb)~x4x3UFNT~v!2S4j>fS!x>rZk6?Hnd03|Yr=k9Y*%5!byoyQ+oj?w>)f z?g!>VNcVmKjv4gE*dKy05GwLPRLpX~hj3~mu1Up$!+jL-0UzLiAVjchxwtF=(ZOUD zO;w5t*t#^eT)?#w2)P4OOp8KQ8;eh5SQ{5LK9YXGuk&d)5~gLAhTh?mvEsGMFWU58 z*dm2j+H1Q1z8T?3m+e12c)29iuBUTppOHsi&Xb(%?Y?jBKL_c4XMbxAEna_U;U*b* zS+AKF4oyr}I%;JQFrrj)T<%zSTlq4Z3rCZ~Bf$ z>W4Na%s!s7jvpw!SlkXx4Kv|8sos_P&L>qmKW&tOQ##*@n5-)g0zM zdKfn2%(+da2NObHaJ#Hih$^cd_qt|kWbaXRs^y2`#lf%^f|}o zl;DdKn*%>A19!3`(=_fCN`f;!wRf9>U~YieXU8gso0kS2Z{F#!waC!-QoRW%N-m#GDX>`5C^BI5;aX*_@Pq!s34a8f zcSxxV<&~X0-MsQ~bL`_I?b-68Uf+y9CwEkb@@|{yDE9LgU**ep{+@x)KL*7K3_l6m zIy8au!LtcjI{$Dsq7t2rAe=>F#SI*M8vq;^aI;0GfM*O4lIp6ZwnQ7G3u{n8_AHRL z2l5#%+7t>n2SP3wde2~-#Qx9U2E=y>AwMcE*?S!e37qyLe$YEuNkaY3#oy!lWfxQ; z-SrO#6DqPdGmZ*~BZqK4PK|n52z=HF@FRHs0XSM9zEw>{g-Qj1-5e=W4&$>_zb!XGWI|ow@-)lfhi1lK|2*j) zw`n**sKYuqXO3iIoujWzilW1zWlFY*`Firp4)0FR+4LY$Y+}fb53T7(7f33~1e!D| zCdyBVu{Du6;4e3~Ms2w!uX4nSF5tx)IQ+^q{f@v~-ochcljTQ*Lb?e}2APJW$9 zbHDv(2Rv$uPA+A-S^6a7(=y3h66Id&>)eAn_j~xpt~;ftYLkFUR&vkAxBkqX3!)^4 z^VURu-GAD8zRG1g>B};{GwRG{B7MuOG%A6w`R3_^qc{p{A^roBADOb19`gkC97A#x z87JAa&qi;$hvlG%_W9t)MuT(JT1#_hv<7E%*F<_A+Jrhw5iS*8o^z3{ei_zH<84N! z9?=Wri|0~&FaPF6f3?shtG?D#<%hq}%cHSxIUCi`F&#+e7Q+Y}+@#PE5D7?nggs!7 z&7FgA$s(Ww~V`jC3O;@<+H$#wI4m*eS8DP zB48hk2Ekhsq!b{tA`bKo<>^u5dhkj@1NGKKFn#0a>%Az{+Y4HbP@-VfXxD=5R!BJS z+K=_N*BVz^uQ#^4#g~#&WaKcze_4b0E^9CIOOFml*7(SKet9SjD(mR@cHGl5d_Jwm ztTd`}U>)_2)AEk2yE8MaE=@Gu&@?z(*n4eQ z83qe|M}C>y9-mu6*|)p7%!O>-zb^1i%sOWeO(~ZQqjNci@y&DF+FhsaZx~LB7z~Z2|hHTN0bk#qykwr%HLIhhpKMMd(x zK0bAImHxr+{pN)`^v&1DzE5hUB(JNQJL!j3ea-O4EYpjQ^QPowRL$8x&})cHQ-7x8 zydY)<1O~ug{LorxFhEuoyh5j}h%*9Ha_kSbRxI>)y~&i(u4ubz~mnL z+u$FtPH7Ba3poTn4nfT@L1lTsDC$foSa*v>@eObvYmW>IQZ7?xV2drVBGC&WkKswO zeB+pd&2gzAwb=xG-wXlSb9I)^rFRgWAJj4!kL2SUm8aQ7)O5sqQk?^SL8 z^5*^r+KQOja%PjC$F`-%sUNt%Cz3yM`oaEdQ>}i@%75TuJ#D_+9499viBA?bS#1dZtvXi4hjjL5d3;?F7~w<()uw?zDeE{hd0dtpD{q%s75xlEPwa>BZjNWMGIa_yv>26jS{w;be(a! zkS=XRp0mrxTt_q5Mq6-gTEbjP{U0E06fb@IXTk62l2^h8&K22wZFUKl`pH~(1>-9O zi_hlr6jwg}HY_os>n3<=c7x=nc*Da9?s``F=jPfb@3RVCws7}=uH~_}#nT78lAeZM zO>}=3c58EudR9Jnc9HAV^=X;grIj^&pH7eN>haeJdy_U8p_ZN3C%se8(tW?>u7MqU z#S%g@SDF=jR^4#9xanScqg0Bp;~(#=KZkD48J=;^e%2*bg=Y7zMY~J4*Cd;smRRa9 z-4ST^xpwM=IgQoBHIhmvFW-NlU>Kh=;lhBU#F^LS18+WEjQiC`tvuXY^7Q7RrDl9X z1)&r6y6Xidy4BsFxCt0$hQHboRdd5AvR3Z$Lnl|?KWm?X+NPy<%B0L54!ar`#02iK z=Xbup>{gdKPgeO0qcR_b_uu23#Re{ZvY7wK(};K3g~-WKj-L;-dg#adZGC3adD2C|?ou(F{LYDd-fzUN1IwJefm zToSSY(=+M*-Z}Y^(+>8Ri&*`dao~Z=Dp6>9&f@8~@%YSv-@6;X<;6er8yvdp5vry8 zEGI!y?94HFjq^I+pxq)H{+mzW8%Xj6R$ovXKMkb=Mrdo zF7ipDRU9@x=v}m=LV9wfbZFUu-SaE=H}*`N9FW{BWeKKdAGOd`>e=MX^bnq|)$u2m zu8wwmF5+Hi6~^aE-QXPR`BhBrXH{Q>9MIG$J>=s~TG`s|05o-aVz^5i&u&z#`?#p8 z?%1oymG(yOR}R$v?wa|eTf8%G?K6>C=58l{Mc>_Ab+5dzReo93i|%Sy?w=#i^!25)rdd>x=AuQ9Gsaw5GKZ=R)Y zU)H}|yuRk%HGhNJW0cL>E~(FpX8cqhnJ>{1v0B5fHTh(bL2Qb0mxPqDNR7;Yo2YKL zhKG?8jQ*djs>QiX$X(780b9H&v9VEytFlfxfqRY&~?>?u2$KJxV z$GZ&d9z0@PT&%vI4zS3?#@Qxl^qBMk>>?_ozM6J;sUS#vFmX?sY^||q{!2eDpUtMX z`L1wHSbzM-!@2DuWz*NLp0{gz+P8K4B0VKmaXst5;v6L)U6-V8koIgw%vsH*xiP}V zdv;RJ2E7QGZM;WP?U+#P*`UDL$0q59P}CfIt+XzmcRntg-Mf5dMa%vB%kGL9Br4CI zwof&7LvG*t(gim<{rq}<{x&2p8JRpcLM-7`utdKg&t1XNkiPeE->i-LkHr@Hc;za; zd{M#6dtP)Ee&@07o^WJFz z;e*I=CM4G51JaeqcF(|@jlzQ`w=1}~$Zr=p>I^)y7O4NEMKCl79{TuT3sw*22yt&T zIF@mQ_@bN>FnWYIq{}l^j-Pk7Y1RpFhR{TxA-=w9dDO1{>00LY^6$DY#+&QRw>Ahzw&(=Uib2uOew$SJIm7+S!L4-Fm2f4<#-<=nFB7><#t}H@|je z2LCx@i=O+39(ycayI+wUJTXh@W%Ja$g3bSJR8A4Rnr}UGr)`0WlEwnJmhH+<^A#+F za=~3(pOSp~c2Sr7rd!v2`EtCvHh=#0mRxZoChOG;ZqG7fveVfE%elj4Y+90~B|_%* zUdkXlw%%CdG0$MOnp(-?Dvb)Ew5-zNPq%diyIx&ww*=JrS$m`Wo&se((*#qE73+3R z{C%ct0qvdQyoBtul6&0tx!Wt{h4?$;66$1aPE-!KW|bKCo%C0eY)~~TpCEJZq^X+J z0)>a;zx22mB*aTf%j^E$xb)}Zgw8+r?aNiXC}uf8^h~I_8@dE_d$v!1Ajc*x+y}Vn zXLci(m$`i{kowi@K1VHHvpQ)v!%auFdCjI|hqMm{-BrwWo?a^bJ<&ZW^`3obUSgL* zpnH~3zpuyx+WA{6C<-k=5z(uoApL;pns2kGe1E>dIllUk!>^UbP0rm-fvsO1b@`|L zN-o+@23MaCD^DIc)vYZfwdYgE&LZK*{pI6!OmW_klM*glo?hI%(zbcy+B+H>mrM^W z?WhV9xLT~KAsO+==aTMn@34()i(B2hx4ybKFd1=DmzmZC_6swJ9N_K-`*$b@&I+{z z;3dWU*RfyGqko;^MGg#f3#4xooWV$9RuU5o;~Rl9DA~V3As-1)i3&7Dc$GsR32m7- z!iPgfl?&GG9dR0mzYr029y;`p)uRb$8|U)x!GRJEdjzD~X!S{8$^Cv^8<> z{-T7_lLhvE;D2Fc`EG@kYM0HQ$(vF=x+e{;X}B6YxToW0R_4{frmXEFq1(CrV)UoY z-?i{$^rK_Rjp}bTG6q(g%sj1(E{;*^IvI zxZkuBqUI6W9@Nz`y-#I$^Y-3%HaIj*JcIAefsCPY@#p2|&U~=T=x|;V9B*Lj!t-9Z zY@tq4Sa-{MGiyU{Nu@>K>pmWL+NWN9c(fjeZBPVLyIGtPjucKS150v@Mm;sI}S6J5@P0xC|V^*;@MWnRD zfi$yObn(7W-ms#t)s}~@xTPB%;T>B4O)j*5(%{J(hs10v;xl*px>MhN=^Ns%o!1~0 z7DHN7;9~FIX7)m}w@zMQ{by5t*)O(w4ax^>d45>Et*DUSkp6Dh=FaKXqP5qZ<_KivRsJ?V)_GBImXEy`2!ect$Zu5S)oA!fR?vx#@u!RO-v)d^(1OtVPQH+HAK z1ihh{`01LeEPk)HW?Kc{l=eLd4be7!rW%EfT4x)22jmX#ZPN+gJ<@J4Dbc4=ufc#a zVqUwWcDeSA%!x`DpW16bSyGZuE| z9UUDE7J8BY9Z&JEZA127XcQ*T z`$P^IJnlPvHmvm5wQBuqT+bRRqIs6x6bcK`nkD$@L&4kPYF@#hfVswQ(l=L&^>6g4 ze9_ldNQ>@OjJucoHLu;x;>r7kagVDzMdj0O*;~lgr))bZ(I^NPR?Zx~?~)*H`Y=>a&im+2^_6Dy2A;l3hgZ zPCRJgQ}RS$nTXgA;^1U{! zQ9yaoP&_iy7&_3hRV!9l+B%z>SlX_zF=w=o85Vk;aBGtt-T(l6!A}$e+r$b>@%5p4 zxSuAjz?zZp0N_MHSc1h~17 z1Kl{l$JWJ0`I_h?-~;gR@v3Cw_Y<}lG1^WPSYK@Ia5T=4i^7%&MX>Xt&A|H?6ZUAq zP}p-Ko{9ZRosOQCq&re<(C81b_zM+V_z~SUfjR8a#>zm#O+;2d+(c%fH*6EfOS6EE zR#}9b2uZM^eJ9j?*cyxICYs=&F`OObIhnhVxqaf8E>xCb-@egDUFJrzr#uG(89)Td zc;)4#pRNAJejW-qFS_RMz>H^Kc-}L21@k^U69rErmCLbo1*0?^!}Bu(vP#4tqeUYP z;sera&B2`uNw}bA0BfZ=1XC{BXdZ4O|Do<2+&ze8cvxABzaf@sBn5eV%h0nP%mymX zhGxTB2oAxNi}oeNHV`JZ zdKZEz7rloNlhayW@Yo=hIboxqgv*9rrVF19wn!J6jV~c4rzN}CvtbPhLoCxs3zYHM zK#B=48z^K8JvI;~w%QhgDHk0?h{-$M9~;&je#A13B&&hThF%&79vdjf z9z8a!g@F)Ex#+Egn4Fdgg2x82%*oiy(ZXkg_%wE7gDv%fX5&tX$!Xyi_G80*qDSAa z&`5mXJ_%>0(F?l3W5ay%hA^?^T+nRR5Mpv#+y&lm5XasG zr-YcC79wDt zrTy9aAIPU+h5gZNL@kIgIhFZm_5#TH2dlM@w)}_CGN;P>%$DJPWWL@?ss7}5GGdR9?fPgAttAa_v{}&h<7CyAXkhxDy z5U-ocRk&>E(&_l?1}mJ7X2T8S!*SN)Kgg*6FYD$WzGb@ZIm`x`z3@zf)*@E*9L;9> z8eBI2pn=YQ-LNW_BbI5T76Qx2`x2xmj-F|(>g5QgT=cB9_-r`WGKbk9mf@L3?9-xc z@hu~#5O!mO6}LvSxlM@4snj+5u_0!&#SWhhv4_SgSfkmL5n^(xXZ^ph*#W8_;H+3O z@=XURLZfFI)I(VHX*8P~gqWOaQNyQq#40I0qt3PQ`)Yu|X`uY>01`y?_uG&e+hkVqrGO{Vwd&Se0VYY%UODa;h5(vq3C# z;sv;zaoNySM)5r~R$~;+#)1%&Q^iqUE{5VLJS$)}#CM1)n{nCDH9%oDkSZs7ra_ok z1yD4bP(n;j^+4gVK`g^;h`$I_DEMq(4~_RlfYo!vupz|cRNayN7Xh({c6P;QgPhaY z?KfDtMKqg2LQGD@7ukDg;&Yn08!j8VtRg(q*xeywMHSI(_7h@qDzON&K`g^Fjrf&W zg^JGxIoUA3FF`$oRZc{+2_?kjR7a7S4eU)JHdswWv}G}OA~u{VB(i_mBA(;j1ePIv z1AK3a)iXr1QS!iN!@0U4`#Da0>()tN8CkLHZr!i~g=jVkp7?Aymn-~VZryIJkE}MUlBxKKq_^Y>!z%p{bi+p>+>xNZn5Y6TnAttA~gYZm4EW`eX zRm=~uOe2YS;rAo_S_Vb6Lt=aAeVbL-55bg+wkO2oROSz6gII>y5PQ?*zW8kLy(xr= z)mlTd$sok!RCx_Py(4}cW<%`L!u{~sAYOpozKxYaL$j$T#N<>M4ffE8WlqNCnLj=o zRe ze=*0I!@b0+{Dt&VUi2+iRFn=Fyv`sUF!X7kxt-X^b?qP)_G3Ze(8-vYHSuXX39vc;}15&A&zCzI=U+_XlIshIZR*5fkW20Cg z8G>K%a6e#rkYbheLZfwH(8dJ5;OGNL;0ry@tlCfr6pi%XHZ)gA90@u}L45`(MWJz6 zb)wKX5~0jEqpDFb7^E7724mHXLW4Q72OHJEfDs`DDKsLheiRz(Fvua5)tt9gjA-`##r@% z(8jvMIWcB}(z1fBKx#qg?qW`tMCa+zNKO&VR-k0wvHn5wCgJ{JRfRz}`51dJl(-2m z7egb4sEJ}2r++k(Z4)swkGY+!+AHXGb`c=5G!{}~K{u9F)dYd2k-DSs8;hRQkj4ob zl~v&cjk+M37!^CIA-xkA8y+-PwG#w8FM0<@*yu@3&yt3^A9733CIKZ$qscI9ONd~` z(Hr}Bsyi6Mrt5}WQ-s~z=EiyvKZ1Lv1~{) zT8d6_htsf-HY0usD`kNmLKn`!(PPLEw%{ETwm{DqR!s)9Io^1VxWKMyG@Bki?;y0` zJ^wzqQQ<=Uk;3$u!yYzvFj@nQSpz8opvQt4R|bPy@K;>)IR6b>5rh4mBWz^7GV~14 vy$`KdteOG3_m}VeTdyF~2|eOyZ3B2T;AJ~uA~XQ`;D7dpKnW=U`hWiqSIZk* literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 4217be58420..438095f7f91 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ plugins/sonar-l10n-en-plugin plugins/sonar-email-notifications-plugin plugins/sonar-git-plugin + plugins/sonar-svn-plugin plugins/sonar-xoo-plugin @@ -597,6 +598,12 @@ ${project.version} sonar-plugin + + org.codehaus.sonar.plugins + sonar-svn-plugin + ${project.version} + sonar-plugin + org.codehaus.sonar sonar-squid diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index 77451cd9922..9fc60b2fe0b 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -137,6 +137,12 @@ sonar-plugin provided + + org.codehaus.sonar.plugins + sonar-svn-plugin + sonar-plugin + provided + org.sonatype.jsw-binaries jsw-binaries diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java index bdb9cd4b3ef..4221e1abdfa 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java @@ -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"); -- 2.39.5