]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18408 Integrate git-files-blame algorithm to scanner-engine
authorLéo Geoffroy <99647462+leo-geoffroy-sonarsource@users.noreply.github.com>
Tue, 14 Mar 2023 15:22:08 +0000 (16:22 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 14 Mar 2023 20:03:28 +0000 (20:03 +0000)
Co-authored-by: lukasz-jarocki-sonarsource <lukasz.jarocki@sonarsource.com>
Co-authored-by: leo-geoffroy-sonarsource <leo.geoffroy@sonarsource.com>
Co-authored-by: benjamin-campomenosi-sonarsource <benjamin.campomenosi@sonarsource.com>
Co-authored-by: Duarte Meneses <duarte.meneses@sonarsource.com>
52 files changed:
build.gradle
sonar-scanner-engine/build.gradle
sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java
sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java
sonar-scanner-engine/src/main/java/org/sonar/scm/git/NativeGitBlameCommand.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/BlameStrategy.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scm/git/strategy/DefaultBlameStrategy.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandIT.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scm/git/CompositeBlameCommandTest.java
sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitBlameCommandTest.java [deleted file]
sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitUtils.java
sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitBlameCommandTest.java
sonar-scanner-engine/src/test/java/org/sonar/scm/git/NativeGitBlameCommandTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scm/git/strategy/DefaultBlameStrategyTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fifth-file.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/first-file.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/fourth-file.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/second-file.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5files-5commits/third-file.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/5lines-5commits/5lines.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/ReadMe.txt [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git-few-comitters/pom.xml [new file with mode: 0644]
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 with mode: 0644]
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 with mode: 0644]
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 with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/pom.xml [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/AnotherDummy.java [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/main/java/org/dummy/Dummy.java [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/dummy-git/src/test/java/org/dummy/AnotherDummyTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/merge-commits/merge-commit.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-many-merges-and-renames/file1.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-one-commit/one-commit.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-renamed-many-times/renamed-many-times.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/one-file-two-commits/two-commits.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/firstcopy.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-moved-around-with-conflicts/secondcopy.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/firstfile.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-files-one-commit/secondfile.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/expected-blame/two-merge-commits/two-merge-commits.js [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5files-5commits.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/5lines-5commits.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/dummy-git-few-comitters.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/merge-commits.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-many-merges-and-renames.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-one-commit.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-renamed-many-times.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/one-file-two-commits.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-moved-around-with-conflicts.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-files-one-commit.zip [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip [new file with mode: 0644]

index ef662359dc0544c83660680b530ff1fd0097fc5b..6e3f4a527e987cc5b0ad2da97aa3bd282cc9544c 100644 (file)
@@ -352,6 +352,7 @@ subprojects {
       dependency 'org.postgresql:postgresql:42.5.1'
       dependency 'org.reflections:reflections:0.10.2'
       dependency 'org.simpleframework:simple:5.1.6'
+      dependency 'org.sonarsource.git.blame:git-files-blame:1.0.0.157'
       dependency 'org.sonarsource.orchestrator:sonar-orchestrator:3.40.0.183'
       dependency 'org.sonarsource.update-center:sonar-update-center-common:1.29.0.1000'
       dependency("org.springframework:spring-context:${springVersion}") {
index 851230ea84fae2aa8605bd8b360debe95ec5ab3d..269ec1996e527fc1bf37ce77a84d66796490e855 100644 (file)
@@ -34,6 +34,7 @@ dependencies {
   api 'org.slf4j:log4j-over-slf4j'
   api 'org.slf4j:slf4j-api'
   api 'org.sonarsource.api.plugin:sonar-plugin-api'
+  api 'org.sonarsource.git.blame:git-files-blame'
   api 'org.sonarsource.update-center:sonar-update-center-common'
   api 'org.springframework:spring-context'
 
@@ -60,7 +61,7 @@ dependencies {
 }
 
 license {
-  excludes(["**/Fake.java", "**/Fake.groovy", "org/sonar/scanner/cpd/ManyStatements.java"])
+  excludes(["**/Fake.java", "**/Fake.groovy", "org/sonar/scanner/cpd/ManyStatements.java", "org/sonar/scm/git/expected-blame/**/*"])
 }
 
 artifactoryPublish.skip = false
index 1ea3b4a4f393d431a1b4e3d7534104d76dffcf55..69c02674fd3c45866bfaf5470b7cce0f3a64b371 100644 (file)
@@ -22,14 +22,19 @@ package org.sonar.scm.git;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -44,8 +49,13 @@ import org.sonar.api.scan.filesystem.PathResolver;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
+import org.sonar.scm.git.blame.BlameResult;
+import org.sonar.scm.git.blame.RepositoryBlameCommand;
+import org.sonar.scm.git.strategy.BlameStrategy;
+import org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum;
 
 import static java.util.Optional.ofNullable;
+import static org.sonar.scm.git.strategy.DefaultBlameStrategy.BlameAlgorithmEnum.GIT_FILES_BLAME;
 
 public class CompositeBlameCommand extends BlameCommand {
   private static final Logger LOG = Loggers.get(CompositeBlameCommand.class);
@@ -53,12 +63,16 @@ public class CompositeBlameCommand extends BlameCommand {
   private final AnalysisWarnings analysisWarnings;
   private final PathResolver pathResolver;
   private final JGitBlameCommand jgitCmd;
-  private final GitBlameCommand nativeCmd;
+  private final NativeGitBlameCommand nativeCmd;
   private boolean nativeGitEnabled = false;
 
-  public CompositeBlameCommand(AnalysisWarnings analysisWarnings, PathResolver pathResolver, JGitBlameCommand jgitCmd, GitBlameCommand nativeCmd) {
+  private final BlameStrategy blameStrategy;
+
+  public CompositeBlameCommand(AnalysisWarnings analysisWarnings, PathResolver pathResolver, JGitBlameCommand jgitCmd,
+    NativeGitBlameCommand nativeCmd, BlameStrategy blameStrategy) {
     this.analysisWarnings = analysisWarnings;
     this.pathResolver = pathResolver;
+    this.blameStrategy = blameStrategy;
     this.jgitCmd = jgitCmd;
     this.nativeCmd = nativeCmd;
   }
@@ -66,23 +80,48 @@ public class CompositeBlameCommand extends BlameCommand {
   @Override
   public void blame(BlameInput input, BlameOutput output) {
     File basedir = input.fileSystem().baseDir();
-    try (Repository repo = JGitUtils.buildRepository(basedir.toPath()); Git git = Git.wrap(repo)) {
+    try (Repository repo = JGitUtils.buildRepository(basedir.toPath())) {
+
       File gitBaseDir = repo.getWorkTree();
       if (cloneIsInvalid(gitBaseDir)) {
         return;
       }
       Profiler profiler = Profiler.create(LOG);
       profiler.startDebug("Collecting committed files");
-      Set<String> committedFiles = collectAllCommittedFiles(repo);
+      Map<String, InputFile> inputFileByGitRelativePath = getCommittedFilesToBlame(repo, gitBaseDir, input);
       profiler.stopDebug();
+
+      BlameAlgorithmEnum blameAlgorithmEnum = this.blameStrategy.getBlameAlgorithm(Runtime.getRuntime().availableProcessors(), inputFileByGitRelativePath.size());
+      LOG.debug("Using {} strategy to blame files", blameAlgorithmEnum);
+      if (blameAlgorithmEnum == GIT_FILES_BLAME) {
+        blameWithFilesGitCommand(output, repo, inputFileByGitRelativePath);
+      } else {
+        blameWithNativeGitCommand(output, repo, inputFileByGitRelativePath, gitBaseDir);
+      }
+    }
+  }
+
+  private Map<String, InputFile> getCommittedFilesToBlame(Repository repo, File gitBaseDir, BlameInput input) {
+    Set<String> committedFiles = collectAllCommittedFiles(repo);
+    Map<String, InputFile> inputFileByGitRelativePath = new HashMap<>();
+    for (InputFile inputFile : input.filesToBlame()) {
+      String relative = pathResolver.relativePath(gitBaseDir, inputFile.file());
+      if (relative == null || !committedFiles.contains(relative)) {
+        continue;
+      }
+      inputFileByGitRelativePath.put(relative, inputFile);
+    }
+    return inputFileByGitRelativePath;
+  }
+
+  private void blameWithNativeGitCommand(BlameOutput output, Repository repo, Map<String, InputFile> inputFileByGitRelativePath, File gitBaseDir) {
+    try (Git git = Git.wrap(repo)) {
       nativeGitEnabled = nativeCmd.checkIfEnabled();
       ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory());
 
-      for (InputFile inputFile : input.filesToBlame()) {
-        String filename = pathResolver.relativePath(gitBaseDir, inputFile.file());
-        if (filename == null || !committedFiles.contains(filename)) {
-          continue;
-        }
+      for (Map.Entry<String, InputFile> e : inputFileByGitRelativePath.entrySet()) {
+        InputFile inputFile = e.getValue();
+        String filename = e.getKey();
         // exceptions thrown by the blame method will be ignored
         executorService.submit(() -> blame(output, git, gitBaseDir, inputFile, filename));
       }
@@ -151,6 +190,29 @@ public class CompositeBlameCommand extends BlameCommand {
     }
   }
 
+  private static void blameWithFilesGitCommand(BlameOutput output, Repository repo, Map<String, InputFile> inputFileByGitRelativePath) {
+    RepositoryBlameCommand blameCommand = new RepositoryBlameCommand(repo)
+      .setTextComparator(RawTextComparator.WS_IGNORE_ALL)
+      .setMultithreading(true)
+      .setFilePaths(inputFileByGitRelativePath.keySet());
+    try {
+      BlameResult blameResult = blameCommand.call();
+
+      for (Map.Entry<String, InputFile> e : inputFileByGitRelativePath.entrySet()) {
+        BlameResult.FileBlame fileBlameResult = blameResult.getFileBlameByPath().get(e.getKey());
+
+        if (fileBlameResult == null) {
+          LOG.debug("Unable to blame file {}.", e.getValue().filename());
+          continue;
+        }
+
+        saveBlameInformationForFileInTheOutput(fileBlameResult, e.getValue(), output);
+      }
+    } catch (GitAPIException e) {
+      LOG.warn("There was an issue when interacting with git repository: " + e.getMessage(), e);
+    }
+  }
+
   private boolean cloneIsInvalid(File gitBaseDir) {
     if (Files.isRegularFile(gitBaseDir.toPath().resolve(".git/objects/info/alternates"))) {
       LOG.info("This git repository references another local repository which is not well supported. SCM information might be missing for some files. "
@@ -166,4 +228,27 @@ public class CompositeBlameCommand extends BlameCommand {
 
     return false;
   }
+
+  private static void saveBlameInformationForFileInTheOutput(BlameResult.FileBlame fileBlame, InputFile file, BlameOutput output) {
+    List<BlameLine> linesList = new ArrayList<>();
+    for (int i = 0; i < fileBlame.lines(); i++) {
+      if (fileBlame.getAuthorEmails()[i] == null || fileBlame.getCommitHashes()[i] == null || fileBlame.getCommitDates()[i] == null) {
+        LOG.debug("Unable to blame file {}. No blame info at line {}. Is file committed? [Author: {} Source commit: {}]", file.filename());
+        linesList.clear();
+        break;
+      }
+      linesList.add(new BlameLine()
+        .date(fileBlame.getCommitDates()[i])
+        .revision(fileBlame.getCommitHashes()[i])
+        .author(fileBlame.getAuthorEmails()[i]));
+    }
+    if (!linesList.isEmpty()) {
+      if (linesList.size() == file.lines() - 1) {
+        // SONARPLUGINS-3097 Git does not report blame on last empty line
+        linesList.add(linesList.get(linesList.size() - 1));
+      }
+      output.blameResult(file, linesList);
+    }
+  }
+
 }
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java
deleted file mode 100644 (file)
index 9469ec1..0000000
+++ /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<String> 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<BlameLine> 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<BlameLine> blameLines = new LinkedList<>();
-    private String sha1 = null;
-    private String committerTime = null;
-    private String authorMail = null;
-
-    public List<BlameLine> 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 {
-
-  }
-}
index 70f7e2918d01e1f5c2e7f3372860b17bd16d8290..e06c95c7c482ace79a3ca7b43f3f6d66f5f52724 100644 (file)
@@ -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 (file)
index 0000000..0dd2e16
--- /dev/null
@@ -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<String> 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<BlameLine> 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<BlameLine> blameLines = new LinkedList<>();
+    private String sha1 = null;
+    private String committerTime = null;
+    private String authorMail = null;
+
+    public List<BlameLine> 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 (file)
index 0000000..7092d5e
--- /dev/null
@@ -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 (file)
index 0000000..d52aed2
--- /dev/null
@@ -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 (file)
index 0000000..3178cf6
--- /dev/null
@@ -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<String> 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<BlameAlgorithmEnum> 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<InputFile, List<BlameLine>> blame, File gitFolder) throws Exception {
+    Map<Path, List<BlameLine>> expectedBlame = readExpectedBlame(gitFolder.getName());
+
+    assertThat(blame.entrySet())
+      .as("Blamed files: " + blame.keySet() + ". Expected blamed files " + expectedBlame.keySet())
+      .hasSize(expectedBlame.size());
+
+    for (Map.Entry<InputFile, List<BlameLine>> 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<Path, List<BlameLine>> readExpectedBlame(String expectedBlameFolder) throws Exception {
+    Path expectedBlameFiles = new File(Utils.class.getResource("expected-blame/" + expectedBlameFolder).toURI()).toPath();
+    Map<Path, List<BlameLine>> expectedBlame = new HashMap<>();
+
+    List<Path> filesInExpectedBlameFolder = Files.walk(expectedBlameFiles).filter(Files::isRegularFile).collect(Collectors.toList());
+    for (Path expectedFileBlamePath : filesInExpectedBlameFolder) {
+      List<BlameLine> blameLines = new ArrayList<>();
+      List<String> 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<InputFile, List<BlameLine>> blame = new ConcurrentHashMap<>();
+
+    @Override
+    public void blameResult(InputFile inputFile, List<BlameLine> list) {
+      blame.put(inputFile, list);
+    }
+  }
+
+  private void setUpBlameInputWithFile(Path baseDir) throws IOException {
+    DefaultFileSystem fs = new DefaultFileSystem(baseDir);
+    when(input.fileSystem()).thenReturn(fs);
+
+    try (Stream<Path> stream = Files.walk(baseDir)) {
+      List<InputFile> 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);
+    }
+  }
+}
index 622196549bd22b5f6ccc3ef2261a475c62e5cab4..088ee40d541c0de9a40fd62b5b45c07934f6d803 100644 (file)
@@ -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<Object> 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 (file)
index 834f02b..0000000
+++ /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<BlameLine> 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<BlameLine> blame = blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
-
-    Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
-    String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
-    String author1 = "david@gageot.net";
-
-    // second commit, which has a commit date different than the author date
-    Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200");
-    String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7";
-    String author2 = "duarte.meneses@sonarsource.com";
-
-    List<BlameLine> expectedBlame = new LinkedList<>();
-    for (int i = 0; i < 25; i++) {
-      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
-    }
-    for (int i = 0; i < 3; i++) {
-      expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
-    }
-    for (int i = 0; i < 1; i++) {
-      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
-    }
-
-    assertThat(blame).isEqualTo(expectedBlame);
-  }
-
-  @Test
-  public void 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<BlameLine> blame = blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
-
-    Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
-    String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
-    String author1 = "david@gageot.net";
-
-    // second commit, which has a commit date different than the author date
-    Date revisionDate2 = DateUtils.parseDateTime("2022-10-11T14:14:26+0200");
-    String revision2 = "7609f824d5ff7018bebf107cdbe4edcc901b574f";
-    String author2 = "duarte.meneses@sonarsource.com";
-
-    List<BlameLine> expectedBlame = new LinkedList<>();
-    for (int i = 0; i < 25; i++) {
-      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
-    }
-    for (int i = 0; i < 3; i++) {
-      expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
-    }
-    for (int i = 0; i < 1; i++) {
-      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
-    }
-
-    assertThat(blame).isEqualTo(expectedBlame);
-  }
-
-  @Test
-  public void 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<String>) 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<BlameLine> blame = blameCommand.blame(baseDir, filePath);
-    assertThat(blame).hasSize(1);
-    BlameLine blameLine = blame.get(0);
-    assertThat(blameLine.author()).isNull();
-    assertThat(blameLine.revision()).isNotNull();
-    assertThat(blameLine.date()).isNotNull();
-  }
-
-  @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<BlameLine> 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<BlameLine> blame = blameCommand.blame(baseDir, filePath);
-    assertThat(blame).hasSize(1);
-    BlameLine blameLine = blame.get(0);
-    assertThat(blameLine.author()).isNull();
-    assertThat(blameLine.revision()).isNotNull();
-    assertThat(blameLine.date()).isNotNull();
-  }
-
-  @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<String>) 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<String>) 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<String>) invocation.getArgument(1);
-      argument.accept(commandOutput);
-      return mockProcess;
-    });
-
-    return mockFactory;
-  }
-}
index 242b31fe372a06111a31b0386c7ef67608cbb722..cb7d1f68aecdaf33a6ddf4c2694b6f5c5884351c 100644 (file)
@@ -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);
index 6257b1bb5123ccff9d3892d795dbd22333629ae1..13b152994fba88339a261c662fab42f65587b2ef 100644 (file)
@@ -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);
+  }
+
 
 }
