diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2014-09-24 15:15:05 +0200 |
---|---|---|
committer | Julien HENRY <julien.henry@sonarsource.com> | 2014-09-25 16:25:24 +0200 |
commit | b375ce53532d11c75c9c920b36f0c4692d123c87 (patch) | |
tree | 5d6204c5574c3b354fa99afa0c7fb2a44ce78493 /plugins/sonar-git-plugin | |
parent | 78fbdc2a8445e9131a10d2210b88d0e4d9927a14 (diff) | |
download | sonarqube-b375ce53532d11c75c9c920b36f0c4692d123c87.tar.gz sonarqube-b375ce53532d11c75c9c920b36f0c4692d123c87.zip |
SONAR-5642 Initial work on Git provider
Diffstat (limited to 'plugins/sonar-git-plugin')
7 files changed, 574 insertions, 0 deletions
diff --git a/plugins/sonar-git-plugin/pom.xml b/plugins/sonar-git-plugin/pom.xml new file mode 100644 index 00000000000..ef707db2795 --- /dev/null +++ b/plugins/sonar-git-plugin/pom.xml @@ -0,0 +1,77 @@ +<?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-git-plugin</artifactId> + <name>SonarQube :: Plugins :: Git</name> + <packaging>sonar-plugin</packaging> + <description>Git 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> + <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> + <plugins> + <plugin> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-packaging-maven-plugin</artifactId> + <configuration> + <pluginKey>git</pluginKey> + <pluginName>Git</pluginName> + <pluginClass>org.sonar.plugins.scm.git.GitPlugin</pluginClass> + </configuration> + </plugin> + </plugins> + </build> +</project> 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 new file mode 100644 index 00000000000..033cef7b7de --- /dev/null +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitPlugin.java @@ -0,0 +1,34 @@ +/* + * 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.collect.ImmutableList; +import org.sonar.api.SonarPlugin; + +import java.util.List; + +public final class GitPlugin extends SonarPlugin { + + public List getExtensions() { + return ImmutableList.of( + GitScmProvider.class); + } + +} diff --git a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitScmProvider.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitScmProvider.java new file mode 100644 index 00000000000..7fbfe516c20 --- /dev/null +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/GitScmProvider.java @@ -0,0 +1,97 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.scm.ScmProvider; +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; + +public class GitScmProvider implements ScmProvider { + + private static final Logger LOG = LoggerFactory.getLogger(GitScmProvider.class); + + @Override + public String key() { + return "git"; + } + + @Override + public boolean supports(File baseDir) { + return new File(baseDir, ".git").exists(); + } + + @Override + public void blame(FileSystem fs, Iterable<InputFile> files, BlameResult result) { + for (InputFile inputFile : files) { + String filename = inputFile.relativePath(); + Command cl = createCommandLine(fs.baseDir(), filename); + SonarGitBlameConsumer consumer = new SonarGitBlameConsumer(LOG); + StringStreamConsumer stderr = new StringStreamConsumer(); + + int exitCode = execute(cl, consumer, stderr); + if (exitCode != 0) { + throw new IllegalStateException("The git blame command [" + cl.toString() + "] failed: " + stderr.getOutput()); + } + result.add(inputFile, consumer.getLines()); + } + } + + public static int execute(Command cl, StreamConsumer consumer, StreamConsumer stderr) { + LOG.info("Executing: " + cl); + LOG.info("Working directory: " + cl.getDirectory().getAbsolutePath()); + + return CommandExecutor.create().execute(cl, consumer, stderr, 10 * 1000); + } + + private static Command createCommandLine(File workingDirectory, String filename) { + Command cl = Command.create("git"); + cl.addArgument("blame"); + if (workingDirectory != null) { + cl.setDirectory(workingDirectory); + } + cl.addArgument("--porcelain"); + cl.addArgument(filename); + cl.addArgument("-w"); + 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-git-plugin/src/main/java/org/sonar/plugins/scm/git/SonarGitBlameConsumer.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/SonarGitBlameConsumer.java new file mode 100644 index 00000000000..3102b00f105 --- /dev/null +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/SonarGitBlameConsumer.java @@ -0,0 +1,190 @@ +/* + * Sonar SCM Activity Plugin + * Copyright (C) 2010 SonarSource + * dev@sonar.codehaus.org + * + * This program 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. + * + * This program 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 02 + */ + +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 SonarGitBlameConsumer implements StreamConsumer { + + private static final String GIT_COMMITTER_PREFIX = "committer"; + private static final String GIT_COMMITTER_TIME = GIT_COMMITTER_PREFIX + "-time "; + private static final String GIT_AUTHOR_EMAIL = "author-mail "; + private static final String GIT_COMMITTER_EMAIL = GIT_COMMITTER_PREFIX + "-mail "; + private static final String OPENING_EMAIL_FIELD = "<"; + private static final String CLOSING_EMAIL_FIELD = ">"; + + private List<BlameLine> lines = new ArrayList<BlameLine>(); + + /** + * Since the porcelain format only contains the commit information + * the first time a specific sha-1 commit appears, we need to store + * this information somwehere. + * <p/> + * key: the sha-1 of the commit + * value: the {@link BlameLine} containing the full committer/author info + */ + private Map<String, BlameLine> commitInfo = new HashMap<String, BlameLine>(); + + private boolean expectRevisionLine = true; + + private String revision = null; + private String author = null; + private String committer = null; + private Date time = null; + private Logger logger; + + public Logger getLogger() { + return logger; + } + + public SonarGitBlameConsumer(Logger logger) { + this.logger = logger; + } + + public void consumeLine(String line) { + if (line == null) { + return; + } + + if (expectRevisionLine) { + // this is the revision line + consumeRevisionLine(line); + } else { + + if (extractCommitInfoFromLine(line)) { + return; + } + + if (line.startsWith("\t")) { + // this is the content line. + // we actually don't need the content, but this is the right time to add the blame line + consumeContentLine(); + } + } + } + + @VisibleForTesting + protected boolean extractCommitInfoFromLine(String line) { + if (line.startsWith(GIT_AUTHOR_EMAIL)) { + author = extractEmail(line); + return true; + } + + if (line.startsWith(GIT_COMMITTER_EMAIL)) { + committer = extractEmail(line); + return true; + } + + if (line.startsWith(GIT_COMMITTER_TIME)) { + String timeStr = line.substring(GIT_COMMITTER_TIME.length()); + time = new Date(Long.parseLong(timeStr) * 1000L); + return true; + } + 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); + int emailEndIndex = line.indexOf(CLOSING_EMAIL_FIELD); + + if (emailStartIndex == -1 || emailEndIndex == -1 || emailEndIndex <= emailStartIndex) { + return null; + } + return line.substring(emailStartIndex + 1, emailEndIndex); + } + + private void consumeContentLine() { + BlameLine blameLine = new BlameLine(time, revision, author, committer); + getLines().add(blameLine); + + // 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; + } + + private void consumeRevisionLine(String line) { + String[] parts = line.split("\\s", 4); + + if (parts.length >= 1) { + revision = parts[0]; + + BlameLine oldLine = commitInfo.get(revision); + + if (oldLine != null) { + // restore the commit info + author = oldLine.getAuthor(); + committer = oldLine.getCommitter(); + time = oldLine.getDate(); + } + + expectRevisionLine = false; + } + } + + public List<BlameLine> getLines() { + return lines; + } +} diff --git a/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/package-info.java b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/package-info.java new file mode 100644 index 00000000000..748474dc689 --- /dev/null +++ b/plugins/sonar-git-plugin/src/main/java/org/sonar/plugins/scm/git/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.git; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitPluginTest.java b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitPluginTest.java new file mode 100644 index 00000000000..d65e0eb5844 --- /dev/null +++ b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/GitPluginTest.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.git; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class GitPluginTest { + + @Test + public void getExtensions() { + assertThat(new GitPlugin().getExtensions()).hasSize(1); + } +} diff --git a/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/medium/GitMediumTest.java b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/medium/GitMediumTest.java new file mode 100644 index 00000000000..a1f34593f1b --- /dev/null +++ b/plugins/sonar-git-plugin/src/test/java/org/sonar/plugins/scm/git/medium/GitMediumTest.java @@ -0,0 +1,119 @@ +/* + * 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.medium; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.duplication.DuplicationGroup; +import org.sonar.batch.mediumtest.BatchMediumTester; +import org.sonar.batch.mediumtest.BatchMediumTester.TaskResult; +import org.sonar.plugins.scm.git.GitPlugin; +import org.sonar.xoo.XooPlugin; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.fest.assertions.Assertions.assertThat; + +public class GitMediumTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + public BatchMediumTester tester = BatchMediumTester.builder() + .registerPlugin("xoo", new XooPlugin()) + .registerPlugin("git", new GitPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .bootstrapProperties(ImmutableMap.of("sonar.analysis.mode", "sensor")) + .build(); + + private File baseDir; + + private ImmutableMap.Builder<String, String> builder; + + @Before + public void prepare() throws IOException { + tester.start(); + + baseDir = temp.newFolder(); + + builder = ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project"); + } + + @After + public void stop() { + tester.stop(); + } + + @Test + public void testDuplications() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\nfoo\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, duplicatedStuff); + + TaskResult result = tester.newTask() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.verbose", "true") + .build()) + .start(); + + assertThat(result.inputFiles()).hasSize(2); + + // 4 measures per file + assertThat(result.measures()).hasSize(8); + + InputFile inputFile = result.inputFiles().get(0); + // One clone group + List<DuplicationGroup> duplicationGroups = result.duplicationsFor(inputFile); + assertThat(duplicationGroups).hasSize(1); + + DuplicationGroup cloneGroup = duplicationGroups.get(0); + assertThat(cloneGroup.duplicates()).hasSize(1); + assertThat(cloneGroup.originBlock().startLine()).isEqualTo(1); + assertThat(cloneGroup.originBlock().length()).isEqualTo(17); + } + +} |