aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-scanner-engine/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'sonar-scanner-engine/src/main/java/org')
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java103
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java4
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java (renamed from sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java)8
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java25
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java80
5 files changed, 206 insertions, 14 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
index 1ea3b4a4f39..69c02674fd3 100644
--- 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
@@ -22,14 +22,19 @@ package org.sonar.scm.git;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -44,8 +49,13 @@ import org.sonar.api.scan.filesystem.PathResolver;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
+import org.sonar.scm.git.blame.BlameResult;
+import org.sonar.scm.git.blame.RepositoryBlameCommand;
+import org.sonar.scm.git.strategy.BlameStrategy;
+import org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum;
import static java.util.Optional.ofNullable;
+import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_FILES_BLAME;
public class CompositeBlameCommand extends BlameCommand {
private static final Logger LOG = Loggers.get(CompositeBlameCommand.class);
@@ -53,12 +63,16 @@ public class CompositeBlameCommand extends BlameCommand {
private final AnalysisWarnings analysisWarnings;
private final PathResolver pathResolver;
private final JGitBlameCommand jgitCmd;
- private final GitBlameCommand nativeCmd;
+ private final NativeGitBlameCommand nativeCmd;
private boolean nativeGitEnabled = false;
- public CompositeBlameCommand(AnalysisWarnings analysisWarnings, PathResolver pathResolver, JGitBlameCommand jgitCmd, GitBlameCommand nativeCmd) {
+ private final BlameStrategy blameStrategy;
+
+ public CompositeBlameCommand(AnalysisWarnings analysisWarnings, PathResolver pathResolver, JGitBlameCommand jgitCmd,
+ NativeGitBlameCommand nativeCmd, BlameStrategy blameStrategy) {
this.analysisWarnings = analysisWarnings;
this.pathResolver = pathResolver;
+ this.blameStrategy = blameStrategy;
this.jgitCmd = jgitCmd;
this.nativeCmd = nativeCmd;
}
@@ -66,23 +80,48 @@ public class CompositeBlameCommand extends BlameCommand {
@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)) {
+ try (Repository repo = JGitUtils.buildRepository(basedir.toPath())) {
+
File gitBaseDir = repo.getWorkTree();
if (cloneIsInvalid(gitBaseDir)) {
return;
}
Profiler profiler = Profiler.create(LOG);
profiler.startDebug("Collecting committed files");
- Set<String> committedFiles = collectAllCommittedFiles(repo);
+ Map<String, InputFile> inputFileByGitRelativePath = getCommittedFilesToBlame(repo, gitBaseDir, input);
profiler.stopDebug();
+
+ BlameAlgorithmEnum blameAlgorithmEnum = this.blameStrategy.getBlameAlgorithm(Runtime.getRuntime().availableProcessors(), inputFileByGitRelativePath.size());
+ LOG.debug("Using {} strategy to blame files", blameAlgorithmEnum);
+ if (blameAlgorithmEnum == GIT_FILES_BLAME) {
+ blameWithFilesGitCommand(output, repo, inputFileByGitRelativePath);
+ } else {
+ blameWithNativeGitCommand(output, repo, inputFileByGitRelativePath, gitBaseDir);
+ }
+ }
+ }
+
+ private Map<String, InputFile> getCommittedFilesToBlame(Repository repo, File gitBaseDir, BlameInput input) {
+ Set<String> committedFiles = collectAllCommittedFiles(repo);
+ Map<String, InputFile> inputFileByGitRelativePath = new HashMap<>();
+ for (InputFile inputFile : input.filesToBlame()) {
+ String relative = pathResolver.relativePath(gitBaseDir, inputFile.file());
+ if (relative == null || !committedFiles.contains(relative)) {
+ continue;
+ }
+ inputFileByGitRelativePath.put(relative, inputFile);
+ }
+ return inputFileByGitRelativePath;
+ }
+
+ private void blameWithNativeGitCommand(BlameOutput output, Repository repo, Map<String, InputFile> inputFileByGitRelativePath, File gitBaseDir) {
+ try (Git git = Git.wrap(repo)) {
nativeGitEnabled = nativeCmd.checkIfEnabled();
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory());
- for (InputFile inputFile : input.filesToBlame()) {
- String filename = pathResolver.relativePath(gitBaseDir, inputFile.file());
- if (filename == null || !committedFiles.contains(filename)) {
- continue;
- }
+ for (Map.Entry<String, InputFile> e : inputFileByGitRelativePath.entrySet()) {
+ InputFile inputFile = e.getValue();
+ String filename = e.getKey();
// exceptions thrown by the blame method will be ignored
executorService.submit(() -> blame(output, git, gitBaseDir, inputFile, filename));
}
@@ -151,6 +190,29 @@ public class CompositeBlameCommand extends BlameCommand {
}
}
+ private static void blameWithFilesGitCommand(BlameOutput output, Repository repo, Map<String, InputFile> inputFileByGitRelativePath) {
+ RepositoryBlameCommand blameCommand = new RepositoryBlameCommand(repo)
+ .setTextComparator(RawTextComparator.WS_IGNORE_ALL)
+ .setMultithreading(true)
+ .setFilePaths(inputFileByGitRelativePath.keySet());
+ try {
+ BlameResult blameResult = blameCommand.call();
+
+ for (Map.Entry<String, InputFile> e : inputFileByGitRelativePath.entrySet()) {
+ BlameResult.FileBlame fileBlameResult = blameResult.getFileBlameByPath().get(e.getKey());
+
+ if (fileBlameResult == null) {
+ LOG.debug("Unable to blame file {}.", e.getValue().filename());
+ continue;
+ }
+
+ saveBlameInformationForFileInTheOutput(fileBlameResult, e.getValue(), output);
+ }
+ } catch (GitAPIException e) {
+ LOG.warn("There was an issue when interacting with git repository: " + e.getMessage(), e);
+ }
+ }
+
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. "
@@ -166,4 +228,27 @@ public class CompositeBlameCommand extends BlameCommand {
return false;
}
+
+ private static void saveBlameInformationForFileInTheOutput(BlameResult.FileBlame fileBlame, InputFile file, BlameOutput output) {
+ List<BlameLine> linesList = new ArrayList<>();
+ for (int i = 0; i < fileBlame.lines(); i++) {
+ if (fileBlame.getAuthorEmails()[i] == null || fileBlame.getCommitHashes()[i] == null || fileBlame.getCommitDates()[i] == null) {
+ LOG.debug("Unable to blame file {}. No blame info at line {}. Is file committed? [Author: {} Source commit: {}]", file.filename());
+ linesList.clear();
+ break;
+ }
+ linesList.add(new BlameLine()
+ .date(fileBlame.getCommitDates()[i])
+ .revision(fileBlame.getCommitHashes()[i])
+ .author(fileBlame.getAuthorEmails()[i]));
+ }
+ if (!linesList.isEmpty()) {
+ if (linesList.size() == file.lines() - 1) {
+ // SONARPLUGINS-3097 Git does not report blame on last empty line
+ linesList.add(linesList.get(linesList.size() - 1));
+ }
+ output.blameResult(file, linesList);
+ }
+ }
+
}
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 70f7e2918d0..e06c95c7c48 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
@@ -22,6 +22,7 @@ package org.sonar.scm.git;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.util.FS;
+import org.sonar.scm.git.strategy.DefaultBlameStrategy;
public final class GitScmSupport {
private GitScmSupport() {
@@ -33,8 +34,9 @@ public final class GitScmSupport {
return Arrays.asList(
JGitBlameCommand.class,
CompositeBlameCommand.class,
+ NativeGitBlameCommand.class,
+ DefaultBlameStrategy.class,
ProcessWrapperFactory.class,
- GitBlameCommand.class,
GitScmProvider.class,
GitIgnoreCommand.class);
}
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/NativeGitBlameCommand.java
index 9469ec15c38..0dd2e1620ef 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/NativeGitBlameCommand.java
@@ -39,13 +39,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import static java.util.Collections.emptyList;
import static org.sonar.api.utils.Preconditions.checkState;
-public class GitBlameCommand {
+public class NativeGitBlameCommand {
protected static final String BLAME_COMMAND = "blame";
protected static final String GIT_DIR_FLAG = "--git-dir";
protected static final String GIT_DIR_ARGUMENT = "%s/.git";
protected static final String GIT_DIR_FORCE_FLAG = "-C";
- private static final Logger LOG = Loggers.get(GitBlameCommand.class);
+ private static final Logger LOG = Loggers.get(NativeGitBlameCommand.class);
private static final Pattern EMAIL_PATTERN = Pattern.compile("<(.*?)>");
private static final String COMMITTER_TIME = "committer-time ";
private static final String AUTHOR_MAIL = "author-mail ";
@@ -64,12 +64,12 @@ public class GitBlameCommand {
private String gitCommand;
@Autowired
- public GitBlameCommand(System2 system, ProcessWrapperFactory processWrapperFactory) {
+ public NativeGitBlameCommand(System2 system, ProcessWrapperFactory processWrapperFactory) {
this.system = system;
this.processWrapperFactory = processWrapperFactory;
}
- GitBlameCommand(String gitCommand, System2 system, ProcessWrapperFactory processWrapperFactory) {
+ NativeGitBlameCommand(String gitCommand, System2 system, ProcessWrapperFactory processWrapperFactory) {
this.gitCommand = gitCommand;
this.system = system;
this.processWrapperFactory = processWrapperFactory;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java
new file mode 100644
index 00000000000..7092d5e5e05
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.strategy;
+
+public interface BlameStrategy {
+
+ DefaultBlameStrategy.BlameAlgorithmEnum getBlameAlgorithm(int availableProcessors, int numberOfFiles);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java
new file mode 100644
index 00000000000..d52aed22d0d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.strategy;
+
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.*;
+
+/**
+ * The blame strategy defines when the files-blame algorithm is used over jgit or native git algorithm.
+ * It has been found that the JGit/Git native algorithm performs better in certain circumstances, such as:
+ * - When we have many cores available for multi-threading
+ * - The number of files to be blame by the algorithm
+ * This two metrics are correlated:
+ * - The more threads available, the more it is favorable to use the JGit/Git native algorithm
+ * - The more files available, the more it is favorable to use the git-files-blame algorithm
+ */
+public class DefaultBlameStrategy implements BlameStrategy {
+
+ private static final Logger LOG = Loggers.get(DefaultBlameStrategy.class);
+ private static final int FILES_GIT_BLAME_TRIGGER = 10;
+ public static final String PROP_SONAR_SCM_USE_BLAME_ALGORITHM = "sonar.scm.use.blame.algorithm";
+ private final Configuration configuration;
+
+ public DefaultBlameStrategy(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public BlameAlgorithmEnum getBlameAlgorithm(int availableProcessors, int numberOfFiles) {
+ BlameAlgorithmEnum forcedStrategy = configuration.get(PROP_SONAR_SCM_USE_BLAME_ALGORITHM)
+ .map(BlameAlgorithmEnum::valueOf)
+ .orElse(null);
+
+ if (forcedStrategy != null) {
+ return forcedStrategy;
+ }
+
+ if (availableProcessors == 0) {
+ LOG.warn("Available processors are 0. Falling back to native git blame");
+ return GIT_NATIVE_BLAME;
+ }
+ if (numberOfFiles / availableProcessors > FILES_GIT_BLAME_TRIGGER) {
+ return GIT_FILES_BLAME;
+ }
+
+ return GIT_NATIVE_BLAME;
+ }
+
+ public enum BlameAlgorithmEnum {
+ /**
+ * Strategy using native git for the blame, or JGit on single file as a fallback
+ */
+ GIT_NATIVE_BLAME,
+ /**
+ * Strategy using git-files-blame algorithm
+ */
+ GIT_FILES_BLAME;
+ }
+
+}