index 2100475da9d566d16e79a3315f47e50caafd8979..af8a102f367839397bed1c461b616a2251021d4c 100644 (file)
@@ -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<BlameLine> 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 (file)
index 0000000..8bebca1
--- /dev/null
@@ -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<BlameLine> 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<BlameLine> blame = blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
+
+    Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
+    String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
+    String author1 = "david@gageot.net";
+
+    // second commit, which has a commit date different than the author date
+    Date revisionDate2 = DateUtils.parseDateTime("2015-05-19T13:31:09+0200");
+    String revision2 = "0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7";
+    String author2 = "duarte.meneses@sonarsource.com";
+
+    List<BlameLine> expectedBlame = new LinkedList<>();
+    for (int i = 0; i < 25; i++) {
+      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+    }
+    for (int i = 0; i < 3; i++) {
+      expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
+    }
+    for (int i = 0; i < 1; i++) {
+      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+    }
+
+    assertThat(blame).isEqualTo(expectedBlame);
+  }
+
+  @Test
+  public void 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<BlameLine> blame = blameCommand.blame(baseDir.toPath(), DUMMY_JAVA);
+
+    Date revisionDate1 = DateUtils.parseDateTime("2012-07-17T16:12:48+0200");
+    String revision1 = "6b3aab35a3ea32c1636fee56f996e677653c48ea";
+    String author1 = "david@gageot.net";
+
+    // second commit, which has a commit date different than the author date
+    Date revisionDate2 = DateUtils.parseDateTime("2022-10-11T14:14:26+0200");
+    String revision2 = "7609f824d5ff7018bebf107cdbe4edcc901b574f";
+    String author2 = "duarte.meneses@sonarsource.com";
+
+    List<BlameLine> expectedBlame = new LinkedList<>();
+    for (int i = 0; i < 25; i++) {
+      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+    }
+    for (int i = 0; i < 3; i++) {
+      expectedBlame.add(new BlameLine().revision(revision2).date(revisionDate2).author(author2));
+    }
+    for (int i = 0; i < 1; i++) {
+      expectedBlame.add(new BlameLine().revision(revision1).date(revisionDate1).author(author1));
+    }
+
+    assertThat(blame).isEqualTo(expectedBlame);
+  }
+
+  @Test
+  public void 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<String>) 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<BlameLine> blame = blameCommand.blame(baseDir, filePath);
+    assertThat(blame).hasSize(1);
+    BlameLine blameLine = blame.get(0);
+    assertThat(blameLine.author()).isNull();
+    assertThat(blameLine.revision()).isNotNull();
+    assertThat(blameLine.date()).isNotNull();
+  }
+
+  @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<BlameLine> 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<BlameLine> blame = blameCommand.blame(baseDir, filePath);
+    assertThat(blame).hasSize(1);
+    BlameLine blameLine = blame.get(0);
+    assertThat(blameLine.author()).isNull();
+    assertThat(blameLine.revision()).isNotNull();
+    assertThat(blameLine.date()).isNotNull();
+  }
+
+  @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<String>) 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<String>) 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<String>) 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 (file)
index 0000000..2cf351e
--- /dev/null
@@ -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 (file)
index 0000000..f87f63f
--- /dev/null
@@ -0,0 +1 @@
+37463a67cb907766a8fb2178fd4fc9a1b6f4f14f (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..b2ad74c
--- /dev/null
@@ -0,0 +1 @@
+5c2578e72d07e95781682a3ce1be61a954e39df8 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..cdb5989
--- /dev/null
@@ -0,0 +1 @@
+a7015bf40760ca91ae7d73d9102958fc870b30b9 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..1cf8ae4
--- /dev/null
@@ -0,0 +1 @@
+8bda33b1296875441f08c157140eb8504e13ebb2 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..9e9bcd2
--- /dev/null
@@ -0,0 +1 @@
+442ca0c2c76fe6eee3246cae40fe36c077f4d4a0 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..eaa6ca6
--- /dev/null
@@ -0,0 +1,5 @@
+d0da62499e8476f7d7599b37d2f2201770cf6790 (<lukasz.jarocki@sonarsource.com> 2023-03-01T10:31:46+01:00 1) first line
+bed9b1f0ae87ecda5ef1c805e303e2295676317b (<lukasz.jarocki@sonarsource.com> 2023-03-01T10:32:16+01:00 2) second line
+939282feaabde0d049e739fd3e30937836c6fc13 (<lukasz.jarocki@sonarsource.com> 2023-03-01T10:32:33+01:00 3) third line
+f4cfbd0b9fe74daea9dd588ec41e8da71505e959 (<lukasz.jarocki@sonarsource.com> 2023-03-01T10:32:46+01:00 4) fourth line
+5fa3e610adad109fdc89cdfa68592ddd7a0ba3c6 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..9949224
--- /dev/null
@@ -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 (file)
index 0000000..2424e4f
--- /dev/null
@@ -0,0 +1,52 @@
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  1) <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  2)     <modelVersion>4.0.0</modelVersion>
+783905e3d8b3e6f89a63c573d5fcf60af7845ebe (<david@gageot.net> 2012-07-20T18:52:36+02:00  3)     <groupId>dummy-git</groupId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  4)     <artifactId>dummy</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  5)     <version>1.0</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  6)     <packaging>jar</packaging>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  7)     <name>Dummy</name>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  8)
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  9)     <properties>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 10)         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 11)     </properties>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 12)
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 13)     <build>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 14)         <pluginManagement>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 15)             <plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 16)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 17)                     <artifactId>maven-clean-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 18)                     <version>2.4.1</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 19)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 20)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 21)                     <artifactId>maven-compiler-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 22)                     <version>2.3.2</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 23)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 24)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 25)                     <artifactId>maven-resources-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 26)                     <version>2.5</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 27)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 28)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 29)                     <artifactId>maven-source-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 30)                     <version>2.1.2</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 31)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 32)             </plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 33)         </pluginManagement>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 34)         <plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 35)             <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 36)                 <artifactId>maven-compiler-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 37)                 <configuration>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 38)                     <source>1.5</source>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 39)                     <target>1.5</target>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 40)                 </configuration>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 41)             </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 42)         </plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 43)     </build>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 44)
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 45)     <dependencies>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 46)         <dependency>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 47)             <groupId>junit</groupId>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 48)             <artifactId>junit</artifactId>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 49)             <version>4.10</version>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 50)         </dependency>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 51)     </dependencies>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 52) </project>
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 (file)
index 0000000..ffec5c6
--- /dev/null
@@ -0,0 +1,29 @@
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  1) /*
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  2)  * Sonar, open source software quality management tool.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  4)  * mailto:contact AT sonarsource DOT com
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  5)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  6)  * Sonar is free software; you can redistribute it and/or
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  8)  * License as published by the Free Software Foundation; either
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  9)  * version 3 of the License, or (at your option) any later version.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 10)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 14)  * Lesser General Public License for more details.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 15)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 17)  * License along with Sonar; if not, write to the Free Software
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 19)  */
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 20) package org.dummy;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 21)
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 22) public class AnotherDummy {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 23)   public String say(boolean hello) {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 24)     if (hello) {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 25)       return "Hello";
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 26)     }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 27)     return new String("GoodBye");
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 28)   }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 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 (file)
index 0000000..d549589
--- /dev/null
@@ -0,0 +1,29 @@
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  1) /*
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  2)  * Sonar, open source software quality management tool.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  4)  * mailto:contact AT sonarsource DOT com
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  5)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  6)  * Sonar is free software; you can redistribute it and/or
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  8)  * License as published by the Free Software Foundation; either
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  9)  * version 3 of the License, or (at your option) any later version.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 10)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 14)  * Lesser General Public License for more details.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 15)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 17)  * License along with Sonar; if not, write to the Free Software
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 19)  */
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 20) package org.dummy;
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 21)
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 22) public class Dummy {
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 23)   public String sayHello() {
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 24)     return "Hello";
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 25)   }
+7609f824d5ff7018bebf107cdbe4edcc901b574f (<duarte.meneses@sonarsource.com> 2022-10-11T14:14:26+02:00 26)   public String sayBye() {
+7609f824d5ff7018bebf107cdbe4edcc901b574f (<duarte.meneses@sonarsource.com> 2022-10-11T14:14:26+02:00 27)     return "Bye";
+7609f824d5ff7018bebf107cdbe4edcc901b574f (<duarte.meneses@sonarsource.com> 2022-10-11T14:14:26+02:00 28)   }
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               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 (file)
index 0000000..92c55be
--- /dev/null
@@ -0,0 +1,31 @@
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  1) /*
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  2)  * Sonar, open source software quality management tool.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  4)  * mailto:contact AT sonarsource DOT com
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  5)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  6)  * Sonar is free software; you can redistribute it and/or
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  8)  * License as published by the Free Software Foundation; either
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  9)  * version 3 of the License, or (at your option) any later version.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 10)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 14)  * Lesser General Public License for more details.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 15)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 17)  * License along with Sonar; if not, write to the Free Software
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 19)  */
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 20) package org.dummy;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 21)
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 22) import org.junit.Test;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 23)
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 24) import static org.junit.Assert.assertEquals;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 25)
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 26) public class AnotherDummyTest {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 27)   @Test
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 28)   public void should_say_hello() {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 29)     assertEquals("Hello", new AnotherDummy().say(true));
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 30)   }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 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 (file)
index 0000000..d770317
--- /dev/null
@@ -0,0 +1,52 @@
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  1) <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  2)     <modelVersion>4.0.0</modelVersion>
+783905e3d8b3e6f89a63c573d5fcf60af7845ebe (<david@gageot.net> 2012-07-20T18:52:36+02:00  3)     <groupId>dummy-git</groupId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  4)     <artifactId>dummy</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  5)     <version>1.0</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  6)     <packaging>jar</packaging>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  7)     <name>Dummy</name>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  8) 
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00  9)     <properties>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 10)         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 11)     </properties>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 12) 
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 13)     <build>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 14)         <pluginManagement>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 15)             <plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 16)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 17)                     <artifactId>maven-clean-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 18)                     <version>2.4.1</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 19)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 20)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 21)                     <artifactId>maven-compiler-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 22)                     <version>2.3.2</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 23)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 24)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 25)                     <artifactId>maven-resources-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 26)                     <version>2.5</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 27)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 28)                 <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 29)                     <artifactId>maven-source-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 30)                     <version>2.1.2</version>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 31)                 </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 32)             </plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 33)         </pluginManagement>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 34)         <plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 35)             <plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 36)                 <artifactId>maven-compiler-plugin</artifactId>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 37)                 <configuration>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 38)                     <source>1.5</source>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 39)                     <target>1.5</target>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 40)                 </configuration>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 41)             </plugin>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 42)         </plugins>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 43)     </build>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 44) 
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 45)     <dependencies>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 46)         <dependency>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 47)             <groupId>junit</groupId>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 48)             <artifactId>junit</artifactId>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 49)             <version>4.10</version>
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 50)         </dependency>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 51)     </dependencies>
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net> 2012-07-17T16:12:48+02:00 52) </project>
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 (file)
index 0000000..bf0da1e
--- /dev/null
@@ -0,0 +1,29 @@
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  1) /*
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  2)  * Sonar, open source software quality management tool.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  4)  * mailto:contact AT sonarsource DOT com
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  5)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  6)  * Sonar is free software; you can redistribute it and/or
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  8)  * License as published by the Free Software Foundation; either
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  9)  * version 3 of the License, or (at your option) any later version.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 10)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 14)  * Lesser General Public License for more details.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 15)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 17)  * License along with Sonar; if not, write to the Free Software
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 19)  */
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 20) package org.dummy;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 21) 
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 22) public class AnotherDummy {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 23)   public String say(boolean hello) {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 24)     if (hello) {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 25)       return "Hello";
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 26)     }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 27)     return new String("GoodBye");
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 28)   }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 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 (file)
index 0000000..8245e31
--- /dev/null
@@ -0,0 +1,29 @@
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  1) /*
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  2)  * Sonar, open source software quality management tool.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  4)  * mailto:contact AT sonarsource DOT com
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  5)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  6)  * Sonar is free software; you can redistribute it and/or
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  8)  * License as published by the Free Software Foundation; either
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00  9)  * version 3 of the License, or (at your option) any later version.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 10)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 14)  * Lesser General Public License for more details.
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 15)  *
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 17)  * License along with Sonar; if not, write to the Free Software
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 19)  */
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 20) package org.dummy;
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 21) 
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 22) public class Dummy {
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 23)   public String sayHello() {
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 24)     return "Hello";
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               2012-07-17T16:12:48+02:00 25)   }
+0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 (<duarte.meneses@sonarsource.com> 2015-05-19T13:31:09+02:00 26)   public String sayBye() {
+0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 (<duarte.meneses@sonarsource.com> 2015-05-19T13:31:09+02:00 27)     return "Bye";
+0d269c1acfb8e6d4d33f3c43041eb87e0df0f5e7 (<duarte.meneses@sonarsource.com> 2015-05-19T13:31:09+02:00 28)   }
+6b3aab35a3ea32c1636fee56f996e677653c48ea (<david@gageot.net>               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 (file)
index 0000000..c7df309
--- /dev/null
@@ -0,0 +1,31 @@
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  1) /*
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  2)  * Sonar, open source software quality management tool.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  3)  * Copyright (C) 2008-2012 SonarSource
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  4)  * mailto:contact AT sonarsource DOT com
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  5)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  6)  * Sonar is free software; you can redistribute it and/or
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  7)  * modify it under the terms of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  8)  * License as published by the Free Software Foundation; either
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00  9)  * version 3 of the License, or (at your option) any later version.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 10)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 11)  * Sonar is distributed in the hope that it will be useful,
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 12)  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 13)  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 14)  * Lesser General Public License for more details.
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 15)  *
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 16)  * You should have received a copy of the GNU Lesser General Public
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 17)  * License along with Sonar; if not, write to the Free Software
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 18)  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 19)  */
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 20) package org.dummy;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 21) 
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 22) import org.junit.Test;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 23) 
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 24) import static org.junit.Assert.assertEquals;
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 25) 
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 26) public class AnotherDummyTest {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 27)   @Test
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 28)   public void should_say_hello() {
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 29)     assertEquals("Hello", new AnotherDummy().say(true));
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 2012-07-20T15:26:23+02:00 30)   }
+ffb55a516440d069145dd9199e4ac67976b2338b (<david@gageot.net> 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 (file)
index 0000000..3fca0bd
--- /dev/null
@@ -0,0 +1,3 @@
+2b6ccf6d99dea2840613563aa54fdc5c8efc05cb (<lukasz.jarocki@sonarsource.com> 2023-03-01T14:43:49+01:00 1) first commit
+5a729e03b90e11d3d4f582a63cd2df93a7db41e0 (<lukasz.jarocki@sonarsource.com> 2023-03-01T14:44:47+01:00 2) second commit on master
+e62f234121479b8aa3bf60481324784c6ed6ba40 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..1a868d1
--- /dev/null
@@ -0,0 +1,10 @@
+691242bf1b8319a7398be257c206203f7d6b8743 file1.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:28:35+01:00  1) 111 - changed
+cb5f118c653af2e6d4d30f798714f45f5338c11c file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:29:03+01:00  2) 222 - changed
+84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:26:31+01:00  3) 333
+ea53e21a12061a733434454cf6cb8c6c29094fd0 file1.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:33:06+01:00  4) 444 - changed in master
+84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:26:31+01:00  5) 555
+223e2f4e80d72199dae13b94690099dccc20eeea file1.js (<lukasz.jarocki@sonarsource.com> 2023-03-01T10:52:44+01:00  6) 666 - changed on branch1
+84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:26:31+01:00  7) 777
+84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:26:31+01:00  8) 888
+84dd29d9b45487fecae9d952f1e71c5dea4cd356 file.js  (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:26:31+01:00  9) 999
+c700a4b511aeecb225b4bd86dbe964f29730cffd file.js  (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..348567b
--- /dev/null
@@ -0,0 +1 @@
+23628a3b878ae8dd5e51042c7a39057a3e4ce45c (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..d9ca90d
--- /dev/null
@@ -0,0 +1,5 @@
+13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:16:08+01:00 1) initial-change
+13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:16:08+01:00 2) second-change
+13bc62d2a546d41696e25e26ae44b3671f5ddf89 c.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:16:08+01:00 3) third-change
+3561639d487376f1d8c0297838fd2a94c5563d61 d.js (<lukasz.jarocki@sonarsource.com> 2023-02-24T10:16:39+01:00 4) fourth-change
+5123767d66854a431e9f6a05a88cf7db6e73aabc e.js (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..4d40b8e
--- /dev/null
@@ -0,0 +1,2 @@
+51c8ead53b248e93e66e5038c826524c0730f271 (<lukasz.jarocki@sonarsource.com> 2023-02-23T14:39:44+01:00 1) first line first commit
+b497efbe6a7ff547dee5dcc368b8920dba0fd908 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..167d039
--- /dev/null
@@ -0,0 +1 @@
+5ee03833d69a38d440a8685eb33dd96b0bea24e6 first.js (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..89de99f
--- /dev/null
@@ -0,0 +1 @@
+dab8b2a7c229b1111fbfe7c7dda7f9913ebb71f9 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..5ce08ea
--- /dev/null
@@ -0,0 +1 @@
+a66e868e71a5e871ca9d9d74035adc0a743ac537 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..927595f
--- /dev/null
@@ -0,0 +1 @@
+a66e868e71a5e871ca9d9d74035adc0a743ac537 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..06935cc
--- /dev/null
@@ -0,0 +1,4 @@
+48559ab7c378fb7181dc515967bd0dcfc71a8a57 (<lukasz.jarocki@sonarsource.com> 2023-03-01T14:54:30+01:00 1) first commit
+b5767c89ce517009ede7a6c7e69f46f4d2270fb5 (<lukasz.jarocki@sonarsource.com> 2023-03-01T14:56:53+01:00 2) fifth commit on master
+69cf0175d31c5a2c1517ac2d3515864324c436e9 (<lukasz.jarocki@sonarsource.com> 2023-03-01T14:55:02+01:00 3) second commit on branch
+c65549591670241841ae5882453ea05e51e3df20 (<lukasz.jarocki@sonarsource.com> 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 (file)
index 0000000..2e5068a
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 (file)
index 0000000..0c441f8
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 (file)
index 0000000..fcef9cd
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 (file)
index 0000000..60e9d2a
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 (file)
index 0000000..ce7e7b9
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 (file)
index 0000000..becfbf7
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 (file)
index 0000000..2efd3dd
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 (file)
index 0000000..81b25b8
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 (file)
index 0000000..1999eb6
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 (file)
index 0000000..d90349a
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 (file)
index 0000000..3f8c40f
Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/org/sonar/scm/git/test-repos/two-merge-commits.zip differ