aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-scanner-engine
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2022-04-13 17:42:07 -0500
committersonartech <sonartech@sonarsource.com>2022-04-21 20:02:49 +0000
commit4e770aa94bc813d6c8598eb86ec283fd48dfd970 (patch)
tree87a8ba59ca49b46faedbade45c26198b6352afec /sonar-scanner-engine
parentb5e4dea47b838beafcad61ace24aea6a2d23ed1c (diff)
downloadsonarqube-4e770aa94bc813d6c8598eb86ec283fd48dfd970.tar.gz
sonarqube-4e770aa94bc813d6c8598eb86ec283fd48dfd970.zip
SONAR-16290 Use native git to collect blame information
Diffstat (limited to 'sonar-scanner-engine')
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java119
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java211
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java8
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitBlameCommand.java82
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandTest.java287
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java187
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java27
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmSupportTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitUtils.java44
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java295
11 files changed, 782 insertions, 482 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
new file mode 100644
index 00000000000..83223959ee8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
@@ -0,0 +1,119 @@
+/*
+ * 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.File;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Repository;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.scm.BlameCommand;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.notifications.AnalysisWarnings;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+public class CompositeBlameCommand extends BlameCommand {
+ private static final Logger LOG = Loggers.get(CompositeBlameCommand.class);
+
+ private final AnalysisWarnings analysisWarnings;
+ private final PathResolver pathResolver;
+ private final JGitBlameCommand jgitCmd;
+ private final GitBlameCommand nativeCmd;
+ private boolean nativeGitEnabled = false;
+
+ public CompositeBlameCommand(AnalysisWarnings analysisWarnings, PathResolver pathResolver, JGitBlameCommand jgitCmd, GitBlameCommand nativeCmd) {
+ this.analysisWarnings = analysisWarnings;
+ this.pathResolver = pathResolver;
+ this.jgitCmd = jgitCmd;
+ this.nativeCmd = nativeCmd;
+ }
+
+ @Override
+ public void blame(BlameInput input, BlameOutput output) {
+ File basedir = input.fileSystem().baseDir();
+ try (Repository repo = JGitUtils.buildRepository(basedir.toPath()); Git git = Git.wrap(repo)) {
+ File gitBaseDir = repo.getWorkTree();
+ if (cloneIsInvalid(gitBaseDir)) {
+ return;
+ }
+ nativeGitEnabled = nativeCmd.isEnabled(basedir.toPath());
+ Stream<InputFile> stream = StreamSupport.stream(input.filesToBlame().spliterator(), true);
+ ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory(), null, false);
+ // exceptions thrown by the blame method will be ignored
+ forkJoinPool.submit(() -> stream.forEach(inputFile -> blame(output, git, gitBaseDir, inputFile)));
+ try {
+ forkJoinPool.shutdown();
+ forkJoinPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ LOG.info("Git blame interrupted");
+ }
+ }
+ }
+
+ private void blame(BlameOutput output, Git git, File gitBaseDir, InputFile inputFile) {
+ String filename = pathResolver.relativePath(gitBaseDir, inputFile.file());
+ LOG.debug("Blame file {}", filename);
+ List<BlameLine> blame = null;
+ if (nativeGitEnabled) {
+ try {
+ blame = nativeCmd.blame(gitBaseDir.toPath(), filename);
+ } catch (Exception e) {
+ // fallback to jgit
+ }
+ }
+
+ if (blame == null) {
+ blame = jgitCmd.blame(git, filename);
+ }
+
+ if (!blame.isEmpty()) {
+ if (blame.size() == inputFile.lines() - 1) {
+ // SONARPLUGINS-3097 Git do not report blame on last empty line
+ blame.add(blame.get(blame.size() - 1));
+ }
+ output.blameResult(inputFile, blame);
+ }
+ }
+
+ private boolean cloneIsInvalid(File gitBaseDir) {
+ if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/objects/info/alternates"))) {
+ LOG.info("This git repository references another local repository which is not well supported. SCM information might be missing for some files. "
+ + "You can avoid borrow objects from another local repository by not using --reference or --shared when cloning it.");
+ }
+
+ if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/shallow"))) {
+ LOG.warn("Shallow clone detected, no blame information will be provided. "
+ + "You can convert to non-shallow with 'git fetch --unshallow'.");
+ analysisWarnings.addUnique("Shallow clone detected during the analysis. "
+ + "Some files will miss SCM information. This will affect features like auto-assignment of issues. "
+ + "Please configure your build to disable shallow clone.");
+ return true;
+ }
+
+ return false;
+ }
+}
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
index 856a1503ec9..3fffbe0ea83 100644
--- 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
@@ -20,156 +20,143 @@
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.time.Instant;
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 java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.springframework.beans.factory.annotation.Autowired;
-import static java.util.Objects.requireNonNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static org.sonar.api.utils.Preconditions.checkState;
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 Logger LOG = Loggers.get(GitBlameCommand.class);
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("<(\\S*?)>");
+ private static final String COMITTER_TIME = "committer-time ";
+ private static final String COMITTER_MAIL = "committer-mail ";
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";
+ private static final String BLAME_LINE_PORCELAIN_FLAG = "--line-porcelain";
+ private static final String IGNORE_WHITESPACES = "-w";
- public static List<BlameLine> executeCommand(Path directory, String... command) throws IOException, InterruptedException {
- requireNonNull(directory, "directory");
+ private final String gitCommand;
- if (!Files.exists(directory)) {
- throw new RuntimeException("Directory does not exist, unable to run git operations:'" + directory + "'");
+ @Autowired
+ public GitBlameCommand() {
+ this(GIT_COMMAND);
+ }
+
+ public GitBlameCommand(String gitCommand) {
+ this.gitCommand = gitCommand;
+ }
+
+ public boolean isEnabled(Path baseDir) {
+ try {
+ MutableString stdOut = new MutableString();
+ executeCommand(baseDir, l -> stdOut.string = l, gitCommand, "--version");
+ return stdOut.string != null && stdOut.string.startsWith("git version");
+ } catch (Exception e) {
+ LOG.debug("Failed to find git native client", e);
+ return false;
+ }
+ }
+
+ public List<BlameLine> blame(Path baseDir, String fileName) {
+ BlameOutputProcessor outputProcessor = new BlameOutputProcessor();
+ try {
+ executeCommand(baseDir, outputProcessor::process, gitCommand, BLAME_COMMAND, BLAME_LINE_PORCELAIN_FLAG, IGNORE_WHITESPACES, fileName);
+ return outputProcessor.getBlameLines();
+ } catch (Exception e) {
+ LOG.debug("Blame failed for " + fileName, e);
+ return emptyList();
}
+ }
+ private void executeCommand(Path baseDir, Consumer<String> stdOutLineConsumer, String... command) throws Exception {
ProcessBuilder pb = new ProcessBuilder()
.command(command)
- .directory(directory.toFile());
+ .directory(baseDir.toFile());
Process p = pb.start();
-
- List<String> commandOutput = new ArrayList<>();
- InputStream processStdOutput = p.getInputStream();
-
- try (BufferedReader br = new BufferedReader(new InputStreamReader(processStdOutput))) {
+ try {
+ InputStream processStdOutput = p.getInputStream();
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(processStdOutput, UTF_8))) {
String outputLine;
-
while ((outputLine = br.readLine()) != null) {
- commandOutput.add(outputLine);
+ stdOutLineConsumer.accept(outputLine);
}
+ }
- int exit = p.waitFor();
-
- if (exit != 0) {
- throw new AssertionError(String.format("Command execution exited with code: %d", exit));
- }
+ int exit = p.waitFor();
+ if (exit != 0) {
+ throw new IllegalStateException(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];
+ private static class BlameOutputProcessor {
+ private final List<BlameLine> blameLines = new ArrayList<>();
+ private String sha1;
+ private String committerTime;
+ private String committerMail;
- if (commit.length() != 40) {
- throw new IllegalStateException(String.format("Failed to fetch correct commit hash, must be of length 40: %s", commit));
+ public List<BlameLine> getBlameLines() {
+ return unmodifiableList(blameLines);
}
- 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;
+ public void process(String line) {
+ if (sha1 == null) {
+ sha1 = line.split(" ")[0];
+ } else if (line.startsWith("\t")) {
+ saveEntry();
+ } else if (line.startsWith(COMITTER_TIME)) {
+ committerTime = line.substring(COMITTER_TIME.length());
+ } else if (line.startsWith(COMITTER_MAIL)) {
+ Matcher matcher = EMAIL_PATTERN.matcher(line);
+ if (!matcher.find(COMITTER_MAIL.length()) || matcher.groupCount() != 1) {
+ throw new IllegalStateException("Couldn't parse committer email from: " + line);
+ }
+ committerMail = matcher.group(1);
+ if (committerMail.equals("not.committed.yet")) {
+ throw new IllegalStateException("Uncommitted line found");
+ }
+ }
}
- @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();
+ private void saveEntry() {
+ checkState(committerMail != null, "Did not find a committer email for an entry");
+ checkState(committerTime != null, "Did not find a committer time for an entry");
+ checkState(sha1 != null, "Did not find a commit sha1 for an entry");
+ try {
+ blameLines.add(new BlameLine()
+ .revision(sha1)
+ .author(committerMail)
+ .date(Date.from(Instant.ofEpochSecond(Long.parseLong(committerTime)))));
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Invalid committer time found: " + committerTime);
}
+ committerMail = null;
+ sha1 = null;
+ committerTime = null;
}
}
+ private static class MutableString {
+ String string;
+ }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
index e66e4f79303..bef15b669bc 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
@@ -64,14 +64,14 @@ public class GitScmProvider extends ScmProvider {
private static final Logger LOG = Loggers.get(GitScmProvider.class);
private static final String COULD_NOT_FIND_REF = "Could not find ref '%s' in refs/heads, refs/remotes, refs/remotes/upstream or refs/remotes/origin";
- private final JGitBlameCommand jgitBlameCommand;
+ private final BlameCommand blameCommand;
private final AnalysisWarnings analysisWarnings;
private final GitIgnoreCommand gitIgnoreCommand;
private final System2 system2;
private final String documentationLink;
- public GitScmProvider(JGitBlameCommand jgitBlameCommand, AnalysisWarnings analysisWarnings, GitIgnoreCommand gitIgnoreCommand, System2 system2) {
- this.jgitBlameCommand = jgitBlameCommand;
+ public GitScmProvider(CompositeBlameCommand blameCommand, AnalysisWarnings analysisWarnings, GitIgnoreCommand gitIgnoreCommand, System2 system2) {
+ this.blameCommand = blameCommand;
this.analysisWarnings = analysisWarnings;
this.gitIgnoreCommand = gitIgnoreCommand;
this.system2 = system2;
@@ -96,7 +96,7 @@ public class GitScmProvider extends ScmProvider {
@Override
public BlameCommand blameCommand() {
- return this.jgitBlameCommand;
+ return this.blameCommand;
}
@CheckForNull
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java
index 2d412fe74b2..a67bd56320d 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java
@@ -32,6 +32,8 @@ public final class GitScmSupport {
FS.FileStoreAttributes.setBackground(true);
return Arrays.asList(
JGitBlameCommand.class,
+ CompositeBlameCommand.class,
+ GitBlameCommand.class,
GitScmProvider.class,
GitIgnoreCommand.class);
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitBlameCommand.java
index f939ceb28e4..e9868a03c52 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitBlameCommand.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitBlameCommand.java
@@ -19,80 +19,21 @@
*/
package org.sonar.scm.git;
-import java.io.File;
-import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.blame.BlameResult;
import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.lib.Repository;
-import org.sonar.api.batch.fs.InputFile;
-import org.sonar.api.batch.scm.BlameCommand;
import org.sonar.api.batch.scm.BlameLine;
-import org.sonar.api.notifications.AnalysisWarnings;
-import org.sonar.api.scan.filesystem.PathResolver;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
-public class JGitBlameCommand extends BlameCommand {
+import static java.util.Collections.emptyList;
+public class JGitBlameCommand {
private static final Logger LOG = Loggers.get(JGitBlameCommand.class);
- private final PathResolver pathResolver;
- private final AnalysisWarnings analysisWarnings;
-
- public JGitBlameCommand(PathResolver pathResolver, AnalysisWarnings analysisWarnings) {
- this.pathResolver = pathResolver;
- this.analysisWarnings = analysisWarnings;
- }
-
- @Override
- public void blame(BlameInput input, BlameOutput output) {
- File basedir = input.fileSystem().baseDir();
- try (Repository repo = JGitUtils.buildRepository(basedir.toPath()); Git git = Git.wrap(repo)) {
- File gitBaseDir = repo.getWorkTree();
-
- if (cloneIsInvalid(gitBaseDir)) {
- return;
- }
-
- Stream<InputFile> stream = StreamSupport.stream(input.filesToBlame().spliterator(), true);
- ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory(), null, false);
- forkJoinPool.submit(() -> stream.forEach(inputFile -> blame(output, git, gitBaseDir, inputFile)));
- try {
- forkJoinPool.shutdown();
- forkJoinPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- LOG.info("Git blame interrupted");
- }
- }
- }
-
- private boolean cloneIsInvalid(File gitBaseDir) {
- if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/objects/info/alternates"))) {
- LOG.info("This git repository references another local repository which is not well supported. SCM information might be missing for some files. "
- + "You can avoid borrow objects from another local repository by not using --reference or --shared when cloning it.");
- }
-
- if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/shallow"))) {
- LOG.warn("Shallow clone detected, no blame information will be provided. "
- + "You can convert to non-shallow with 'git fetch --unshallow'.");
- analysisWarnings.addUnique("Shallow clone detected during the analysis. "
- + "Some files will miss SCM information. This will affect features like auto-assignment of issues. "
- + "Please configure your build to disable shallow clone.");
- return true;
- }
-
- return false;
- }
-
- private void blame(BlameOutput output, Git git, File gitBaseDir, InputFile inputFile) {
- String filename = pathResolver.relativePath(gitBaseDir, inputFile.file());
+ public List<BlameLine> blame(Git git, String filename) {
LOG.debug("Blame file {}", filename);
BlameResult blameResult;
try {
@@ -101,29 +42,26 @@ public class JGitBlameCommand extends BlameCommand {
.setTextComparator(RawTextComparator.WS_IGNORE_ALL)
.setFilePath(filename).call();
} catch (Exception e) {
- throw new IllegalStateException("Unable to blame file " + inputFile.relativePath(), e);
+ throw new IllegalStateException("Unable to blame file " + filename, e);
}
List<BlameLine> lines = new ArrayList<>();
if (blameResult == null) {
- LOG.debug("Unable to blame file {}. It is probably a symlink.", inputFile.relativePath());
- return;
+ LOG.debug("Unable to blame file {}. It is probably a symlink.", filename);
+ return emptyList();
}
for (int i = 0; i < blameResult.getResultContents().size(); i++) {
if (blameResult.getSourceAuthor(i) == null || blameResult.getSourceCommit(i) == null) {
- LOG.debug("Unable to blame file {}. No blame info at line {}. Is file committed? [Author: {} Source commit: {}]", inputFile.relativePath(), i + 1,
+ LOG.debug("Unable to blame file {}. No blame info at line {}. Is file committed? [Author: {} Source commit: {}]", filename, i + 1,
blameResult.getSourceAuthor(i), blameResult.getSourceCommit(i));
- return;
+ return emptyList();
}
lines.add(new BlameLine()
.date(blameResult.getSourceCommitter(i).getWhen())
.revision(blameResult.getSourceCommit(i).getName())
.author(blameResult.getSourceAuthor(i).getEmailAddress()));
}
- if (lines.size() == inputFile.lines() - 1) {
- // SONARPLUGINS-3097 Git do not report blame on last empty line
- lines.add(lines.get(lines.size() - 1));
- }
- output.blameResult(inputFile, lines);
+
+ return lines;
}
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandTest.java
new file mode 100644
index 00000000000..af610eae2c5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.scm.BlameCommand;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.notifications.AnalysisWarnings;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.startsWith;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.scm.git.Utils.javaUnzip;
+
+public class CompositeBlameCommandTest {
+ private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java";
+ private final PathResolver pathResolver = new PathResolver();
+ private final JGitBlameCommand jGitBlameCommand = new JGitBlameCommand();
+ private final GitBlameCommand gitBlameCommand = new GitBlameCommand();
+ private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
+ private final CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitBlameCommand);
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+ private final BlameCommand.BlameInput input = mock(BlameCommand.BlameInput.class);
+
+ @Test
+ public void use_jgit_if_native_git_disabled() throws IOException {
+ GitBlameCommand gitCmd = new GitBlameCommand("invalidcommandnotfound");
+ BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd);
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "dummy-git");
+ setUpBlameInputWithFile(baseDir.toPath());
+ TestBlameOutput output = new TestBlameOutput();
+ blameCmd.blame(input, output);
+ assertThat(output.blame).hasSize(1);
+ assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29);
+ }
+
+ @Test
+ public void fallback_to_jgit_if_native_git_fails() throws IOException {
+ GitBlameCommand gitCmd = mock(GitBlameCommand.class);
+ BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd);
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "dummy-git");
+ when(gitCmd.isEnabled(baseDir.toPath())).thenReturn(true);
+ when(gitCmd.blame(baseDir.toPath(), DUMMY_JAVA)).thenThrow(new IllegalStateException());
+ setUpBlameInputWithFile(baseDir.toPath());
+ TestBlameOutput output = new TestBlameOutput();
+ blameCmd.blame(input, output);
+ assertThat(output.blame).hasSize(1);
+ assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29);
+ }
+
+ @Test
+ public void use_native_git_by_default() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+ File baseDir = new File(projectDir, "dummy-git");
+
+ // skip test if git is not installed
+ assumeTrue(gitBlameCommand.isEnabled(baseDir.toPath()));
+
+ JGitBlameCommand jgit = mock(JGitBlameCommand.class);
+ BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jgit, gitBlameCommand);
+
+ setUpBlameInputWithFile(baseDir.toPath());
+ TestBlameOutput output = new TestBlameOutput();
+ blameCmd.blame(input, output);
+ assertThat(output.blame).hasSize(1);
+ assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29);
+ verifyNoInteractions(jgit);
+ }
+
+ @Test
+ public void return_early_when_shallow_clone_detected() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("shallow-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "shallow-git");
+
+ setUpBlameInputWithFile(baseDir.toPath());
+
+ // register warning with default wrapper
+ BlameCommand.BlameOutput output = mock(BlameCommand.BlameOutput.class);
+ blameCommand.blame(input, output);
+
+ assertThat(logTester.logs()).first()
+ .matches(s -> s.contains("Shallow clone detected, no blame information will be provided."));
+ verifyNoInteractions(output);
+
+ verify(analysisWarnings).addUnique(startsWith("Shallow clone detected"));
+ }
+
+ @Test
+ public void fail_if_not_git_project() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "dummy-git");
+
+ // Delete .git
+ FileUtils.forceDelete(new File(baseDir, ".git"));
+
+ setUpBlameInputWithFile(baseDir.toPath());
+
+ BlameCommand.BlameOutput blameResult = mock(BlameCommand.BlameOutput.class);
+
+ assertThatThrownBy(() -> blameCommand.blame(input, blameResult))
+ .isInstanceOf(MessageException.class)
+ .hasMessageContaining("Not inside a Git work tree: ");
+ }
+
+ @Test
+ public void dont_fail_with_symlink() throws IOException {
+ assumeTrue(!System2.INSTANCE.isOsWindows());
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "dummy-git");
+ String relativePath2 = "src/main/java/org/dummy/Dummy2.java";
+ DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA)
+ .setModuleBaseDir(baseDir.toPath())
+ .build();
+ DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", relativePath2)
+ .setModuleBaseDir(baseDir.toPath())
+ .build();
+
+ // Create symlink
+ Files.createSymbolicLink(inputFile2.file().toPath(), inputFile.file().toPath());
+
+ when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2));
+ TestBlameOutput output = new TestBlameOutput();
+ blameCommand.blame(input, output);
+ }
+
+
+ @Test
+ public void return_early_when_clone_with_reference_detected() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git-reference-clone.zip", projectDir);
+
+ Path baseDir = projectDir.toPath().resolve("dummy-git2");
+
+ DefaultFileSystem fs = new DefaultFileSystem(baseDir);
+ when(input.fileSystem()).thenReturn(fs);
+
+ DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).setModuleBaseDir(baseDir).build();
+ when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile));
+
+ // register warning
+ TestBlameOutput output = new TestBlameOutput();
+ blameCommand.blame(input, output);
+
+ assertThat(logTester.logs()).first()
+ .matches(s -> s.contains("This git repository references another local repository which is not well supported"));
+
+ // contains commits referenced from the old clone and commits in the new clone
+ assertThat(output.blame).containsKey(inputFile);
+ assertThat(output.blame.get(inputFile).stream().map(BlameLine::revision))
+ .containsOnly("6b3aab35a3ea32c1636fee56f996e677653c48ea", "843c7c30d7ebd9a479e8f1daead91036c75cbc4e", "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7");
+ verifyNoInteractions(analysisWarnings);
+ }
+
+ @Test
+ public void blame_on_nested_module() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git-nested.zip", projectDir);
+ File baseDir = new File(projectDir, "dummy-git-nested/dummy-project");
+ DefaultFileSystem fs = new DefaultFileSystem(baseDir);
+ when(input.fileSystem()).thenReturn(fs);
+ DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA)
+ .setModuleBaseDir(baseDir.toPath())
+ .build();
+ fs.add(inputFile);
+
+ BlameCommand.BlameOutput blameResult = mock(BlameCommand.BlameOutput.class);
+ when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
+ blameCommand.blame(input, blameResult);
+
+ Date revisionDate = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
+ String revision = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
+ String author = "david@gageot.net";
+ verify(blameResult).blameResult(inputFile,
+ Arrays.asList(
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author),
+ new BlameLine().revision(revision).date(revisionDate).author(author)));
+ }
+
+ private BlameCommand.BlameInput setUpBlameInputWithFile(Path baseDir) {
+ DefaultFileSystem fs = new DefaultFileSystem(baseDir);
+ when(input.fileSystem()).thenReturn(fs);
+
+ DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).setModuleBaseDir(baseDir).build();
+ when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile));
+ return input;
+ }
+
+ private File createNewTempFolder() throws IOException {
+ //This is needed for Windows, otherwise the created File point to invalid (shortened by Windows) temp folder path
+ return temp.newFolder().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toFile();
+ }
+
+ private static class TestBlameOutput implements BlameCommand.BlameOutput {
+ private final Map<InputFile, List<BlameLine>> blame = new LinkedHashMap<>();
+
+ @Override public void blameResult(InputFile inputFile, List<BlameLine> list) {
+ blame.put(inputFile, list);
+ }
+ }
+}
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
index 2785ceeb7ad..0de3252668c 100644
--- 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
@@ -1,51 +1,178 @@
+/*
+ * 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.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.Date;
+import java.util.LinkedList;
import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
-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;
+import static org.junit.Assume.assumeTrue;
+import static org.sonar.scm.git.GitUtils.createFile;
+import static org.sonar.scm.git.GitUtils.createRepository;
+import static org.sonar.scm.git.Utils.javaUnzip;
public class GitBlameCommandTest {
- private static final String ORIGIN_URL = "https://github.com/klaussinani/taskbook.git";
+ private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java";
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public LogTester logTester = new LogTester();
+ private final GitBlameCommand blameCommand = new GitBlameCommand();
+
+ @Test
+ public void blame_collects_all_lines() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+ File baseDir = new File(projectDir, "dummy-git");
+
+ List<BlameLine> blame = blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
+
+ Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
+ String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
+ String author1 = "david@gageot.net";
+
+ // second commit, which has a commit date different than the author date
+ Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200");
+ String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7";
+ String author2 = "duarte.meneses@sonarsource.com";
+
+ List<BlameLine> expectedBlame = new LinkedList<>();
+ for (int i = 0; i < 25; i++) {
+ expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+ }
+ for (int i = 0; i < 3; i++) {
+ expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
+ }
+ for (int i = 0; i < 1; i++) {
+ expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+ }
+
+ assertThat(blame).isEqualTo(expectedBlame);
+ }
@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);
+ public void modified_file_returns_no_blame() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ Path baseDir = projectDir.toPath().resolve("dummy-git");
+
+ // Emulate a modification
+ Files.write(baseDir.resolve(DUMMY_JAVA), "modification and \n some new line".getBytes());
+
+ assertThat(blameCommand.blame(baseDir, DUMMY_JAVA)).isEmpty();
}
@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");
+ public void new_file_returns_no_blame() throws IOException {
+ File projectDir = createNewTempFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ File baseDir = new File(projectDir, "dummy-git");
+ String relativePath2 = "src/main/java/org/dummy/Dummy2.java";
+
+ // Emulate a new file
+ FileUtils.copyFile(new File(baseDir, DUMMY_JAVA), new File(baseDir, relativePath2));
+
+ assertThat(blameCommand.blame(baseDir.toPath(), DUMMY_JAVA)).hasSize(29);
+ assertThat(blameCommand.blame(baseDir.toPath(), relativePath2)).isEmpty();
}
@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
+ public void symlink_doesnt_fail() throws IOException {
+ assumeTrue(!System2.INSTANCE.isOsWindows());
+ File projectDir = temp.newFolder();
+ javaUnzip("dummy-git.zip", projectDir);
+
+ Path baseDir = projectDir.toPath().resolve("dummy-git");
+ String relativePath2 = "src/main/java/org/dummy/Dummy2.java";
+
+ // Create symlink
+ Files.createSymbolicLink(baseDir.resolve(relativePath2), baseDir.resolve(DUMMY_JAVA));
+
+ blameCommand.blame(baseDir, DUMMY_JAVA);
+ blameCommand.blame(baseDir, relativePath2);
+ }
+
+ @Test
+ public void git_should_be_detected() throws IOException {
+ Path baseDir = temp.newFolder().toPath();
+ GitBlameCommand blameCommand = new GitBlameCommand();
+ assertThat(blameCommand.isEnabled(baseDir)).isTrue();
+ }
+
+ @Test
+ public void git_should_not_be_detected() throws IOException {
+ Path baseDir = temp.newFolder().toPath();
+ GitBlameCommand blameCommand = new GitBlameCommand("randomcmdthatwillneverbefound");
+ assertThat(blameCommand.isEnabled(baseDir)).isFalse();
+ }
+
+ @Test
+ public void return_empty_if_command_fails() throws IOException {
+ Path baseDir = temp.newFolder().toPath();
+ GitBlameCommand blameCommand = new GitBlameCommand("randomcmdthatwillneverbefound");
+ assertThat(blameCommand.blame(baseDir, "file")).isEmpty();
+ }
+
+ @Test
+ public void blame_without_email_doesnt_fail() throws IOException, GitAPIException {
+ Path baseDir = temp.newFolder().toPath();
+ Git git = createRepository(baseDir);
+ String filePath = "file.txt";
+ createFile(filePath, "line", baseDir);
+ commitWithNoEamil(git, filePath);
+
+ GitBlameCommand blameCommand = new GitBlameCommand();
+ List<BlameLine> blame = blameCommand.blame(baseDir, filePath);
+ assertThat(blame).hasSize(1);
+ BlameLine blameLine = blame.get(0);
+ assertThat(blameLine.author()).isNull();
+ assertThat(blameLine.revision()).isNotNull();
+ assertThat(blameLine.date()).isNotNull();
+ }
+
+ private void commitWithNoEamil(Git git, String path) throws GitAPIException {
+ git.add().addFilepattern(path).call();
+ git.commit().setCommitter("joe", "").setMessage("msg").call();
+ }
+
+ private File createNewTempFolder() throws IOException {
+ //This is needed for Windows, otherwise the created File point to invalid (shortened by Windows) temp folder path
+ return temp.newFolder().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toFile();
}
-} \ No newline at end of file
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
index bb626c5f01f..d7f5006e035 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
@@ -28,7 +28,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
-import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -73,6 +72,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.api.utils.log.LoggerLevel.WARN;
+import static org.sonar.scm.git.GitUtils.createFile;
+import static org.sonar.scm.git.GitUtils.createRepository;
import static org.sonar.scm.git.Utils.javaUnzip;
public class GitScmProviderTest {
@@ -136,8 +137,10 @@ public class GitScmProviderTest {
@Test
public void returnImplem() {
- JGitBlameCommand jblameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings);
- GitScmProvider gitScmProvider = new GitScmProvider(jblameCommand, analysisWarnings, gitIgnoreCommand, system2);
+ JGitBlameCommand jblameCommand = new JGitBlameCommand();
+ GitBlameCommand nativeBlameCommand = new GitBlameCommand();
+ CompositeBlameCommand compositeBlameCommand = new CompositeBlameCommand(analysisWarnings, new PathResolver(), jblameCommand, nativeBlameCommand);
+ GitScmProvider gitScmProvider = new GitScmProvider(compositeBlameCommand, analysisWarnings, gitIgnoreCommand, system2);
assertThat(gitScmProvider.blameCommand()).isEqualTo(jblameCommand);
}
@@ -176,8 +179,8 @@ public class GitScmProviderTest {
assertThat(newScmProvider().supports(baseDir)).isTrue();
}
- private static JGitBlameCommand mockCommand() {
- return mock(JGitBlameCommand.class);
+ private static CompositeBlameCommand mockCommand() {
+ return mock(CompositeBlameCommand.class);
}
@Test
@@ -305,12 +308,6 @@ public class GitScmProviderTest {
assertThat(changedLines.entrySet().iterator().next().getValue()).containsOnly(4, 5, 6);
}
- private Git createRepository(Path worktree) throws IOException {
- Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
- repo.create();
- return new Git(repo);
- }
-
private void addSubmodule(Git mainGit, String submoduleName, String uriToSubmodule) throws GitAPIException {
mainGit.submoduleAdd().setPath(submoduleName).setURI(uriToSubmodule).call();
mainGit.commit().setAuthor("joe", "joe@example.com").setMessage("adding submodule").call();
@@ -663,7 +660,7 @@ public class GitScmProviderTest {
}
private GitScmProvider newGitScmProvider() {
- return new GitScmProvider(mock(JGitBlameCommand.class), analysisWarnings, gitIgnoreCommand, system2);
+ return new GitScmProvider(mock(CompositeBlameCommand.class), analysisWarnings, gitIgnoreCommand, system2);
}
@Test
@@ -760,12 +757,6 @@ public class GitScmProviderTest {
commit(git, relativePath);
}
- private void createFile(String relativePath, String content, Path worktree) throws IOException {
- Path newFile = worktree.resolve(relativePath);
- Files.createDirectories(newFile.getParent());
- Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE);
- }
-
private void addLineToFile(String relativePath, int lineNumber) throws IOException {
Path filePath = worktree.resolve(relativePath);
List<String> lines = Files.readAllLines(filePath);
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmSupportTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmSupportTest.java
index 41fe48f9d34..b3749f9e8e1 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmSupportTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmSupportTest.java
@@ -27,7 +27,7 @@ public class GitScmSupportTest {
@Test
public void getClasses() {
- assertThat(GitScmSupport.getObjects()).hasSize(3);
+ assertThat(GitScmSupport.getObjects()).isNotEmpty();
}
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitUtils.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitUtils.java
new file mode 100644
index 00000000000..e47475ad22a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+
+public class GitUtils {
+ public static Git createRepository(Path worktree) throws IOException {
+ Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
+ repo.create();
+ return new Git(repo);
+ }
+
+ public static void createFile(String relativePath, String content, Path worktree) throws IOException {
+ Path newFile = worktree.resolve(relativePath);
+ Files.createDirectories(newFile.getParent());
+ Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE);
+ }
+
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java
index 6ceb80baf7f..2f51e80b77d 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java
@@ -24,39 +24,22 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.Date;
-import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Repository;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-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.fs.internal.TestInputFileBuilder;
-import org.sonar.api.batch.scm.BlameCommand.BlameInput;
-import org.sonar.api.batch.scm.BlameCommand.BlameOutput;
import org.sonar.api.batch.scm.BlameLine;
-import org.sonar.api.notifications.AnalysisWarnings;
-import org.sonar.api.scan.filesystem.PathResolver;
import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.log.LogTester;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assume.assumeTrue;
-import static org.mockito.ArgumentMatchers.startsWith;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.when;
import static org.sonar.scm.git.Utils.javaUnzip;
public class JGitBlameCommandTest {
@@ -69,277 +52,99 @@ public class JGitBlameCommandTest {
@Rule
public LogTester logTester = new LogTester();
- private final BlameInput input = mock(BlameInput.class);
+ private final JGitBlameCommand jGitBlameCommand = new JGitBlameCommand();
@Test
- public void testBlame() throws IOException {
+ public void blame_returns_all_lines() throws IOException {
File projectDir = createNewTempFolder();
javaUnzip("dummy-git.zip", projectDir);
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
File baseDir = new File(projectDir, "dummy-git");
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA)
- .setModuleBaseDir(baseDir.toPath())
- .build();
- fs.add(inputFile);
-
- BlameOutput blameResult = mock(BlameOutput.class);
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
- jGitBlameCommand.blame(input, blameResult);
-
- Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
- String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
- String author1 = "david@gageot.net";
-
- // second commit, which has a commit date different than the author date
- Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200");
- String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7";
- String author2 = "duarte.meneses@sonarsource.com";
- List<BlameLine> expectedBlame = new LinkedList<>();
- for (int i = 0; i < 25; i++) {
- expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+ try (Git git = loadRepository(baseDir.toPath())) {
+ List<BlameLine> blameLines = jGitBlameCommand.blame(git, DUMMY_JAVA);
+
+ Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
+ String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
+ String author1 = "david@gageot.net";
+
+ // second commit, which has a commit date different than the author date
+ Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200");
+ String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7";
+ String author2 = "duarte.meneses@sonarsource.com";
+
+ List<BlameLine> expectedBlame = new LinkedList<>();
+ for (int i = 0; i < 25; i++) {
+ expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+ }
+ for (int i = 0; i < 3; i++) {
+ expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
+ }
+ for (int i = 0; i < 1; i++) {
+ expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+ }
+
+ assertThat(blameLines).isEqualTo(expectedBlame);
}
- for (int i = 0; i < 3; i++) {
- expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
- }
- for (int i = 0; i < 1; i++) {
- expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
- }
-
- verify(blameResult).blameResult(inputFile, expectedBlame);
- }
-
- @Test
- public void properFailureIfNotAGitProject() throws IOException {
- File projectDir = createNewTempFolder();
- javaUnzip("dummy-git.zip", projectDir);
-
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
- File baseDir = new File(projectDir, "dummy-git");
-
- // Delete .git
- FileUtils.forceDelete(new File(baseDir, ".git"));
-
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).build();
- fs.add(inputFile);
-
- BlameOutput blameResult = mock(BlameOutput.class);
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
-
- assertThatThrownBy(() -> jGitBlameCommand.blame(input, blameResult))
- .isInstanceOf(MessageException.class)
- .hasMessageContaining("Not inside a Git work tree: ");
- }
-
- @Test
- public void testBlameOnNestedModule() throws IOException {
- File projectDir = createNewTempFolder();
- javaUnzip("dummy-git-nested.zip", projectDir);
-
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
- File baseDir = new File(projectDir, "dummy-git-nested/dummy-project");
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA)
- .setModuleBaseDir(baseDir.toPath())
- .build();
- fs.add(inputFile);
-
- BlameOutput blameResult = mock(BlameOutput.class);
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
- jGitBlameCommand.blame(input, blameResult);
-
- Date revisionDate = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
- String revision = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
- String author = "david@gageot.net";
- verify(blameResult).blameResult(inputFile,
- Arrays.asList(
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author),
- new BlameLine().revision(revision).date(revisionDate).author(author)));
}
@Test
- public void dontFailOnModifiedFile() throws IOException {
+ public void modified_file_returns_no_blame() throws IOException {
File projectDir = createNewTempFolder();
javaUnzip("dummy-git.zip", projectDir);
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
- File baseDir = new File(projectDir, "dummy-git");
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- String relativePath = DUMMY_JAVA;
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath).build();
- fs.add(inputFile);
+ Path baseDir = projectDir.toPath().resolve("dummy-git");
// Emulate a modification
- Files.write(baseDir.toPath().resolve(relativePath), "modification and \n some new line".getBytes());
-
- BlameOutput blameResult = mock(BlameOutput.class);
+ Files.write(baseDir.resolve(DUMMY_JAVA), "modification and \n some new line".getBytes());
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
- jGitBlameCommand.blame(input, blameResult);
+ try (Git git = loadRepository(baseDir)) {
+ assertThat(jGitBlameCommand.blame(git, DUMMY_JAVA)).isEmpty();
+ }
}
@Test
- public void dontFailOnNewFile() throws IOException {
+ public void new_file_returns_no_blame() throws IOException {
File projectDir = createNewTempFolder();
javaUnzip("dummy-git.zip", projectDir);
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
File baseDir = new File(projectDir, "dummy-git");
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- String relativePath = DUMMY_JAVA;
String relativePath2 = "src/main/java/org/dummy/Dummy2.java";
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath).build();
- fs.add(inputFile);
- DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", relativePath2).build();
- fs.add(inputFile2);
// Emulate a new file
- FileUtils.copyFile(new File(baseDir, relativePath), new File(baseDir, relativePath2));
+ FileUtils.copyFile(new File(baseDir, DUMMY_JAVA), new File(baseDir, relativePath2));
- BlameOutput blameResult = mock(BlameOutput.class);
-
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2));
- jGitBlameCommand.blame(input, blameResult);
+ try (Git git = loadRepository(baseDir.toPath())) {
+ assertThat(jGitBlameCommand.blame(git, DUMMY_JAVA)).hasSize(29);
+ assertThat(jGitBlameCommand.blame(git, relativePath2)).isEmpty();
+ }
}
@Test
- public void dontFailOnSymlink() throws IOException {
+ public void symlink_doesnt_fail() throws IOException {
assumeTrue(!System2.INSTANCE.isOsWindows());
File projectDir = temp.newFolder();
javaUnzip("dummy-git.zip", projectDir);
- JGitBlameCommand jGitBlameCommand = newJGitBlameCommand();
-
- File baseDir = new File(projectDir, "dummy-git");
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
- String relativePath = DUMMY_JAVA;
+ Path baseDir = projectDir.toPath().resolve("dummy-git");
String relativePath2 = "src/main/java/org/dummy/Dummy2.java";
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath)
- .setModuleBaseDir(baseDir.toPath())
- .build();
- fs.add(inputFile);
- DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", relativePath2)
- .setModuleBaseDir(baseDir.toPath())
- .build();
- fs.add(inputFile2);
// Create symlink
- Files.createSymbolicLink(inputFile2.file().toPath(), inputFile.file().toPath());
+ Files.createSymbolicLink(baseDir.resolve(relativePath2), baseDir.resolve(DUMMY_JAVA));
- BlameOutput blameResult = mock(BlameOutput.class);
-
- when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2));
- jGitBlameCommand.blame(input, blameResult);
+ try (Git git = loadRepository(baseDir)) {
+ jGitBlameCommand.blame(git, DUMMY_JAVA);
+ jGitBlameCommand.blame(git, relativePath2);
+ }
}
- @Test
- public void return_early_when_shallow_clone_detected() throws IOException {
- File projectDir = createNewTempFolder();
- javaUnzip("shallow-git.zip", projectDir);
-
- File baseDir = new File(projectDir, "shallow-git");
-
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
-
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).build();
- when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile));
-
- // register warning with default wrapper
- AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
- JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings);
- BlameOutput output = mock(BlameOutput.class);
- jGitBlameCommand.blame(input, output);
-
- assertThat(logTester.logs()).first()
- .matches(s -> s.contains("Shallow clone detected, no blame information will be provided."));
- verifyNoInteractions(output);
-
- verify(analysisWarnings).addUnique(startsWith("Shallow clone detected"));
- }
-
- @Test
- public void return_early_when_clone_with_reference_detected() throws IOException {
- File projectDir = createNewTempFolder();
- javaUnzip("dummy-git-reference-clone.zip", projectDir);
-
- Path baseDir = projectDir.toPath().resolve("dummy-git2");
-
- DefaultFileSystem fs = new DefaultFileSystem(baseDir);
- when(input.fileSystem()).thenReturn(fs);
-
- DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA).setModuleBaseDir(baseDir).build();
- when(input.filesToBlame()).thenReturn(Collections.singleton(inputFile));
-
- // register warning
- AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
- JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(new PathResolver(), analysisWarnings);
- TestBlameOutput output = new TestBlameOutput();
- jGitBlameCommand.blame(input, output);
-
- assertThat(logTester.logs()).first()
- .matches(s -> s.contains("This git repository references another local repository which is not well supported"));
-
- // contains commits referenced from the old clone and commits in the new clone
- assertThat(output.blame).containsKey(inputFile);
- assertThat(output.blame.get(inputFile).stream().map(BlameLine::revision))
- .containsOnly("6b3aab35a3ea32c1636fee56f996e677653c48ea", "843c7c30d7ebd9a479e8f1daead91036c75cbc4e", "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7");
- verifyNoInteractions(analysisWarnings);
+ private Git loadRepository(Path dir) {
+ Repository repo = JGitUtils.buildRepository(dir);
+ return Git.wrap(repo);
}
private File createNewTempFolder() throws IOException {
//This is needed for Windows, otherwise the created File point to invalid (shortened by Windows) temp folder path
return temp.newFolder().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toFile();
}
-
- private JGitBlameCommand newJGitBlameCommand() {
- return new JGitBlameCommand(new PathResolver(), mock(AnalysisWarnings.class));
- }
-
- private static class TestBlameOutput implements BlameOutput {
- private Map<InputFile, List<BlameLine>> blame = new LinkedHashMap<>();
-
- @Override public void blameResult(InputFile inputFile, List<BlameLine> list) {
- blame.put(inputFile, list);
- }
- }
-
}