diff options
author | Klaudio Sinani <klaudio.sinani@sonarsource.com> | 2022-04-13 21:05:57 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-04-21 20:02:49 +0000 |
commit | b5e4dea47b838beafcad61ace24aea6a2d23ed1c (patch) | |
tree | e9ca3dc3588d571658bf7583fddbd6591f94ccfb /sonar-scanner-engine/src | |
parent | 0599d3bbc85be6a96a18cd67ca84bc0c236c5f51 (diff) | |
download | sonarqube-b5e4dea47b838beafcad61ace24aea6a2d23ed1c.tar.gz sonarqube-b5e4dea47b838beafcad61ace24aea6a2d23ed1c.zip |
MMF-2692 Initial git blame command implementation
Diffstat (limited to 'sonar-scanner-engine/src')
-rw-r--r-- | sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java | 175 | ||||
-rw-r--r-- | sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java | 51 |
2 files changed, 226 insertions, 0 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java new file mode 100644 index 00000000000..856a1503ec9 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java @@ -0,0 +1,175 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 02110-1301, USA. + */ +package org.sonar.scm.git; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.batch.scm.BlameLine; + +import static java.util.Objects.requireNonNull; + +public class GitBlameCommand { + private static final String AUTHOR = "author"; + private static final String COMMIT = "commit"; + private static final String TIMESTAMP = "timestamp"; + private static final String TIMEZONE = "timezone"; + private static final String LINE = "line"; + + private static final String GIT_COMMAND = "git"; + private static final String BLAME_COMMAND = "blame"; + private static final String BLAME_LONG_FLAG = "-l"; + private static final String BLAME_SHOW_EMAIL_FLAG = "--show-email"; + private static final String BLAME_TIMESTAMP_FLAG = "-t"; + + public static List<BlameLine> executeCommand(Path directory, String... command) throws IOException, InterruptedException { + requireNonNull(directory, "directory"); + + if (!Files.exists(directory)) { + throw new RuntimeException("Directory does not exist, unable to run git operations:'" + directory + "'"); + } + + ProcessBuilder pb = new ProcessBuilder() + .command(command) + .directory(directory.toFile()); + + Process p = pb.start(); + + List<String> commandOutput = new ArrayList<>(); + InputStream processStdOutput = p.getInputStream(); + + try (BufferedReader br = new BufferedReader(new InputStreamReader(processStdOutput))) { + String outputLine; + + while ((outputLine = br.readLine()) != null) { + commandOutput.add(outputLine); + } + + int exit = p.waitFor(); + + if (exit != 0) { + throw new AssertionError(String.format("Command execution exited with code: %d", exit)); + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + p.destroy(); + } + + return commandOutput + .stream() + .map(GitBlameCommand::parseBlameLine) + .collect(Collectors.toList()); + } + + private static Map<String, String> getBlameAuthoringData(String blameLine) { + String[] blameLineFormatted = blameLine.trim().split("\\s+", 2); + + String commit = blameLineFormatted[0]; + + if (commit.length() != 40) { + throw new IllegalStateException(String.format("Failed to fetch correct commit hash, must be of length 40: %s", commit)); + } + + String authoringData = StringUtils.substringBetween(blameLineFormatted[1], "(", ")"); + String[] authoringDataFormatted = authoringData.trim().split("\\s+", 4); + + String author = StringUtils.substringBetween(authoringDataFormatted[0], "<", ">"); + String timestamp = authoringDataFormatted[1]; + String timezone = authoringDataFormatted[2]; + String line = authoringDataFormatted[3]; + + Map<String, String> blameData = new HashMap<>(); + + blameData.put(COMMIT, commit); + blameData.put(AUTHOR, author); + blameData.put(TIMESTAMP, timestamp); + blameData.put(TIMEZONE, timezone); + blameData.put(LINE, line); + + return blameData; + } + + private static BlameLine parseBlameLine(String blameLine) { + Map<String, String> blameData = getBlameAuthoringData(blameLine); + + return new BlameLine() + .date(new Date(Long.parseLong(blameData.get(TIMESTAMP)))) // should also take timezone into consideration + .revision(blameData.get(COMMIT)) + .author(blameData.get(AUTHOR)); + } + + public static void gitInit(Path directory) throws IOException, InterruptedException { + executeCommand(directory, "git", "init"); + } + + public static void gitStage(Path directory) throws IOException, InterruptedException { + executeCommand(directory, "git", "add", "-A"); + } + + public static void gitCommit(Path directory, String message) throws IOException, InterruptedException { + executeCommand(directory, GIT_COMMAND, COMMIT, "-m", message); + } + + public static void gitClone(Path directory, String originUrl) throws IOException, InterruptedException { + executeCommand(directory.getParent(), "git", "clone", originUrl, directory.getFileName().toString()); + } + + public static List<BlameLine> gitBlame(Path directory, String fileName) throws IOException, InterruptedException { + return executeCommand(directory, GIT_COMMAND, BLAME_COMMAND, BLAME_LONG_FLAG, BLAME_SHOW_EMAIL_FLAG, BLAME_TIMESTAMP_FLAG, fileName); + } + + private static class StreamGobbler extends Thread { + private final InputStream is; + private final String type; + + private StreamGobbler(InputStream is, String type) { + this.is = is; + this.type = type; + } + + @Override + public void run() { + try (BufferedReader br = new BufferedReader(new InputStreamReader(is));) { + List<String> commandOutput = new ArrayList<>(); + String outputLine; + + while ((outputLine = br.readLine()) != null) { + commandOutput.add(outputLine); + System.out.println(type + "> " + outputLine); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java new file mode 100644 index 00000000000..2785ceeb7ad --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java @@ -0,0 +1,51 @@ +package org.sonar.scm.git; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.Test; +import org.sonar.api.batch.scm.BlameLine; + +import static org.sonar.scm.git.GitBlameCommand.gitBlame; +import static org.sonar.scm.git.GitBlameCommand.gitClone; +import static org.sonar.scm.git.GitBlameCommand.gitCommit; +import static org.sonar.scm.git.GitBlameCommand.gitInit; +import static org.sonar.scm.git.GitBlameCommand.gitStage; +import static org.assertj.core.api.Assertions.assertThat; + +public class GitBlameCommandTest { + private static final String ORIGIN_URL = "https://github.com/klaussinani/taskbook.git"; + + @Test + public void testBlame() throws IOException, InterruptedException { + String tmpDirectory = Files.createTempDirectory("tmpDirectory").toFile().getAbsolutePath(); + Path directory = Paths.get(tmpDirectory); + gitClone(directory, ORIGIN_URL); + List<BlameLine> blameOutput = gitBlame(directory, "readme.md"); + assertThat(blameOutput.size()).isEqualTo(378); + } + + @Test + public void initAndAddFile() throws IOException, InterruptedException { + String tmpDirectory = Files.createTempDirectory("tmpDirectory").toFile().getAbsolutePath(); + Path directory = Paths.get(tmpDirectory); + Files.createDirectories(directory); + gitInit(directory); + Files.write(directory.resolve("bar.c"), new byte[0]); + gitStage(directory); + gitCommit(directory, "Add bar.c"); + } + + @Test + public void cloneAndAddFile() throws IOException, InterruptedException { + String tmpDirectory = Files.createTempDirectory("tmpDirectory").toFile().getAbsolutePath(); + Path directory = Paths.get(tmpDirectory); + gitClone(directory, ORIGIN_URL); + Files.write(directory.resolve("bar.c"), new byte[0]); + gitStage(directory); + gitCommit(directory, "Add bar.c"); + // gitPush(directory); // don't push + } +}
\ No newline at end of file |