From d8a3797bd65b1b04f4ccfa9b7a0fc1c59dd0d6eb Mon Sep 17 00:00:00 2001 From: Léo Geoffroy <99647462+leo-geoffroy-sonarsource@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:22:08 +0100 Subject: SONAR-18408 Integrate git-files-blame algorithm to scanner-engine Co-authored-by: lukasz-jarocki-sonarsource Co-authored-by: leo-geoffroy-sonarsource Co-authored-by: benjamin-campomenosi-sonarsource Co-authored-by: Duarte Meneses --- .../org/sonar/scm/git/CompositeBlameCommand.java | 103 +++++- .../java/org/sonar/scm/git/GitBlameCommand.java | 213 ----------- .../main/java/org/sonar/scm/git/GitScmSupport.java | 4 +- .../org/sonar/scm/git/NativeGitBlameCommand.java | 213 +++++++++++ .../org/sonar/scm/git/strategy/BlameStrategy.java | 25 ++ .../scm/git/strategy/DefaultBlameStrategy.java | 80 +++++ .../org/sonar/scm/git/CompositeBlameCommandIT.java | 184 ++++++++++ .../sonar/scm/git/CompositeBlameCommandTest.java | 150 ++++---- .../org/sonar/scm/git/GitBlameCommandTest.java | 388 -------------------- .../java/org/sonar/scm/git/GitScmProviderTest.java | 6 +- .../src/test/java/org/sonar/scm/git/GitUtils.java | 24 ++ .../org/sonar/scm/git/JGitBlameCommandTest.java | 30 +- .../sonar/scm/git/NativeGitBlameCommandTest.java | 391 +++++++++++++++++++++ .../scm/git/strategy/DefaultBlameStrategyTest.java | 82 +++++ .../expected-blame/5files-5commits/fifth-file.js | 1 + .../expected-blame/5files-5commits/first-file.js | 1 + .../expected-blame/5files-5commits/fourth-file.js | 1 + .../expected-blame/5files-5commits/second-file.js | 1 + .../expected-blame/5files-5commits/third-file.js | 1 + .../git/expected-blame/5lines-5commits/5lines.js | 5 + .../org/sonar/scm/git/expected-blame/ReadMe.txt | 6 + .../expected-blame/dummy-git-few-comitters/pom.xml | 52 +++ .../src/main/java/org/dummy/AnotherDummy.java | 29 ++ .../src/main/java/org/dummy/Dummy.java | 29 ++ .../src/test/java/org/dummy/AnotherDummyTest.java | 31 ++ .../sonar/scm/git/expected-blame/dummy-git/pom.xml | 52 +++ .../src/main/java/org/dummy/AnotherDummy.java | 29 ++ .../dummy-git/src/main/java/org/dummy/Dummy.java | 29 ++ .../src/test/java/org/dummy/AnotherDummyTest.java | 31 ++ .../expected-blame/merge-commits/merge-commit.js | 3 + .../one-file-many-merges-and-renames/file1.js | 10 + .../one-file-one-commit/one-commit.js | 1 + .../renamed-many-times.js | 5 + .../one-file-two-commits/two-commits.js | 2 + .../firstcopy.js | 1 + .../secondcopy.js | 1 + .../two-files-one-commit/firstfile.js | 1 + .../two-files-one-commit/secondfile.js | 1 + .../two-merge-commits/two-merge-commits.js | 4 + .../sonar/scm/git/test-repos/5files-5commits.zip | Bin 0 -> 27239 bytes .../sonar/scm/git/test-repos/5lines-5commits.zip | Bin 0 -> 28514 bytes .../scm/git/test-repos/dummy-git-few-comitters.zip | Bin 0 -> 64004 bytes .../org/sonar/scm/git/test-repos/merge-commits.zip | Bin 0 -> 21206 bytes .../one-file-many-merges-and-renames.zip | Bin 0 -> 40675 bytes .../scm/git/test-repos/one-file-one-commit.zip | Bin 0 -> 17336 bytes .../git/test-repos/one-file-renamed-many-times.zip | Bin 0 -> 23835 bytes .../scm/git/test-repos/one-file-two-commits.zip | Bin 0 -> 18942 bytes .../two-files-moved-around-with-conflicts.zip | Bin 0 -> 25956 bytes .../scm/git/test-repos/two-files-one-commit.zip | Bin 0 -> 18077 bytes .../sonar/scm/git/test-repos/two-merge-commits.zip | Bin 0 -> 26802 bytes 50 files changed, 1520 insertions(+), 700 deletions(-) delete mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandIT.java delete mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scm/git/strategy/DefaultBlameStrategyTest.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fifth-file.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/first-file.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fourth-file.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/second-file.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/third-file.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5lines-5commits/5lines.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/ReadMe.txt create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/pom.xml create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/AnotherDummy.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/Dummy.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/test/java/org/dummy/AnotherDummyTest.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/pom.xml create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/AnotherDummy.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/Dummy.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/test/java/org/dummy/AnotherDummyTest.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/merge-commits/merge-commit.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-many-merges-and-renames/file1.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-one-commit/one-commit.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-renamed-many-times/renamed-many-times.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-two-commits/two-commits.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/firstcopy.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/secondcopy.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/firstfile.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/secondfile.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-merge-commits/two-merge-commits.js create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5files-5commits.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5lines-5commits.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/dummy-git-few-comitters.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/merge-commits.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-many-merges-and-renames.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-one-commit.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-renamed-many-times.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-two-commits.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-moved-around-with-conflicts.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-one-commit.zip create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip (limited to 'sonar-scanner-engine/src') 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 committedFiles = collectAllCommittedFiles(repo); + Map 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 getCommittedFilesToBlame(Repository repo, File gitBaseDir, BlameInput input) { + Set committedFiles = collectAllCommittedFiles(repo); + Map 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 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 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 inputFileByGitRelativePath) { + RepositoryBlameCommand blameCommand = new RepositoryBlameCommand(repo) + .setTextComparator(RawTextComparator.WS_IGNORE_ALL) + .setMultithreading(true) + .setFilePaths(inputFileByGitRelativePath.keySet()); + try { + BlameResult blameResult = blameCommand.call(); + + for (Map.Entry 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 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/GitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java deleted file mode 100644 index 9469ec15c38..00000000000 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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; - -import java.io.IOException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.apache.commons.lang.math.NumberUtils; -import org.sonar.api.batch.scm.BlameLine; -import org.sonar.api.utils.System2; -import org.sonar.api.utils.Version; -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.Collections.emptyList; -import static org.sonar.api.utils.Preconditions.checkState; - -public class GitBlameCommand { - 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 Pattern EMAIL_PATTERN = Pattern.compile("<(.*?)>"); - private static final String COMMITTER_TIME = "committer-time "; - private static final String AUTHOR_MAIL = "author-mail "; - - private static final String MINIMUM_REQUIRED_GIT_VERSION = "2.24.0"; - private static final String DEFAULT_GIT_COMMAND = "git"; - private static final String BLAME_LINE_PORCELAIN_FLAG = "--line-porcelain"; - private static final String END_OF_OPTIONS_FLAG = "--end-of-options"; - private static final String IGNORE_WHITESPACES = "-w"; - - private static final Pattern whitespaceRegex = Pattern.compile("\\s+"); - private static final Pattern semanticVersionDelimiter = Pattern.compile("\\."); - - private final System2 system; - private final ProcessWrapperFactory processWrapperFactory; - private String gitCommand; - - @Autowired - public GitBlameCommand(System2 system, ProcessWrapperFactory processWrapperFactory) { - this.system = system; - this.processWrapperFactory = processWrapperFactory; - } - - GitBlameCommand(String gitCommand, System2 system, ProcessWrapperFactory processWrapperFactory) { - this.gitCommand = gitCommand; - this.system = system; - this.processWrapperFactory = processWrapperFactory; - } - - /** - * This method must be executed before org.sonar.scm.git.GitBlameCommand#blame - * - * @return true, if native git is installed - */ - public boolean checkIfEnabled() { - try { - this.gitCommand = locateDefaultGit(); - MutableString stdOut = new MutableString(); - this.processWrapperFactory.create(null, l -> stdOut.string = l, gitCommand, "--version").execute(); - return stdOut.string != null && stdOut.string.startsWith("git version") && isCompatibleGitVersion(stdOut.string); - } catch (Exception e) { - LOG.debug("Failed to find git native client", e); - return false; - } - } - - private String locateDefaultGit() throws IOException { - if (this.gitCommand != null) { - return this.gitCommand; - } - // if not set fall back to defaults - if (system.isOsWindows()) { - return locateGitOnWindows(); - } - return DEFAULT_GIT_COMMAND; - } - - private String locateGitOnWindows() throws IOException { - // Windows will search current directory in addition to the PATH variable, which is unsecure. - // To avoid it we use where.exe to find git binary only in PATH. - LOG.debug("Looking for git command in the PATH using where.exe (Windows)"); - List whereCommandResult = new LinkedList<>(); - this.processWrapperFactory.create(null, whereCommandResult::add, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe") - .execute(); - - if (!whereCommandResult.isEmpty()) { - String out = whereCommandResult.get(0).trim(); - LOG.debug("Found git.exe at {}", out); - return out; - } - throw new IllegalStateException("git.exe not found in PATH. PATH value was: " + system.property("PATH")); - } - - public List blame(Path baseDir, String fileName) throws Exception { - BlameOutputProcessor outputProcessor = new BlameOutputProcessor(); - try { - this.processWrapperFactory.create( - baseDir, - outputProcessor::process, - gitCommand, - GIT_DIR_FLAG, String.format(GIT_DIR_ARGUMENT, baseDir), GIT_DIR_FORCE_FLAG, baseDir.toString(), - BLAME_COMMAND, - BLAME_LINE_PORCELAIN_FLAG, IGNORE_WHITESPACES, END_OF_OPTIONS_FLAG, fileName) - .execute(); - } catch (UncommittedLineException e) { - LOG.debug("Unable to blame file '{}' - it has uncommitted changes", fileName); - return emptyList(); - } - return outputProcessor.getBlameLines(); - } - - private static class BlameOutputProcessor { - private final List blameLines = new LinkedList<>(); - private String sha1 = null; - private String committerTime = null; - private String authorMail = null; - - public List getBlameLines() { - return blameLines; - } - - public void process(String line) { - if (sha1 == null) { - sha1 = line.split(" ")[0]; - } else if (line.startsWith("\t")) { - saveEntry(); - } else if (line.startsWith(COMMITTER_TIME)) { - committerTime = line.substring(COMMITTER_TIME.length()); - } else if (line.startsWith(AUTHOR_MAIL)) { - Matcher matcher = EMAIL_PATTERN.matcher(line); - if (matcher.find(AUTHOR_MAIL.length())) { - authorMail = matcher.group(1); - } - if (authorMail.equals("not.committed.yet")) { - throw new UncommittedLineException(); - } - } - } - - private void saveEntry() { - checkState(authorMail != null, "Did not find an author 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(authorMail) - .date(Date.from(Instant.ofEpochSecond(Long.parseLong(committerTime))))); - } catch (NumberFormatException e) { - throw new IllegalStateException("Invalid committer time found: " + committerTime); - } - authorMail = null; - sha1 = null; - committerTime = null; - } - } - - private static boolean isCompatibleGitVersion(String gitVersionCommandOutput) { - // Due to the danger of argument injection on git blame the use of `--end-of-options` flag is necessary - // The flag is available only on git versions >= 2.24.0 - String gitVersion = whitespaceRegex - .splitAsStream(gitVersionCommandOutput) - .skip(2) - .findFirst() - .orElse(""); - - String formattedGitVersion = formatGitSemanticVersion(gitVersion); - return Version.parse(formattedGitVersion).isGreaterThanOrEqual(Version.parse(MINIMUM_REQUIRED_GIT_VERSION)); - } - - private static String formatGitSemanticVersion(String version) { - return semanticVersionDelimiter - .splitAsStream(version) - .takeWhile(NumberUtils::isNumber) - .collect(Collectors.joining(".")); - } - - private static class MutableString { - String string; - } - - private static class UncommittedLineException extends RuntimeException { - - } -} 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/NativeGitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java new file mode 100644 index 00000000000..0dd2e1620ef --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java @@ -0,0 +1,213 @@ +/* + * 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; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.commons.lang.math.NumberUtils; +import org.sonar.api.batch.scm.BlameLine; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; +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.Collections.emptyList; +import static org.sonar.api.utils.Preconditions.checkState; + +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(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 "; + + private static final String MINIMUM_REQUIRED_GIT_VERSION = "2.24.0"; + private static final String DEFAULT_GIT_COMMAND = "git"; + private static final String BLAME_LINE_PORCELAIN_FLAG = "--line-porcelain"; + private static final String END_OF_OPTIONS_FLAG = "--end-of-options"; + private static final String IGNORE_WHITESPACES = "-w"; + + private static final Pattern whitespaceRegex = Pattern.compile("\\s+"); + private static final Pattern semanticVersionDelimiter = Pattern.compile("\\."); + + private final System2 system; + private final ProcessWrapperFactory processWrapperFactory; + private String gitCommand; + + @Autowired + public NativeGitBlameCommand(System2 system, ProcessWrapperFactory processWrapperFactory) { + this.system = system; + this.processWrapperFactory = processWrapperFactory; + } + + NativeGitBlameCommand(String gitCommand, System2 system, ProcessWrapperFactory processWrapperFactory) { + this.gitCommand = gitCommand; + this.system = system; + this.processWrapperFactory = processWrapperFactory; + } + + /** + * This method must be executed before org.sonar.scm.git.GitBlameCommand#blame + * + * @return true, if native git is installed + */ + public boolean checkIfEnabled() { + try { + this.gitCommand = locateDefaultGit(); + MutableString stdOut = new MutableString(); + this.processWrapperFactory.create(null, l -> stdOut.string = l, gitCommand, "--version").execute(); + return stdOut.string != null && stdOut.string.startsWith("git version") && isCompatibleGitVersion(stdOut.string); + } catch (Exception e) { + LOG.debug("Failed to find git native client", e); + return false; + } + } + + private String locateDefaultGit() throws IOException { + if (this.gitCommand != null) { + return this.gitCommand; + } + // if not set fall back to defaults + if (system.isOsWindows()) { + return locateGitOnWindows(); + } + return DEFAULT_GIT_COMMAND; + } + + private String locateGitOnWindows() throws IOException { + // Windows will search current directory in addition to the PATH variable, which is unsecure. + // To avoid it we use where.exe to find git binary only in PATH. + LOG.debug("Looking for git command in the PATH using where.exe (Windows)"); + List whereCommandResult = new LinkedList<>(); + this.processWrapperFactory.create(null, whereCommandResult::add, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe") + .execute(); + + if (!whereCommandResult.isEmpty()) { + String out = whereCommandResult.get(0).trim(); + LOG.debug("Found git.exe at {}", out); + return out; + } + throw new IllegalStateException("git.exe not found in PATH. PATH value was: " + system.property("PATH")); + } + + public List blame(Path baseDir, String fileName) throws Exception { + BlameOutputProcessor outputProcessor = new BlameOutputProcessor(); + try { + this.processWrapperFactory.create( + baseDir, + outputProcessor::process, + gitCommand, + GIT_DIR_FLAG, String.format(GIT_DIR_ARGUMENT, baseDir), GIT_DIR_FORCE_FLAG, baseDir.toString(), + BLAME_COMMAND, + BLAME_LINE_PORCELAIN_FLAG, IGNORE_WHITESPACES, END_OF_OPTIONS_FLAG, fileName) + .execute(); + } catch (UncommittedLineException e) { + LOG.debug("Unable to blame file '{}' - it has uncommitted changes", fileName); + return emptyList(); + } + return outputProcessor.getBlameLines(); + } + + private static class BlameOutputProcessor { + private final List blameLines = new LinkedList<>(); + private String sha1 = null; + private String committerTime = null; + private String authorMail = null; + + public List getBlameLines() { + return blameLines; + } + + public void process(String line) { + if (sha1 == null) { + sha1 = line.split(" ")[0]; + } else if (line.startsWith("\t")) { + saveEntry(); + } else if (line.startsWith(COMMITTER_TIME)) { + committerTime = line.substring(COMMITTER_TIME.length()); + } else if (line.startsWith(AUTHOR_MAIL)) { + Matcher matcher = EMAIL_PATTERN.matcher(line); + if (matcher.find(AUTHOR_MAIL.length())) { + authorMail = matcher.group(1); + } + if (authorMail.equals("not.committed.yet")) { + throw new UncommittedLineException(); + } + } + } + + private void saveEntry() { + checkState(authorMail != null, "Did not find an author 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(authorMail) + .date(Date.from(Instant.ofEpochSecond(Long.parseLong(committerTime))))); + } catch (NumberFormatException e) { + throw new IllegalStateException("Invalid committer time found: " + committerTime); + } + authorMail = null; + sha1 = null; + committerTime = null; + } + } + + private static boolean isCompatibleGitVersion(String gitVersionCommandOutput) { + // Due to the danger of argument injection on git blame the use of `--end-of-options` flag is necessary + // The flag is available only on git versions >= 2.24.0 + String gitVersion = whitespaceRegex + .splitAsStream(gitVersionCommandOutput) + .skip(2) + .findFirst() + .orElse(""); + + String formattedGitVersion = formatGitSemanticVersion(gitVersion); + return Version.parse(formattedGitVersion).isGreaterThanOrEqual(Version.parse(MINIMUM_REQUIRED_GIT_VERSION)); + } + + private static String formatGitSemanticVersion(String version) { + return semanticVersionDelimiter + .splitAsStream(version) + .takeWhile(NumberUtils::isNumber) + .collect(Collectors.joining(".")); + } + + private static class MutableString { + String string; + } + + private static class UncommittedLineException extends RuntimeException { + + } +} 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; + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandIT.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandIT.java new file mode 100644 index 00000000000..3178cf68327 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandIT.java @@ -0,0 +1,184 @@ +/* + * 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; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultFileSystem; +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.System2; +import org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class CompositeBlameCommandIT { + + private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); + + private final BlameCommand.BlameInput input = mock(BlameCommand.BlameInput.class); + private final JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(); + + private final ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory(); + private final NativeGitBlameCommand nativeGitBlameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + @UseDataProvider("namesOfTheTestRepositoriesWithBlameAlgorithm") + public void testThatBlameAlgorithmOutputsTheSameDataAsGitNativeBlame(String folder, BlameAlgorithmEnum blameAlgorithm) throws Exception { + CompositeBlameCommand underTest = new CompositeBlameCommand(analysisWarnings, new PathResolver(), jGitBlameCommand, nativeGitBlameCommand, (p, f) -> blameAlgorithm); + + TestBlameOutput output = new TestBlameOutput(); + File gitFolder = unzipGitRepository(folder); + + setUpBlameInputWithFile(gitFolder.toPath()); + + underTest.blame(input, output); + + assertBlameMatchesExpectedBlame(output.blame, gitFolder); + } + + @DataProvider + public static Object[][] namesOfTheTestRepositoriesWithBlameAlgorithm() { + List testCases = List.of("one-file-one-commit", + "one-file-two-commits", + "two-files-one-commit", + "merge-commits", + "5lines-5commits", + "5files-5commits", + "two-files-moved-around-with-conflicts", + "one-file-renamed-many-times", + "one-file-many-merges-and-renames", + "two-merge-commits", + "dummy-git", + "dummy-git-few-comitters" + ); + + List blameStrategies = Arrays.stream(BlameAlgorithmEnum.values()).collect(Collectors.toList()); + return testCases.stream() + .flatMap(t -> blameStrategies.stream().map(b -> new Object[]{t, b})) + .toArray(Object[][]::new); + } + + + private void assertBlameMatchesExpectedBlame(Map> blame, File gitFolder) throws Exception { + Map> expectedBlame = readExpectedBlame(gitFolder.getName()); + + assertThat(blame.entrySet()) + .as("Blamed files: " + blame.keySet() + ". Expected blamed files " + expectedBlame.keySet()) + .hasSize(expectedBlame.size()); + + for (Map.Entry> actualBlame : blame.entrySet()) { + Path relativeFilePath = gitFolder.toPath().relativize(actualBlame.getKey().path()); + assertThat(actualBlame.getValue()) + .as("A difference is found in file " + relativeFilePath) + .isEqualTo(expectedBlame.get(relativeFilePath)); + } + } + + // --- helper methods --- // + + private Map> readExpectedBlame(String expectedBlameFolder) throws Exception { + Path expectedBlameFiles = new File(Utils.class.getResource("expected-blame/" + expectedBlameFolder).toURI()).toPath(); + Map> expectedBlame = new HashMap<>(); + + List filesInExpectedBlameFolder = Files.walk(expectedBlameFiles).filter(Files::isRegularFile).collect(Collectors.toList()); + for (Path expectedFileBlamePath : filesInExpectedBlameFolder) { + List blameLines = new ArrayList<>(); + List expectedBlameStrings = Files.readAllLines(expectedFileBlamePath); + for (String line : expectedBlameStrings) { + String revision = line.substring(0, 40); + + int beginningEmail = line.indexOf("<") + 1, endEmail = line.indexOf(">"); + String email = line.substring(beginningEmail, endEmail); + + int beginningDate = line.indexOf("2", endEmail), dateLength = 25; + String sDate = line.substring(beginningDate, beginningDate + dateLength); + Date parsedDate = new Date(OffsetDateTime.parse(sDate).toInstant().toEpochMilli()); + + BlameLine blameLine = new BlameLine() + .revision(revision) + .author(email) + .date(parsedDate); + + blameLines.add(blameLine); + } + expectedBlame.put(expectedBlameFiles.relativize(expectedFileBlamePath), blameLines); + } + return expectedBlame; + } + + private File unzipGitRepository(String repositoryName) throws IOException { + File gitFolderForEachTest = temp.newFolder().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toFile(); + Utils.javaUnzip(repositoryName + ".zip", gitFolderForEachTest); + return gitFolderForEachTest.toPath().resolve(repositoryName).toFile(); + } + + private static class TestBlameOutput implements BlameCommand.BlameOutput { + private final Map> blame = new ConcurrentHashMap<>(); + + @Override + public void blameResult(InputFile inputFile, List list) { + blame.put(inputFile, list); + } + } + + private void setUpBlameInputWithFile(Path baseDir) throws IOException { + DefaultFileSystem fs = new DefaultFileSystem(baseDir); + when(input.fileSystem()).thenReturn(fs); + + try (Stream stream = Files.walk(baseDir)) { + List inputFiles = stream.filter(Files::isRegularFile) + .map(f -> new TestInputFileBuilder("foo", baseDir.toFile(), f.toFile()).build()) + .filter(f -> !f.toString().startsWith(".git") && !f.toString().endsWith(".class")) + .collect(Collectors.toList()); + when(input.filesToBlame()).thenReturn(inputFiles); + } + } +} 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 index 622196549bd..088ee40d541 100644 --- 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 @@ -19,6 +19,9 @@ */ package org.sonar.scm.git; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -30,10 +33,14 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.io.FileUtils; +import org.assertj.core.api.Condition; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputFile; @@ -46,26 +53,33 @@ 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 org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scm.git.strategy.BlameStrategy; +import org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum; 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.any; +import static org.mockito.ArgumentMatchers.intThat; 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; +import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_FILES_BLAME; +import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_NATIVE_BLAME; +@RunWith(DataProviderRunner.class) public class CompositeBlameCommandTest { private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java"; - private final PathResolver pathResolver = new PathResolver(); private final ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory(); private final JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(); - private final GitBlameCommand gitBlameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); + private final NativeGitBlameCommand nativeGitBlameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); - private final CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitBlameCommand); + + private final PathResolver pathResolver = new PathResolver(); @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -74,10 +88,15 @@ public class CompositeBlameCommandTest { public LogTester logTester = new LogTester(); private final BlameCommand.BlameInput input = mock(BlameCommand.BlameInput.class); + @DataProvider + public static List blameAlgorithms() { + return Arrays.stream(BlameAlgorithmEnum.values()).collect(Collectors.toList()); + } + @Test public void use_jgit_if_native_git_disabled() throws IOException { - GitBlameCommand gitCmd = new GitBlameCommand("invalidcommandnotfound", System2.INSTANCE, processWrapperFactory); - BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd); + NativeGitBlameCommand gitCmd = new NativeGitBlameCommand("invalidcommandnotfound", System2.INSTANCE, processWrapperFactory); + BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd, (p, f) -> GIT_NATIVE_BLAME); File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); @@ -85,14 +104,33 @@ public class CompositeBlameCommandTest { setUpBlameInputWithFile(baseDir.toPath()); TestBlameOutput output = new TestBlameOutput(); blameCmd.blame(input, output); + + assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Using GIT_NATIVE_BLAME strategy to blame files"); assertThat(output.blame).hasSize(1); assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29); } + @Test + public void blame_shouldCallStrategyWithCorrectSpecifications() throws IOException { + + BlameStrategy strategyMock = mock(BlameStrategy.class); + BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, strategyMock); + + 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); + + verify(strategyMock).getBlameAlgorithm(intThat((i) -> i > 0), intThat(i -> i == 1)); + } + @Test public void fallback_to_jgit_if_native_git_fails() throws Exception { - GitBlameCommand gitCmd = mock(GitBlameCommand.class); - BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd); + NativeGitBlameCommand gitCmd = mock(NativeGitBlameCommand.class); + BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, gitCmd, (p, f) -> GIT_NATIVE_BLAME); File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); @@ -102,6 +140,8 @@ public class CompositeBlameCommandTest { setUpBlameInputWithFile(baseDir.toPath()); TestBlameOutput output = new TestBlameOutput(); blameCmd.blame(input, output); + + assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Using GIT_NATIVE_BLAME strategy to blame files"); assertThat(output.blame).hasSize(1); assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29); @@ -111,12 +151,15 @@ public class CompositeBlameCommandTest { } @Test - public void skip_files_not_committed() throws Exception { + @UseDataProvider("blameAlgorithms") + public void skip_files_not_committed(BlameAlgorithmEnum strategy) throws Exception { // skip if git not installed - assumeTrue(gitBlameCommand.checkIfEnabled()); + if (strategy == GIT_NATIVE_BLAME) { + assumeTrue(nativeGitBlameCommand.checkIfEnabled()); + } JGitBlameCommand jgit = mock(JGitBlameCommand.class); - BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jgit, gitBlameCommand); + BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jgit, nativeGitBlameCommand, (p, f) -> strategy); File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); @@ -126,18 +169,16 @@ public class CompositeBlameCommandTest { blameCmd.blame(input, output); assertThat(output.blame).hasSize(1); assertThat(output.blame.get(input.filesToBlame().iterator().next())).hasSize(29); - - // never had to fall back to jgit - verifyNoInteractions(jgit); } @Test - public void skip_files_when_head_commit_is_missing() throws IOException { + @UseDataProvider("blameAlgorithms") + public void skip_files_when_head_commit_is_missing(BlameAlgorithmEnum strategy) throws IOException { // skip if git not installed - assumeTrue(gitBlameCommand.checkIfEnabled()); + assumeTrue(nativeGitBlameCommand.checkIfEnabled()); JGitBlameCommand jgit = mock(JGitBlameCommand.class); - BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jgit, gitBlameCommand); + BlameCommand blameCmd = new CompositeBlameCommand(analysisWarnings, pathResolver, jgit, nativeGitBlameCommand, (p, f) -> strategy); File projectDir = createNewTempFolder(); javaUnzip("no-head-git.zip", projectDir); @@ -149,31 +190,15 @@ public class CompositeBlameCommandTest { assertThat(output.blame).isEmpty(); verifyNoInteractions(jgit); - assertThat(logTester.logs()) + assertThat(logTester.logs(LoggerLevel.WARN)) .contains("Could not find HEAD commit"); } @Test - public void use_native_git_by_default() throws IOException { - // skip test if git is not installed - assumeTrue(gitBlameCommand.checkIfEnabled()); - File projectDir = createNewTempFolder(); - javaUnzip("dummy-git.zip", projectDir); - File baseDir = new File(projectDir, "dummy-git"); + @UseDataProvider("blameAlgorithms") + public void return_early_when_shallow_clone_detected(BlameAlgorithmEnum strategy) throws IOException { + CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, (p, f) -> strategy); - 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); @@ -189,6 +214,8 @@ public class CompositeBlameCommandTest { @Test public void fail_if_not_git_project() throws IOException { + CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, (p, f) -> GIT_FILES_BLAME); + File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); @@ -207,7 +234,10 @@ public class CompositeBlameCommandTest { } @Test - public void dont_fail_with_symlink() throws IOException { + @UseDataProvider("blameAlgorithms") + public void dont_fail_with_symlink(BlameAlgorithmEnum strategy) throws IOException { + CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, (p, f) -> strategy); + assumeTrue(!System2.INSTANCE.isOsWindows()); File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); @@ -229,10 +259,13 @@ public class CompositeBlameCommandTest { when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile, inputFile2)); TestBlameOutput output = new TestBlameOutput(); blameCommand.blame(input, output); + assertThat(output.blame).isNotEmpty(); } @Test public void return_early_when_clone_with_reference_detected() throws IOException { + CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, (p, f) -> GIT_FILES_BLAME); + File projectDir = createNewTempFolder(); javaUnzip("dummy-git-reference-clone.zip", projectDir); @@ -248,8 +281,9 @@ public class CompositeBlameCommandTest { 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")); + assertThat(logTester.logs()) + .haveAtLeastOne(new Condition<>(s-> s.startsWith("This git repository references another local repository which is not well supported"), + "log for reference detected")); // contains commits referenced from the old clone and commits in the new clone assertThat(output.blame).containsKey(inputFile); @@ -259,7 +293,9 @@ public class CompositeBlameCommandTest { } @Test - public void blame_on_nested_module() throws IOException { + @UseDataProvider("blameAlgorithms") + public void blame_on_nested_module(BlameAlgorithmEnum strategy) throws IOException { + CompositeBlameCommand blameCommand = new CompositeBlameCommand(analysisWarnings, pathResolver, jGitBlameCommand, nativeGitBlameCommand, (p, f) -> strategy); File projectDir = createNewTempFolder(); javaUnzip("dummy-git-nested.zip", projectDir); File baseDir = new File(projectDir, "dummy-git-nested/dummy-project"); @@ -271,40 +307,16 @@ public class CompositeBlameCommandTest { fs.add(inputFile); BlameCommand.BlameOutput blameResult = mock(BlameCommand.BlameOutput.class); - when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile)); + when(input.filesToBlame()).thenReturn(List.of(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))); + IntStream.range(0, 26) + .mapToObj(i -> new BlameLine().revision(revision).date(revisionDate).author(author)) + .collect(Collectors.toList())); } private BlameCommand.BlameInput setUpBlameInputWithFile(Path baseDir) { @@ -317,7 +329,7 @@ public class CompositeBlameCommandTest { } private File createNewTempFolder() throws IOException { - // This is needed for Windows, otherwise the created File point to invalid (shortened by Windows) temp folder path + // This is needed for Windows, otherwise the created File points to invalid (shortened by Windows) temp folder path return temp.newFolder().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS).toFile(); } 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 deleted file mode 100644 index 834f02bd837..00000000000 --- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java +++ /dev/null @@ -1,388 +0,0 @@ -/* - * 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; - -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.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.junit.Before; -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 org.sonar.scm.git.ProcessWrapperFactory.ProcessWrapper; - -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.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.sonar.scm.git.GitBlameCommand.BLAME_COMMAND; -import static org.sonar.scm.git.GitBlameCommand.GIT_DIR_ARGUMENT; -import static org.sonar.scm.git.GitBlameCommand.GIT_DIR_FLAG; -import static org.sonar.scm.git.GitBlameCommand.GIT_DIR_FORCE_FLAG; -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 DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java"; - - @Rule - public TemporaryFolder temp = new TemporaryFolder(); - @Rule - public LogTester logTester = new LogTester(); - private final ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory(); - private final GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); - - @Before - public void skipTestsIfNoGitFound() { - assumeTrue(blameCommand.checkIfEnabled()); - } - - @Test - public void should_read_lines_only_based_on_new_line() throws Exception { - Path baseDir = createNewTempFolder().toPath(); - String filePath = "file.txt"; - createFile(filePath, "test1\rtest2\r\ttest3", baseDir); - Git git = createRepository(baseDir); - createFile(filePath, "line", baseDir); - commit(git, filePath); - - List blame = blameCommand.blame(baseDir, "file.txt"); - assertThat(blame).hasSize(1); - } - - @Test - public void blame_collects_all_lines() throws Exception { - File projectDir = createNewTempFolder(); - javaUnzip("dummy-git.zip", projectDir); - File baseDir = new File(projectDir, "dummy-git"); - - List 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 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 blame_different_author_and_committer() throws Exception { - File projectDir = createNewTempFolder(); - javaUnzip("dummy-git-different-committer.zip", projectDir); - File baseDir = new File(projectDir, "dummy-git"); - - List 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("2022-10-11T14:14:26+0200"); - String revision2 = "7609f824d5ff7018bebf107cdbe4edcc901b574f"; - String author2 = "duarte.meneses@sonarsource.com"; - - List 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 git_blame_uses_safe_local_repository() throws Exception { - File projectDir = createNewTempFolder(); - File baseDir = new File(projectDir, "dummy-git"); - - ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); - ProcessWrapper mockProcess = mock(ProcessWrapper.class); - String gitCommand = "git"; - when(mockFactory.create(any(), any(), anyString(), anyString(), anyString(), anyString(), - anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) - .then(invocation -> mockProcess); - - GitBlameCommand blameCommand = new GitBlameCommand(gitCommand, System2.INSTANCE, mockFactory); - blameCommand.blame(baseDir.toPath(), DUMMY_JAVA); - - verify(mockFactory).create(any(), any(), eq(gitCommand), - eq(GIT_DIR_FLAG), - eq(String.format(GIT_DIR_ARGUMENT, baseDir.toPath())), - eq(GIT_DIR_FORCE_FLAG), - eq(baseDir.toPath().toString()), - eq(BLAME_COMMAND), - anyString(), anyString(), anyString(), eq(DUMMY_JAVA)); - } - - @Test - public void modified_file_returns_no_blame() throws Exception { - 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 throw_exception_if_symlink_found() throws Exception { - 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); - assertThatThrownBy(() -> blameCommand.blame(baseDir, relativePath2)).isInstanceOf(IllegalStateException.class); - } - - @Test - public void git_should_be_detected() { - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - } - - @Test - public void git_should_not_be_detected() { - GitBlameCommand blameCommand = new GitBlameCommand("randomcmdthatwillneverbefound", System2.INSTANCE, processWrapperFactory); - assertThat(blameCommand.checkIfEnabled()).isFalse(); - } - - @Test - public void git_should_not_be_enabled_if_version_command_is_not_found() { - ProcessWrapperFactory mockedCmd = mockGitVersionCommand("error: unknown option `version'"); - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, mockedCmd); - assertThat(blameCommand.checkIfEnabled()).isFalse(); - } - - @Test - public void git_should_not_be_enabled_if_version_command_does_not_return_string_output() { - ProcessWrapperFactory mockedCmd = mockGitVersionCommand(null); - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, mockedCmd); - assertThat(blameCommand.checkIfEnabled()).isFalse(); - } - - @Test - public void git_should_be_enabled_if_version_is_equal_or_greater_than_required_minimum() { - Stream.of( - "git version 2.24.0", - "git version 2.25.2.1", - "git version 2.24.1.1.windows.2", - "git version 2.25.1.msysgit.2" - ).forEach(output -> { - ProcessWrapperFactory mockedCmd = mockGitVersionCommand(output); - mockGitWhereOnWindows(mockedCmd); - when(mockedCmd.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> { - var argument = (Consumer) invocation.getArgument(1); - argument.accept(output); - return mock(ProcessWrapper.class); - }); - - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, mockedCmd); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - }); - } - - @Test - public void git_should_not_be_enabled_if_version_is_less_than_required_minimum() { - ProcessWrapperFactory mockFactory = mockGitVersionCommand("git version 1.9.0"); - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, mockFactory); - assertThat(blameCommand.checkIfEnabled()).isFalse(); - } - - @Test - public void throw_exception_if_command_fails() throws Exception { - Path baseDir = temp.newFolder().toPath(); - GitBlameCommand blameCommand = new GitBlameCommand("randomcmdthatwillneverbefound", System2.INSTANCE, processWrapperFactory); - assertThatThrownBy(() -> blameCommand.blame(baseDir, "file")).isInstanceOf(IOException.class); - } - - @Test - public void blame_without_email_doesnt_fail() throws Exception { - Path baseDir = temp.newFolder().toPath(); - Git git = createRepository(baseDir); - String filePath = "file.txt"; - createFile(filePath, "line", baseDir); - commitWithNoEmail(git, filePath); - - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - List 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(); - } - - @Test - public void blame_mail_with_spaces_doesnt_fail() throws Exception { - Path baseDir = temp.newFolder().toPath(); - Git git = createRepository(baseDir); - String filePath = "file.txt"; - createFile(filePath, "line", baseDir); - commit(git, filePath, "my DOT name AT server DOT com"); - - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - List blame = blameCommand.blame(baseDir, filePath); - assertThat(blame).hasSize(1); - assertThat(blame.get(0).author()).isEqualTo("my DOT name AT server DOT com"); - } - - @Test - public void do_not_execute() throws Exception { - Path baseDir = temp.newFolder().toPath(); - Git git = createRepository(baseDir); - String filePath = "file.txt"; - createFile(filePath, "line", baseDir); - commitWithNoEmail(git, filePath); - - GitBlameCommand blameCommand = new GitBlameCommand(System2.INSTANCE, processWrapperFactory); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - List 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(); - } - - @Test - public void execution_on_windows_should_fallback_to_full_path() { - System2 system2 = mock(System2.class); - when(system2.isOsWindows()).thenReturn(true); - - ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); - ProcessWrapper mockProcess = mock(ProcessWrapper.class); - mockGitWhereOnWindows(mockFactory); - - when(mockFactory.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> { - var argument = (Consumer) invocation.getArgument(1); - argument.accept("git version 2.30.1"); - return mockProcess; - }); - - GitBlameCommand blameCommand = new GitBlameCommand(system2, mockFactory); - assertThat(blameCommand.checkIfEnabled()).isTrue(); - assertThat(logTester.logs()).contains("Found git.exe at C:\\mockGit.exe"); - } - - @Test - public void execution_on_windows_is_disabled_if_git_not_on_path() { - System2 system2 = mock(System2.class); - when(system2.isOsWindows()).thenReturn(true); - when(system2.property("PATH")).thenReturn("C:\\some-path;C:\\some-another-path"); - - ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); - mockGitWhereOnWindows(mockFactory); - - GitBlameCommand blameCommand = new GitBlameCommand(system2, mockFactory); - assertThat(blameCommand.checkIfEnabled()).isFalse(); - } - - private void commitWithNoEmail(Git git, String path) throws GitAPIException { - commit(git, path, ""); - } - - private void commit(Git git, String path) throws GitAPIException { - commit(git, path, "email@email.com"); - } - - private void commit(Git git, String path, String email) throws GitAPIException { - git.add().addFilepattern(path).call(); - git.commit().setCommitter("joe", email).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(); - } - - private void mockGitWhereOnWindows(ProcessWrapperFactory processWrapperFactory) { - when(processWrapperFactory.create(isNull(), any(), eq("C:\\Windows\\System32\\where.exe"), eq("$PATH:git.exe"))).then(invocation -> { - var argument = (Consumer) invocation.getArgument(1); - argument.accept("C:\\mockGit.exe"); - return mock(ProcessWrapper.class); - }); - } - - private ProcessWrapperFactory mockGitVersionCommand(String commandOutput) { - ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); - ProcessWrapper mockProcess = mock(ProcessWrapper.class); - - when(mockFactory.create(isNull(), any(), eq("git"), eq("--version"))).then(invocation -> { - var argument = (Consumer) invocation.getArgument(1); - argument.accept(commandOutput); - return mockProcess; - }); - - return mockFactory; - } -} 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 242b31fe372..cb7d1f68aec 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 @@ -54,6 +54,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.sonar.api.config.Configuration; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.scan.filesystem.PathResolver; import org.sonar.api.utils.MessageException; @@ -61,6 +62,7 @@ import org.sonar.api.utils.System2; import org.sonar.api.utils.log.LogAndArguments; import org.sonar.api.utils.log.LogTester; import org.sonar.core.documentation.DocumentationLinkGenerator; +import org.sonar.scm.git.strategy.DefaultBlameStrategy; import static java.lang.String.format; import static java.util.Collections.emptySet; @@ -145,8 +147,8 @@ public class GitScmProviderTest { @Test public void returnImplem() { JGitBlameCommand jblameCommand = new JGitBlameCommand(); - GitBlameCommand nativeBlameCommand = new GitBlameCommand(System2.INSTANCE, new ProcessWrapperFactory()); - CompositeBlameCommand compositeBlameCommand = new CompositeBlameCommand(analysisWarnings, new PathResolver(), jblameCommand, nativeBlameCommand); + NativeGitBlameCommand nativeBlameCommand = new NativeGitBlameCommand(System2.INSTANCE, new ProcessWrapperFactory()); + CompositeBlameCommand compositeBlameCommand = new CompositeBlameCommand(analysisWarnings, new PathResolver(), jblameCommand, nativeBlameCommand, new DefaultBlameStrategy(mock(Configuration.class))); GitScmProvider gitScmProvider = new GitScmProvider(compositeBlameCommand, analysisWarnings, gitIgnoreCommand, system2, documentationLinkGenerator); assertThat(gitScmProvider.blameCommand()).isEqualTo(compositeBlameCommand); 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 index 6257b1bb512..13b152994fb 100644 --- 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 @@ -40,5 +40,29 @@ public class GitUtils { Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE); } + public static void createFile(Path worktree, String relativePath, String... lines) throws IOException { + Path newFile = worktree.resolve(relativePath); + Files.createDirectories(newFile.getParent()); + String content = String.join(System.lineSeparator(), lines) + System.lineSeparator(); + Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + + public static void deleteFile(Path worktree, String relativePath) throws IOException { + Path fileToDelete = worktree.resolve(relativePath); + Files.delete(fileToDelete); + } + + public static void copyFile(Path worktree, String origin, String dest) throws IOException { + Path originPath = worktree.resolve(origin); + Path destPath = worktree.resolve(dest); + Files.copy(originPath, destPath); + } + + public static void moveFile(Path worktree, String origin, String dest) throws IOException { + Path originPath = worktree.resolve(origin); + Path destPath = worktree.resolve(dest); + Files.move(originPath, destPath); + } + } 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 2100475da9d..af8a102f367 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 @@ -30,6 +30,7 @@ import java.util.List; import org.apache.commons.io.FileUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.Repository; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -53,15 +54,20 @@ public class JGitBlameCommandTest { public LogTester logTester = new LogTester(); private final JGitBlameCommand jGitBlameCommand = new JGitBlameCommand(); + private Path baseDir; - @Test - public void blame_returns_all_lines() throws IOException { + @Before + public void before() throws IOException { File projectDir = createNewTempFolder(); javaUnzip("dummy-git.zip", projectDir); + baseDir = projectDir.toPath().resolve("dummy-git"); + } + + @Test + public void blame_returns_all_lines() { - File baseDir = new File(projectDir, "dummy-git"); - try (Git git = loadRepository(baseDir.toPath())) { + try (Git git = loadRepository(baseDir)) { List blameLines = jGitBlameCommand.blame(git, DUMMY_JAVA); Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200"); @@ -90,10 +96,6 @@ public class JGitBlameCommandTest { @Test 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()); @@ -105,16 +107,12 @@ public class JGitBlameCommandTest { @Test 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)); + FileUtils.copyFile(new File(baseDir.toFile(), DUMMY_JAVA), new File(baseDir.toFile(), relativePath2)); - try (Git git = loadRepository(baseDir.toPath())) { + try (Git git = loadRepository(baseDir)) { assertThat(jGitBlameCommand.blame(git, DUMMY_JAVA)).hasSize(29); assertThat(jGitBlameCommand.blame(git, relativePath2)).isEmpty(); } @@ -123,10 +121,6 @@ public class JGitBlameCommandTest { @Test 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 diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java new file mode 100644 index 00000000000..8bebca19a87 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java @@ -0,0 +1,391 @@ +/* + * 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; + +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +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.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +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 org.sonar.scm.git.ProcessWrapperFactory.ProcessWrapper; + +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.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.scm.git.NativeGitBlameCommand.BLAME_COMMAND; +import static org.sonar.scm.git.NativeGitBlameCommand.GIT_DIR_ARGUMENT; +import static org.sonar.scm.git.NativeGitBlameCommand.GIT_DIR_FLAG; +import static org.sonar.scm.git.NativeGitBlameCommand.GIT_DIR_FORCE_FLAG; +import static org.sonar.scm.git.GitUtils.createFile; +import static org.sonar.scm.git.GitUtils.createRepository; +import static org.sonar.scm.git.Utils.javaUnzip; + +@RunWith(DataProviderRunner.class) +public class NativeGitBlameCommandTest { + 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 ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory(); + private final NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + + @Before + public void skipTestsIfNoGitFound() { + assumeTrue(blameCommand.checkIfEnabled()); + } + + @Test + public void should_read_lines_only_based_on_new_line() throws Exception { + Path baseDir = createNewTempFolder().toPath(); + String filePath = "file.txt"; + createFile(filePath, "test1\rtest2\r\ttest3", baseDir); + Git git = createRepository(baseDir); + createFile(filePath, "line", baseDir); + commit(git, filePath); + + List blame = blameCommand.blame(baseDir, "file.txt"); + assertThat(blame).hasSize(1); + } + + @Test + public void blame_collects_all_lines() throws Exception { + File projectDir = createNewTempFolder(); + javaUnzip("dummy-git.zip", projectDir); + File baseDir = new File(projectDir, "dummy-git"); + + List 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 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 blame_different_author_and_committer() throws Exception { + File projectDir = createNewTempFolder(); + javaUnzip("dummy-git-different-committer.zip", projectDir); + File baseDir = new File(projectDir, "dummy-git"); + + List 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("2022-10-11T14:14:26+0200"); + String revision2 = "7609f824d5ff7018bebf107cdbe4edcc901b574f"; + String author2 = "duarte.meneses@sonarsource.com"; + + List 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 git_blame_uses_safe_local_repository() throws Exception { + File projectDir = createNewTempFolder(); + File baseDir = new File(projectDir, "dummy-git"); + + ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); + ProcessWrapper mockProcess = mock(ProcessWrapper.class); + String gitCommand = "git"; + when(mockFactory.create(any(), any(), anyString(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) + .then(invocation -> mockProcess); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(gitCommand, System2.INSTANCE, mockFactory); + blameCommand.blame(baseDir.toPath(), DUMMY_JAVA); + + verify(mockFactory).create(any(), any(), eq(gitCommand), + eq(GIT_DIR_FLAG), + eq(String.format(GIT_DIR_ARGUMENT, baseDir.toPath())), + eq(GIT_DIR_FORCE_FLAG), + eq(baseDir.toPath().toString()), + eq(BLAME_COMMAND), + anyString(), anyString(), anyString(), eq(DUMMY_JAVA)); + } + + @Test + public void modified_file_returns_no_blame() throws Exception { + 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 throw_exception_if_symlink_found() throws Exception { + 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); + assertThatThrownBy(() -> blameCommand.blame(baseDir, relativePath2)).isInstanceOf(IllegalStateException.class); + } + + @Test + public void git_should_be_detected() { + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + } + + @Test + public void git_should_not_be_detected() { + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand("randomcmdthatwillneverbefound", System2.INSTANCE, processWrapperFactory); + assertThat(blameCommand.checkIfEnabled()).isFalse(); + } + + @Test + public void git_should_not_be_enabled_if_version_command_is_not_found() { + ProcessWrapperFactory mockedCmd = mockGitVersionCommand("error: unknown option `version'"); + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, mockedCmd); + assertThat(blameCommand.checkIfEnabled()).isFalse(); + } + + @Test + public void git_should_not_be_enabled_if_version_command_does_not_return_string_output() { + ProcessWrapperFactory mockedCmd = mockGitVersionCommand(null); + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, mockedCmd); + assertThat(blameCommand.checkIfEnabled()).isFalse(); + } + + @Test + public void git_should_be_enabled_if_version_is_equal_or_greater_than_required_minimum() { + Stream.of( + "git version 2.24.0", + "git version 2.25.2.1", + "git version 2.24.1.1.windows.2", + "git version 2.25.1.msysgit.2" + ).forEach(output -> { + ProcessWrapperFactory mockedCmd = mockGitVersionCommand(output); + mockGitWhereOnWindows(mockedCmd); + when(mockedCmd.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> { + var argument = (Consumer) invocation.getArgument(1); + argument.accept(output); + return mock(ProcessWrapper.class); + }); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, mockedCmd); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + }); + } + + @Test + public void git_should_not_be_enabled_if_version_is_less_than_required_minimum() { + ProcessWrapperFactory mockFactory = mockGitVersionCommand("git version 1.9.0"); + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, mockFactory); + assertThat(blameCommand.checkIfEnabled()).isFalse(); + } + + @Test + public void throw_exception_if_command_fails() throws Exception { + Path baseDir = temp.newFolder().toPath(); + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand("randomcmdthatwillneverbefound", System2.INSTANCE, processWrapperFactory); + assertThatThrownBy(() -> blameCommand.blame(baseDir, "file")).isInstanceOf(IOException.class); + } + + @Test + public void blame_without_email_doesnt_fail() throws Exception { + Path baseDir = temp.newFolder().toPath(); + Git git = createRepository(baseDir); + String filePath = "file.txt"; + createFile(filePath, "line", baseDir); + commitWithNoEmail(git, filePath); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + List 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(); + } + + @Test + public void blame_mail_with_spaces_doesnt_fail() throws Exception { + Path baseDir = temp.newFolder().toPath(); + Git git = createRepository(baseDir); + String filePath = "file.txt"; + createFile(filePath, "line", baseDir); + commit(git, filePath, "my DOT name AT server DOT com"); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + List blame = blameCommand.blame(baseDir, filePath); + assertThat(blame).hasSize(1); + assertThat(blame.get(0).author()).isEqualTo("my DOT name AT server DOT com"); + } + + @Test + public void do_not_execute() throws Exception { + Path baseDir = temp.newFolder().toPath(); + Git git = createRepository(baseDir); + String filePath = "file.txt"; + createFile(filePath, "line", baseDir); + commitWithNoEmail(git, filePath); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(System2.INSTANCE, processWrapperFactory); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + List 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(); + } + + @Test + public void execution_on_windows_should_fallback_to_full_path() { + System2 system2 = mock(System2.class); + when(system2.isOsWindows()).thenReturn(true); + + ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); + ProcessWrapper mockProcess = mock(ProcessWrapper.class); + mockGitWhereOnWindows(mockFactory); + + when(mockFactory.create(isNull(), any(), eq("C:\\mockGit.exe"), eq("--version"))).then(invocation -> { + var argument = (Consumer) invocation.getArgument(1); + argument.accept("git version 2.30.1"); + return mockProcess; + }); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(system2, mockFactory); + assertThat(blameCommand.checkIfEnabled()).isTrue(); + assertThat(logTester.logs()).contains("Found git.exe at C:\\mockGit.exe"); + } + + @Test + public void execution_on_windows_is_disabled_if_git_not_on_path() { + System2 system2 = mock(System2.class); + when(system2.isOsWindows()).thenReturn(true); + when(system2.property("PATH")).thenReturn("C:\\some-path;C:\\some-another-path"); + + ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); + mockGitWhereOnWindows(mockFactory); + + NativeGitBlameCommand blameCommand = new NativeGitBlameCommand(system2, mockFactory); + assertThat(blameCommand.checkIfEnabled()).isFalse(); + } + + private void commitWithNoEmail(Git git, String path) throws GitAPIException { + commit(git, path, ""); + } + + private void commit(Git git, String path) throws GitAPIException { + commit(git, path, "email@email.com"); + } + + private void commit(Git git, String path, String email) throws GitAPIException { + git.add().addFilepattern(path).call(); + git.commit().setCommitter("joe", email).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(); + } + + private void mockGitWhereOnWindows(ProcessWrapperFactory processWrapperFactory) { + when(processWrapperFactory.create(isNull(), any(), eq("C:\\Windows\\System32\\where.exe"), eq("$PATH:git.exe"))).then(invocation -> { + var argument = (Consumer) invocation.getArgument(1); + argument.accept("C:\\mockGit.exe"); + return mock(ProcessWrapper.class); + }); + } + + private ProcessWrapperFactory mockGitVersionCommand(String commandOutput) { + ProcessWrapperFactory mockFactory = mock(ProcessWrapperFactory.class); + ProcessWrapper mockProcess = mock(ProcessWrapper.class); + + when(mockFactory.create(isNull(), any(), eq("git"), eq("--version"))).then(invocation -> { + var argument = (Consumer) invocation.getArgument(1); + argument.accept(commandOutput); + return mockProcess; + }); + + return mockFactory; + } +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/strategy/DefaultBlameStrategyTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/strategy/DefaultBlameStrategyTest.java new file mode 100644 index 00000000000..2cf351e5e33 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/strategy/DefaultBlameStrategyTest.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_FILES_BLAME; +import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_NATIVE_BLAME; + +public class DefaultBlameStrategyTest { + + private final Configuration configuration = mock(Configuration.class); + private final BlameStrategy underTest = new DefaultBlameStrategy(configuration); + + @Rule + public LogTester logTester = new LogTester(); + + @Test + public void useRepositoryBlame_whenFileBlamePropsEnabled_shouldDisableRepoBlame() { + when(configuration.get(DefaultBlameStrategy.PROP_SONAR_SCM_USE_BLAME_ALGORITHM)).thenReturn(Optional.of(GIT_FILES_BLAME.name())); + + assertThat(underTest.getBlameAlgorithm(1, 1)).isEqualTo(GIT_FILES_BLAME); + + } + + @Test + public void useRepositoryBlame_whenFileBlamePropsDisableOrUnspecified_shouldEnableRepoBlame() { + when(configuration.get(DefaultBlameStrategy.PROP_SONAR_SCM_USE_BLAME_ALGORITHM)).thenReturn(Optional.of(GIT_NATIVE_BLAME.name())); + + assertThat(underTest.getBlameAlgorithm(1, 10000)).isEqualTo(GIT_NATIVE_BLAME); + + } + + @Test + public void useRepositoryBlame_whenFileBlamePropsInvalid_shouldThrowException() { + when(configuration.get(DefaultBlameStrategy.PROP_SONAR_SCM_USE_BLAME_ALGORITHM)).thenReturn(Optional.of("unknown")); + + assertThatThrownBy(() -> underTest.getBlameAlgorithm(1, 1)).isInstanceOf(IllegalArgumentException.class); + + } + + @Test + public void useRepositoryBlame_whenProcessorsCountAndFileSizeSpecified_shouldEnableRepoBlame() { + when(configuration.getBoolean(DefaultBlameStrategy.PROP_SONAR_SCM_USE_BLAME_ALGORITHM)).thenReturn(Optional.empty()); + + assertThat(underTest.getBlameAlgorithm(1, 10000)).isEqualTo(GIT_FILES_BLAME); + assertThat(underTest.getBlameAlgorithm(8, 10)).isEqualTo(GIT_NATIVE_BLAME); + + assertThat(underTest.getBlameAlgorithm(1, 10)).isEqualTo(GIT_NATIVE_BLAME); + assertThat(underTest.getBlameAlgorithm(1, 11)).isEqualTo(GIT_FILES_BLAME); + + + assertThat(underTest.getBlameAlgorithm(0, 10)).isEqualTo(GIT_NATIVE_BLAME); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Available processors are 0. Falling back to native git blame"); + } +} diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fifth-file.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fifth-file.js new file mode 100644 index 00000000000..f87f63f495e --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fifth-file.js @@ -0,0 +1 @@ +37463a67cb907766a8fb2178fd4fc9a1b6f4f14f ( 2023-02-24T09:51:22+01:00 1) fifth-file - changed diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/first-file.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/first-file.js new file mode 100644 index 00000000000..b2ad74c5667 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/first-file.js @@ -0,0 +1 @@ +5c2578e72d07e95781682a3ce1be61a954e39df8 ( 2023-02-24T09:49:41+01:00 1) first-file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fourth-file.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fourth-file.js new file mode 100644 index 00000000000..cdb59898f57 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fourth-file.js @@ -0,0 +1 @@ +a7015bf40760ca91ae7d73d9102958fc870b30b9 ( 2023-02-24T09:51:08+01:00 1) fourth-file - changed diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/second-file.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/second-file.js new file mode 100644 index 00000000000..1cf8ae4a139 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/second-file.js @@ -0,0 +1 @@ +8bda33b1296875441f08c157140eb8504e13ebb2 ( 2023-02-24T09:49:59+01:00 1) second-file - changed diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/third-file.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/third-file.js new file mode 100644 index 00000000000..9e9bcd22016 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/third-file.js @@ -0,0 +1 @@ +442ca0c2c76fe6eee3246cae40fe36c077f4d4a0 ( 2023-02-24T09:50:13+01:00 1) third-file - changed diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5lines-5commits/5lines.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5lines-5commits/5lines.js new file mode 100644 index 00000000000..eaa6ca66feb --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5lines-5commits/5lines.js @@ -0,0 +1,5 @@ +d0da62499e8476f7d7599b37d2f2201770cf6790 ( 2023-03-01T10:31:46+01:00 1) first line +bed9b1f0ae87ecda5ef1c805e303e2295676317b ( 2023-03-01T10:32:16+01:00 2) second line +939282feaabde0d049e739fd3e30937836c6fc13 ( 2023-03-01T10:32:33+01:00 3) third line +f4cfbd0b9fe74daea9dd588ec41e8da71505e959 ( 2023-03-01T10:32:46+01:00 4) fourth line +5fa3e610adad109fdc89cdfa68592ddd7a0ba3c6 ( 2023-03-01T10:33:03+01:00 5) fifth line diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/ReadMe.txt b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/ReadMe.txt new file mode 100644 index 00000000000..9949224313b --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/ReadMe.txt @@ -0,0 +1,6 @@ +This folder contains expected blame data for corresponding repositories (stored in test-repos folder). The data in expected-blame folder is +produced by blaming each file with +git blame {file_name_from_the_repository} --date=iso-strict --show-email -l --root > expected-blame/repository-name/{file_name_from_the_repository} + +The expected blame data is then used in the corresponding unit and integration tests to assert correctness of the new git blame algorithm +implemented in the scanner engine. \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/pom.xml b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/pom.xml new file mode 100644 index 00000000000..2424e4faa60 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/pom.xml @@ -0,0 +1,52 @@ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 1) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 2) 4.0.0 +783905e3d8b3e6f89a63c573d5fcf60af7845ebe ( 2012-07-20T18:52:36+02:00 3) dummy-git +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 4) dummy +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 5) 1.0 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 6) jar +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 7) Dummy +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 8) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 9) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 10) UTF-8 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 11) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 12) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 13) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 14) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 15) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 16) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 17) maven-clean-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 18) 2.4.1 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 19) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 20) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 21) maven-compiler-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 22) 2.3.2 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 23) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 24) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 25) maven-resources-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 26) 2.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 27) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 28) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 29) maven-source-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 30) 2.1.2 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 31) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 32) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 33) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 34) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 35) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 36) maven-compiler-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 37) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 38) 1.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 39) 1.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 40) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 41) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 42) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 43) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 44) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 45) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 46) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 47) junit +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 48) junit +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 49) 4.10 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 50) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 51) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 52) diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/AnotherDummy.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/AnotherDummy.java new file mode 100644 index 00000000000..ffec5c679d1 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/AnotherDummy.java @@ -0,0 +1,29 @@ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 1) /* +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 2) * Sonar, open source software quality management tool. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 3) * Copyright (C) 2008-2012 SonarSource +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 4) * mailto:contact AT sonarsource DOT com +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 5) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 6) * Sonar is free software; you can redistribute it and/or +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 7) * modify it under the terms of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 8) * License as published by the Free Software Foundation; either +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 9) * version 3 of the License, or (at your option) any later version. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 10) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 11) * Sonar is distributed in the hope that it will be useful, +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 14) * Lesser General Public License for more details. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 15) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 16) * You should have received a copy of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 17) * License along with Sonar; if not, write to the Free Software +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 19) */ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 20) package org.dummy; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 21) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 22) public class AnotherDummy { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 23) public String say(boolean hello) { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 24) if (hello) { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 25) return "Hello"; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 26) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 27) return new String("GoodBye"); +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 28) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 29) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/Dummy.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/Dummy.java new file mode 100644 index 00000000000..d54958921de --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/main/java/org/dummy/Dummy.java @@ -0,0 +1,29 @@ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 1) /* +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 2) * Sonar, open source software quality management tool. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 3) * Copyright (C) 2008-2012 SonarSource +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 4) * mailto:contact AT sonarsource DOT com +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 5) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 6) * Sonar is free software; you can redistribute it and/or +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 7) * modify it under the terms of the GNU Lesser General Public +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 8) * License as published by the Free Software Foundation; either +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 9) * version 3 of the License, or (at your option) any later version. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 10) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 11) * Sonar is distributed in the hope that it will be useful, +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 14) * Lesser General Public License for more details. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 15) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 16) * You should have received a copy of the GNU Lesser General Public +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 17) * License along with Sonar; if not, write to the Free Software +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 19) */ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 20) package org.dummy; +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 21) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 22) public class Dummy { +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 23) public String sayHello() { +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 24) return "Hello"; +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 25) } +7609f824d5ff7018bebf107cdbe4edcc901b574f ( 2022-10-11T14:14:26+02:00 26) public String sayBye() { +7609f824d5ff7018bebf107cdbe4edcc901b574f ( 2022-10-11T14:14:26+02:00 27) return "Bye"; +7609f824d5ff7018bebf107cdbe4edcc901b574f ( 2022-10-11T14:14:26+02:00 28) } +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 29) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/test/java/org/dummy/AnotherDummyTest.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/test/java/org/dummy/AnotherDummyTest.java new file mode 100644 index 00000000000..92c55be754a --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/src/test/java/org/dummy/AnotherDummyTest.java @@ -0,0 +1,31 @@ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 1) /* +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 2) * Sonar, open source software quality management tool. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 3) * Copyright (C) 2008-2012 SonarSource +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 4) * mailto:contact AT sonarsource DOT com +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 5) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 6) * Sonar is free software; you can redistribute it and/or +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 7) * modify it under the terms of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 8) * License as published by the Free Software Foundation; either +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 9) * version 3 of the License, or (at your option) any later version. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 10) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 11) * Sonar is distributed in the hope that it will be useful, +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 14) * Lesser General Public License for more details. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 15) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 16) * You should have received a copy of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 17) * License along with Sonar; if not, write to the Free Software +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 19) */ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 20) package org.dummy; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 21) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 22) import org.junit.Test; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 23) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 24) import static org.junit.Assert.assertEquals; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 25) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 26) public class AnotherDummyTest { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 27) @Test +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 28) public void should_say_hello() { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 29) assertEquals("Hello", new AnotherDummy().say(true)); +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 30) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 31) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/pom.xml b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/pom.xml new file mode 100644 index 00000000000..d770317afb6 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/pom.xml @@ -0,0 +1,52 @@ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 1) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 2) 4.0.0 +783905e3d8b3e6f89a63c573d5fcf60af7845ebe ( 2012-07-20T18:52:36+02:00 3) dummy-git +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 4) dummy +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 5) 1.0 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 6) jar +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 7) Dummy +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 8) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 9) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 10) UTF-8 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 11) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 12) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 13) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 14) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 15) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 16) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 17) maven-clean-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 18) 2.4.1 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 19) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 20) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 21) maven-compiler-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 22) 2.3.2 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 23) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 24) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 25) maven-resources-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 26) 2.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 27) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 28) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 29) maven-source-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 30) 2.1.2 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 31) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 32) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 33) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 34) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 35) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 36) maven-compiler-plugin +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 37) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 38) 1.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 39) 1.5 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 40) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 41) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 42) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 43) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 44) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 45) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 46) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 47) junit +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 48) junit +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 49) 4.10 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 50) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 51) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 52) diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/AnotherDummy.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/AnotherDummy.java new file mode 100644 index 00000000000..bf0da1e1e2d --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/AnotherDummy.java @@ -0,0 +1,29 @@ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 1) /* +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 2) * Sonar, open source software quality management tool. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 3) * Copyright (C) 2008-2012 SonarSource +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 4) * mailto:contact AT sonarsource DOT com +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 5) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 6) * Sonar is free software; you can redistribute it and/or +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 7) * modify it under the terms of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 8) * License as published by the Free Software Foundation; either +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 9) * version 3 of the License, or (at your option) any later version. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 10) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 11) * Sonar is distributed in the hope that it will be useful, +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 14) * Lesser General Public License for more details. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 15) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 16) * You should have received a copy of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 17) * License along with Sonar; if not, write to the Free Software +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 19) */ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 20) package org.dummy; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 21) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 22) public class AnotherDummy { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 23) public String say(boolean hello) { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 24) if (hello) { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 25) return "Hello"; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 26) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 27) return new String("GoodBye"); +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 28) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 29) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/Dummy.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/Dummy.java new file mode 100644 index 00000000000..8245e31e6cc --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/Dummy.java @@ -0,0 +1,29 @@ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 1) /* +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 2) * Sonar, open source software quality management tool. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 3) * Copyright (C) 2008-2012 SonarSource +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 4) * mailto:contact AT sonarsource DOT com +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 5) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 6) * Sonar is free software; you can redistribute it and/or +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 7) * modify it under the terms of the GNU Lesser General Public +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 8) * License as published by the Free Software Foundation; either +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 9) * version 3 of the License, or (at your option) any later version. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 10) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 11) * Sonar is distributed in the hope that it will be useful, +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 14) * Lesser General Public License for more details. +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 15) * +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 16) * You should have received a copy of the GNU Lesser General Public +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 17) * License along with Sonar; if not, write to the Free Software +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 19) */ +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 20) package org.dummy; +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 21) +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 22) public class Dummy { +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 23) public String sayHello() { +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 24) return "Hello"; +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 25) } +0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 ( 2015-05-19T13:31:09+02:00 26) public String sayBye() { +0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 ( 2015-05-19T13:31:09+02:00 27) return "Bye"; +0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 ( 2015-05-19T13:31:09+02:00 28) } +6b3aab35a3ea32c1636fee56f996e677653c48ea ( 2012-07-17T16:12:48+02:00 29) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/test/java/org/dummy/AnotherDummyTest.java b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/test/java/org/dummy/AnotherDummyTest.java new file mode 100644 index 00000000000..c7df309c4a2 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/test/java/org/dummy/AnotherDummyTest.java @@ -0,0 +1,31 @@ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 1) /* +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 2) * Sonar, open source software quality management tool. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 3) * Copyright (C) 2008-2012 SonarSource +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 4) * mailto:contact AT sonarsource DOT com +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 5) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 6) * Sonar is free software; you can redistribute it and/or +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 7) * modify it under the terms of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 8) * License as published by the Free Software Foundation; either +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 9) * version 3 of the License, or (at your option) any later version. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 10) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 11) * Sonar is distributed in the hope that it will be useful, +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 12) * but WITHOUT ANY WARRANTY; without even the implied warranty of +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 13) * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 14) * Lesser General Public License for more details. +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 15) * +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 16) * You should have received a copy of the GNU Lesser General Public +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 17) * License along with Sonar; if not, write to the Free Software +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 18) * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 19) */ +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 20) package org.dummy; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 21) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 22) import org.junit.Test; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 23) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 24) import static org.junit.Assert.assertEquals; +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 25) +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 26) public class AnotherDummyTest { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 27) @Test +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 28) public void should_say_hello() { +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 29) assertEquals("Hello", new AnotherDummy().say(true)); +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 30) } +ffb55a516440d069145dd9199e4ac67976b2338b ( 2012-07-20T15:26:23+02:00 31) } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/merge-commits/merge-commit.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/merge-commits/merge-commit.js new file mode 100644 index 00000000000..3fca0bd063a --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/merge-commits/merge-commit.js @@ -0,0 +1,3 @@ +2b6ccf6d99dea2840613563aa54fdc5c8efc05cb ( 2023-03-01T14:43:49+01:00 1) first commit +5a729e03b90e11d3d4f582a63cd2df93a7db41e0 ( 2023-03-01T14:44:47+01:00 2) second commit on master +e62f234121479b8aa3bf60481324784c6ed6ba40 ( 2023-03-01T14:45:59+01:00 3) commit on branch \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-many-merges-and-renames/file1.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-many-merges-and-renames/file1.js new file mode 100644 index 00000000000..1a868d1d919 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-many-merges-and-renames/file1.js @@ -0,0 +1,10 @@ +691242bf1b8319a7398be257c206203f7d6b8743 file1.js ( 2023-02-24T10:28:35+01:00 1) 111 - changed +cb5f118c653af2e6d4d30f798714f45f5338c11c file.js ( 2023-02-24T10:29:03+01:00 2) 222 - changed +84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js ( 2023-02-24T10:26:31+01:00 3) 333 +ea53e21a12061a733434454cf6cb8c6c29094fd0 file1.js ( 2023-02-24T10:33:06+01:00 4) 444 - changed in master +84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js ( 2023-02-24T10:26:31+01:00 5) 555 +223e2f4e80d72199dae13b94690099dccc20eeea file1.js ( 2023-03-01T10:52:44+01:00 6) 666 - changed on branch1 +84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js ( 2023-02-24T10:26:31+01:00 7) 777 +84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js ( 2023-02-24T10:26:31+01:00 8) 888 +84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js ( 2023-02-24T10:26:31+01:00 9) 999 +c700a4b511aeecb225b4bd86dbe964f29730cffd file.js ( 2023-02-24T10:27:59+01:00 10) 100 - changed diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-one-commit/one-commit.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-one-commit/one-commit.js new file mode 100644 index 00000000000..348567b955e --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-one-commit/one-commit.js @@ -0,0 +1 @@ +23628a3b878ae8dd5e51042c7a39057a3e4ce45c ( 2023-02-23T14:29:45+01:00 1) hello world diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-renamed-many-times/renamed-many-times.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-renamed-many-times/renamed-many-times.js new file mode 100644 index 00000000000..d9ca90d398a --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-renamed-many-times/renamed-many-times.js @@ -0,0 +1,5 @@ +13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js ( 2023-02-24T10:16:08+01:00 1) initial-change +13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js ( 2023-02-24T10:16:08+01:00 2) second-change +13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js ( 2023-02-24T10:16:08+01:00 3) third-change +3561639d487376f1d8c0297838fd2a94c5563d61 d.js ( 2023-02-24T10:16:39+01:00 4) fourth-change +5123767d66854a431e9f6a05a88cf7db6e73aabc e.js ( 2023-02-24T10:17:15+01:00 5) fifth-change \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-two-commits/two-commits.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-two-commits/two-commits.js new file mode 100644 index 00000000000..4d40b8eadd9 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-two-commits/two-commits.js @@ -0,0 +1,2 @@ +51c8ead53b248e93e66e5038c826524c0730f271 ( 2023-02-23T14:39:44+01:00 1) first line first commit +b497efbe6a7ff547dee5dcc368b8920dba0fd908 ( 2023-02-23T14:40:18+01:00 2) second line second commit diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/firstcopy.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/firstcopy.js new file mode 100644 index 00000000000..167d039fec9 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/firstcopy.js @@ -0,0 +1 @@ +5ee03833d69a38d440a8685eb33dd96b0bea24e6 first.js ( 2023-03-02T11:47:42+01:00 1) commit on master, file: first.js \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/secondcopy.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/secondcopy.js new file mode 100644 index 00000000000..89de99f0a0a --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/secondcopy.js @@ -0,0 +1 @@ +dab8b2a7c229b1111fbfe7c7dda7f9913ebb71f9 ( 2023-03-02T11:46:47+01:00 1) second commit, file: second.js \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/firstfile.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/firstfile.js new file mode 100644 index 00000000000..5ce08eaf95c --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/firstfile.js @@ -0,0 +1 @@ +a66e868e71a5e871ca9d9d74035adc0a743ac537 ( 2023-02-23T16:45:23+01:00 1) first file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/secondfile.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/secondfile.js new file mode 100644 index 00000000000..927595f0883 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/secondfile.js @@ -0,0 +1 @@ +a66e868e71a5e871ca9d9d74035adc0a743ac537 ( 2023-02-23T16:45:23+01:00 1) second file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-merge-commits/two-merge-commits.js b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-merge-commits/two-merge-commits.js new file mode 100644 index 00000000000..06935cc60fb --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-merge-commits/two-merge-commits.js @@ -0,0 +1,4 @@ +48559ab7c378fb7181dc515967bd0dcfc71a8a57 ( 2023-03-01T14:54:30+01:00 1) first commit +b5767c89ce517009ede7a6c7e69f46f4d2270fb5 ( 2023-03-01T14:56:53+01:00 2) fifth commit on master +69cf0175d31c5a2c1517ac2d3515864324c436e9 ( 2023-03-01T14:55:02+01:00 3) second commit on branch +c65549591670241841ae5882453ea05e51e3df20 ( 2023-03-01T14:56:33+01:00 4) fourth commit on branch \ No newline at end of file diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5files-5commits.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5files-5commits.zip new file mode 100644 index 00000000000..2e5068acc6f Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5files-5commits.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5lines-5commits.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5lines-5commits.zip new file mode 100644 index 00000000000..0c441f8c674 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5lines-5commits.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/dummy-git-few-comitters.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/dummy-git-few-comitters.zip new file mode 100644 index 00000000000..fcef9cdf47c Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/dummy-git-few-comitters.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/merge-commits.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/merge-commits.zip new file mode 100644 index 00000000000..60e9d2a1502 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/merge-commits.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-many-merges-and-renames.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-many-merges-and-renames.zip new file mode 100644 index 00000000000..ce7e7b993ca Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-many-merges-and-renames.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-one-commit.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-one-commit.zip new file mode 100644 index 00000000000..becfbf720e4 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-one-commit.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-renamed-many-times.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-renamed-many-times.zip new file mode 100644 index 00000000000..2efd3dd0004 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-renamed-many-times.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-two-commits.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-two-commits.zip new file mode 100644 index 00000000000..81b25b8b288 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-two-commits.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-moved-around-with-conflicts.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-moved-around-with-conflicts.zip new file mode 100644 index 00000000000..1999eb6d207 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-moved-around-with-conflicts.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-one-commit.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-one-commit.zip new file mode 100644 index 00000000000..d90349a6343 Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-one-commit.zip differ diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip new file mode 100644 index 00000000000..3f8c40fe8ca Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip differ -- cgit v1.2.3