]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13579 Detect files moves in Pull Request scope
authorKlaudio Sinani <klaudio.sinani@sonarsource.com>
Wed, 10 Aug 2022 11:37:31 +0000 (13:37 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 26 Oct 2022 20:03:10 +0000 (20:03 +0000)
SONAR-13579 Get database files from target branch instead of snapshot
SONAR-13579 Store old relative file path to `FileAttributes` class

23 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/Component.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/ComponentImpl.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/ComponentTreeBuilder.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/component/FileAttributes.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/PullRequestFileMoveDetectionStep.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueTrackingDelegator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MovedIssueVisitor.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultIndexedFile.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/fs/internal/DefaultInputFile.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/FileIndexer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFiles.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scm/git/ChangedFile.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/StatusDetectionTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scm/ScmChangedFilesProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scm/ScmChangedFilesTest.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto

index 4889401417ae1370b7a21bfa9676db5a2604d53f..69302fcbfcda34462d87520b3ed46ac8b0bfe7f6 100644 (file)
@@ -87,6 +87,11 @@ public interface Component {
    */
   String getName();
 
+  /**
+   * Get component old relative path.
+   */
+  String getOldName();
+
   /**
    * The component short name. For files and directories this is the parent relative path (ie filename for files). For projects and view this is the same as {@link #getName()}
    */
index a9ac6fbbd26d3f4d02527aba9b12595d313ca1ac..b06e66bb465a3ffaf0a4cb8ffb5bad9b68e0a641 100644 (file)
@@ -92,6 +92,11 @@ public class ComponentImpl implements Component {
     return this.name;
   }
 
+  @Override
+  public String getOldName() {
+    return this.getFileAttributes().getOldName();
+  }
+
   @Override
   public String getShortName() {
     return this.shortName;
@@ -159,6 +164,7 @@ public class ComponentImpl implements Component {
     private String uuid;
     private String key;
     private String name;
+    private String oldName;
     private String shortName;
     private String description;
     private FileAttributes fileAttributes;
index 4ca4a8ab048da61e7c86b4823079c19a4ae7c17d..d4f07d5cbdc5a44781e619d050bbee936d4c8ed6 100644 (file)
@@ -335,7 +335,9 @@ public class ComponentTreeBuilder {
       component.getIsTest(),
       lang != null ? lang.intern() : null,
       component.getLines(),
-      component.getMarkedAsUnchanged());
+      component.getMarkedAsUnchanged(),
+      component.getOldRelativeFilePath()
+    );
   }
 
   private static class Node {
index c1c868ec1f9408a0949530e738749543ad90aa73..fc62e592e149dfb2feac78ccffa46f483529bacb 100644 (file)
@@ -24,6 +24,9 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.commons.lang.StringUtils.abbreviate;
+import static org.apache.commons.lang.StringUtils.trimToNull;
+import static org.sonar.db.component.ComponentValidator.MAX_COMPONENT_NAME_LENGTH;
 
 /**
  * The attributes specific to a Component of type {@link Component.Type#FILE}.
@@ -35,17 +38,19 @@ public class FileAttributes {
   private final String languageKey;
   private final boolean markedAsUnchanged;
   private final int lines;
+  private String oldName;
 
   public FileAttributes(boolean unitTest, @Nullable String languageKey, int lines) {
-    this(unitTest, languageKey, lines, false);
+    this(unitTest, languageKey, lines, false, null);
   }
 
-  public FileAttributes(boolean unitTest, @Nullable String languageKey, int lines, boolean markedAsUnchanged) {
+  public FileAttributes(boolean unitTest, @Nullable String languageKey, int lines, boolean markedAsUnchanged, @Nullable String oldName) {
     this.unitTest = unitTest;
     this.languageKey = languageKey;
     this.markedAsUnchanged = markedAsUnchanged;
     checkArgument(lines > 0, "Number of lines must be greater than zero");
     this.lines = lines;
+    this.oldName = formatOldName(oldName);
   }
 
   public boolean isMarkedAsUnchanged() {
@@ -61,6 +66,11 @@ public class FileAttributes {
     return languageKey;
   }
 
+  @CheckForNull
+  public String getOldName() {
+    return oldName;
+  }
+
   /**
    * Number of lines of the file, can never be less than 1
    */
@@ -75,6 +85,11 @@ public class FileAttributes {
       ", unitTest=" + unitTest +
       ", lines=" + lines +
       ", markedAsUnchanged=" + markedAsUnchanged +
+      ", oldName=" + oldName +
       '}';
   }
+
+  private String formatOldName(@Nullable String name) {
+    return abbreviate(trimToNull(name), MAX_COMPONENT_NAME_LENGTH);
+  }
 }
index 2b484eea0148aa08b35d2358bc29362a0fcccd9f..7bddb40e7179c78ebca1aab5661dd50a281ac417 100644 (file)
@@ -97,6 +97,11 @@ public class FileMoveDetectionStep implements ComputationStep {
 
   @Override
   public void execute(ComputationStep.Context context) {
+    if (analysisMetadataHolder.isPullRequest()) {
+      LOG.debug("Currently within Pull Request scope. Do nothing.");
+      return;
+    }
+
     // do nothing if no files in db (first analysis)
     if (analysisMetadataHolder.isFirstAnalysis()) {
       LOG.debug("First analysis. Do nothing.");
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/PullRequestFileMoveDetectionStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/PullRequestFileMoveDetectionStep.java
new file mode 100644 (file)
index 0000000..77f172f
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.filemove;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import javax.annotation.concurrent.Immutable;
+import org.apache.ibatis.session.ResultHandler;
+import org.jetbrains.annotations.NotNull;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
+import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.FileMoveRowDto;
+
+import static java.util.stream.Collectors.toMap;
+import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
+
+public class PullRequestFileMoveDetectionStep implements ComputationStep {
+  private static final Logger LOG = Loggers.get(PullRequestFileMoveDetectionStep.class);
+
+  private final AnalysisMetadataHolder analysisMetadataHolder;
+  private final TreeRootHolder rootHolder;
+  private final DbClient dbClient;
+  private final MutableMovedFilesRepository movedFilesRepository;
+  private final MutableAddedFileRepository addedFileRepository;
+
+  public PullRequestFileMoveDetectionStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder rootHolder, DbClient dbClient,
+    MutableMovedFilesRepository movedFilesRepository, MutableAddedFileRepository addedFileRepository) {
+    this.analysisMetadataHolder = analysisMetadataHolder;
+    this.rootHolder = rootHolder;
+    this.dbClient = dbClient;
+    this.movedFilesRepository = movedFilesRepository;
+    this.addedFileRepository = addedFileRepository;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Detect file moves in Pull Request scope";
+  }
+
+  @Override
+  public void execute(ComputationStep.Context context) {
+    if (!analysisMetadataHolder.isPullRequest()) {
+      LOG.debug("Currently not within Pull Request scope. Do nothing.");
+      return;
+    }
+
+    Map<String, Component> reportFilesByUuid = getReportFilesByUuid(this.rootHolder.getRoot());
+    context.getStatistics().add("reportFiles", reportFilesByUuid.size());
+
+    if (reportFilesByUuid.isEmpty()) {
+      LOG.debug("No files in report. No file move detection.");
+      return;
+    }
+
+    Map<String, DbComponent> targetBranchDbFilesByUuid = getTargetBranchDbFilesByUuid(analysisMetadataHolder);
+    context.getStatistics().add("dbFiles", targetBranchDbFilesByUuid.size());
+
+    if (targetBranchDbFilesByUuid.isEmpty()) {
+      registerNewlyAddedFiles(reportFilesByUuid);
+      context.getStatistics().add("addedFiles", reportFilesByUuid.size());
+      LOG.debug("Previous snapshot has no file. No file move detection.");
+      return;
+    }
+
+    Map<String, Component> movedFilesByUuid = getMovedFilesByUuid(reportFilesByUuid);
+    context.getStatistics().add("movedFiles", movedFilesByUuid.size());
+
+    Map<String, Component> newlyAddedFilesByUuid = getNewlyAddedFilesByUuid(reportFilesByUuid, targetBranchDbFilesByUuid);
+    context.getStatistics().add("addedFiles", newlyAddedFilesByUuid.size());
+
+    // Do we need to register the moved file in the moved files repo and use the data in the related steps/visitors?
+//    registerMovedFiles(movedFilesByUuid, targetBranchDbFilesByUuid);
+    registerNewlyAddedFiles(newlyAddedFilesByUuid);
+  }
+
+  private void registerMovedFiles(Map<String, Component> movedFilesByUuid, Map<String, DbComponent> dbFilesByUuid) {
+    movedFilesByUuid
+      .forEach((movedFileUuid, movedFile) -> {
+        DbComponent oldFile = getOldFile(dbFilesByUuid.values(), movedFile.getOldName());
+        movedFilesRepository.setOriginalFile(movedFile, toOriginalFile(oldFile));
+      });
+  }
+
+  private void registerNewlyAddedFiles(Map<String, Component> newAddedFilesByUuid) {
+    newAddedFilesByUuid
+      .values()
+      .forEach(addedFileRepository::register);
+  }
+
+  @NotNull
+  private Map<String, Component> getNewlyAddedFilesByUuid(Map<String, Component> reportFilesByUuid, Map<String, DbComponent> dbFilesByUuid) {
+    return reportFilesByUuid
+      .values()
+      .stream()
+      .filter(file -> Objects.isNull(file.getOldName()))
+      .filter(file -> !dbFilesByUuid.containsKey(file.getUuid()))
+      .collect(toMap(Component::getUuid, Function.identity()));
+  }
+
+  private Map<String, Component> getMovedFilesByUuid(Map<String, Component> reportFilesByUuid) {
+    return reportFilesByUuid
+      .values()
+      .stream()
+      .filter(file -> Objects.nonNull(file.getOldName()))
+      .collect(toMap(Component::getUuid, Function.identity()));
+  }
+
+  private DbComponent getOldFile(Collection<DbComponent> dbFiles, String oldFilePath) {
+    return dbFiles
+      .stream()
+      .filter(file -> file.getPath().equals(oldFilePath))
+      .findFirst()
+      .get();
+  }
+
+  public Set<String> difference(Set<String> set1, Set<String> set2) {
+    if (set1.isEmpty() || set2.isEmpty()) {
+      return set1;
+    }
+
+    return Sets.difference(set1, set2).immutableCopy();
+  }
+
+  private Map<String, DbComponent> getTargetBranchDbFilesByUuid(AnalysisMetadataHolder analysisMetadataHolder) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      String targetBranchUuid = getTargetBranchUuid(dbSession, analysisMetadataHolder.getProject().getUuid(), analysisMetadataHolder.getBranch().getTargetBranchName());
+
+      return getTargetBranchDbFiles(dbSession, targetBranchUuid)
+        .stream()
+        .collect(toMap(DbComponent::getUuid, Function.identity()));
+    }
+  }
+
+  private List<DbComponent> getTargetBranchDbFiles(DbSession dbSession, String targetBranchUuid) {
+     List<DbComponent> files = new LinkedList();
+
+    ResultHandler<FileMoveRowDto> storeFileMove = resultContext -> {
+      FileMoveRowDto row = resultContext.getResultObject();
+      files.add(new DbComponent(row.getKey(), row.getUuid(), row.getPath(), row.getLineCount()));
+    };
+
+    dbClient.componentDao().scrollAllFilesForFileMove(dbSession, targetBranchUuid, storeFileMove);
+
+    return files;
+  }
+
+  private String getTargetBranchUuid(DbSession dbSession, String projectUuid, String targetBranchName) {
+      return dbClient.branchDao().selectByBranchKey(dbSession, projectUuid, targetBranchName)
+        .map(BranchDto::getUuid)
+        .orElseThrow(() -> new IllegalStateException("Pull Request has no target branch"));
+  }
+
+  private static Map<String, Component> getReportFilesByUuid(Component root) {
+    final ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
+
+    new DepthTraversalTypeAwareCrawler(
+      new TypeAwareVisitorAdapter(CrawlerDepthLimit.FILE, POST_ORDER) {
+        @Override
+        public void visitFile(Component file) {
+          builder.put(file.getUuid(), file);
+        }
+      }).visit(root);
+
+    return builder.build();
+  }
+
+  private static MovedFilesRepository.OriginalFile toOriginalFile(DbComponent dbComponent) {
+    return new MovedFilesRepository.OriginalFile(dbComponent.getUuid(), dbComponent.getKey());
+  }
+
+  @Immutable
+  private static final class DbComponent {
+    private final String key;
+    private final String uuid;
+    private final String path;
+    private final int lineCount;
+
+    private DbComponent(String key, String uuid, String path, int lineCount) {
+      this.key = key;
+      this.uuid = uuid;
+      this.path = path;
+      this.lineCount = lineCount;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getUuid() {
+      return uuid;
+    }
+
+    public String getPath() {
+      return path;
+    }
+
+    public int getLineCount() {
+      return lineCount;
+    }
+  }
+}
index f1226671b359b042dca971cceb5fa6c724aa38f9..0aab874f3b1f3eeb4574af50249c7e5df8b8a9ff 100644 (file)
@@ -45,12 +45,14 @@ public class IssueTrackingDelegator {
   public TrackingResult track(Component component, Input<DefaultIssue> rawInput) {
     if (analysisMetadataHolder.isPullRequest()) {
       return standardResult(pullRequestTracker.track(component, rawInput));
-    } else if (isFirstAnalysisSecondaryBranch()) {
+    }
+
+    if (isFirstAnalysisSecondaryBranch()) {
       Tracking<DefaultIssue, DefaultIssue> tracking = referenceBranchTracker.track(component, rawInput);
       return new TrackingResult(tracking.getMatchedRaws(), emptyMap(), empty(), tracking.getUnmatchedRaws());
-    } else {
-      return standardResult(tracker.track(component, rawInput));
     }
+
+    return standardResult(tracker.track(component, rawInput));
   }
 
   private static TrackingResult standardResult(Tracking<DefaultIssue, DefaultIssue> tracking) {
index e10912827465333e311fda93dff76e2b420e7d82..4c31b335b4606e2908e6e1f68916dcceb137fb90 100644 (file)
@@ -55,7 +55,7 @@ public class MovedIssueVisitor extends IssueVisitor {
       "Issue %s doesn't belong to file %s registered as original file of current file %s",
       issue, originalFile.getUuid(), component);
 
-    // changes the issue's component uuid, and set issue as changed to enforce it is persisted to DB
+    // changes the issue's component uuid, and set issue as changed, to enforce it is persisted to DB
     issueUpdater.setIssueComponent(issue, component.getUuid(), component.getKey(), new Date(analysisMetadataHolder.getAnalysisDate()));
   }
 }
index 2ba64a8e3e75b95a9e0ca266a86a3384863cc37b..370dbd93c44b83aaeda548cd7b16f332321637d1 100644 (file)
@@ -59,10 +59,13 @@ public class TrackerBaseInputFactory extends BaseInputFactory {
   public Input<DefaultIssue> create(Component component) {
     if (component.getType() == Component.Type.PROJECT) {
       return new ProjectTrackerBaseLazyInput(analysisMetadataHolder, componentsWithUnprocessedIssues, dbClient, issueUpdater, issuesLoader, reportModulesPath, component);
-    } else if (component.getType() == Component.Type.DIRECTORY) {
+    }
+
+    if (component.getType() == Component.Type.DIRECTORY) {
       // Folders have no issues
       return new EmptyTrackerBaseLazyInput(dbClient, component);
     }
+
     return new FileTrackerBaseLazyInput(dbClient, component, movedFilesRepository.getOriginalFile(component).orElse(null));
   }
 
index a482de93c0d0f215d2522194db1e8ef2bc3e0b11..e5a7b038052238c2934c1c1d1f10dbcb04dbaea5 100644 (file)
@@ -23,6 +23,7 @@ import java.util.Arrays;
 import java.util.List;
 import org.sonar.ce.task.container.TaskContainer;
 import org.sonar.ce.task.projectanalysis.filemove.FileMoveDetectionStep;
+import org.sonar.ce.task.projectanalysis.filemove.PullRequestFileMoveDetectionStep;
 import org.sonar.ce.task.projectanalysis.language.HandleUnanalyzedLanguagesStep;
 import org.sonar.ce.task.projectanalysis.measure.PostMeasuresComputationChecksStep;
 import org.sonar.ce.task.projectanalysis.purge.PurgeDatastoresStep;
@@ -54,6 +55,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
     LoadQualityGateStep.class,
     LoadPeriodsStep.class,
     FileMoveDetectionStep.class,
+    PullRequestFileMoveDetectionStep.class,
 
     // load duplications related stuff
     LoadDuplicationsFromReportStep.class,
index e18b00ca1f9541fc4dc1254d3f79d5f1439855de..be062dec66800f77bf994a6cac69edc3d0df9187 100644 (file)
@@ -25,6 +25,7 @@ import java.io.InputStream;
 import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
@@ -38,7 +39,7 @@ import org.sonar.api.utils.PathUtils;
  */
 @Immutable
 public class DefaultIndexedFile extends DefaultInputComponent implements IndexedFile {
-  private static AtomicInteger intGenerator = new AtomicInteger(0);
+  private static final AtomicInteger intGenerator = new AtomicInteger(0);
 
   private final String projectRelativePath;
   private final String moduleRelativePath;
@@ -47,17 +48,23 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
   private final Type type;
   private final Path absolutePath;
   private final SensorStrategy sensorStrategy;
+  private final String oldFilePath;
 
   /**
    * Testing purposes only!
    */
   public DefaultIndexedFile(String projectKey, Path baseDir, String relativePath, @Nullable String language) {
     this(baseDir.resolve(relativePath), projectKey, relativePath, relativePath, Type.MAIN, language, intGenerator.getAndIncrement(),
-      new SensorStrategy());
+      new SensorStrategy(), null);
   }
 
   public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId,
     SensorStrategy sensorStrategy) {
+    this(absolutePath, projectKey, projectRelativePath, moduleRelativePath, type, language, batchId, sensorStrategy, null);
+  }
+
+  public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId,
+    SensorStrategy sensorStrategy, @Nullable String oldFilePath) {
     super(batchId);
     this.projectKey = projectKey;
     this.projectRelativePath = PathUtils.sanitize(projectRelativePath);
@@ -66,6 +73,7 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
     this.language = language;
     this.sensorStrategy = sensorStrategy;
     this.absolutePath = absolutePath;
+    this.oldFilePath = oldFilePath;
   }
 
   @Override
@@ -96,6 +104,15 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
     return absolutePath;
   }
 
+  @CheckForNull
+  public String oldPath() {
+    return oldFilePath;
+  }
+
+  public boolean isMovedFile() {
+    return Objects.nonNull(this.oldPath());
+  }
+
   @Override
   public InputStream inputStream() throws IOException {
     return Files.newInputStream(path());
@@ -117,7 +134,7 @@ public class DefaultIndexedFile extends DefaultInputComponent implements Indexed
    */
   @Override
   public String key() {
-    return new StringBuilder().append(projectKey).append(":").append(projectRelativePath).toString();
+    return String.join(":", projectKey, projectRelativePath);
   }
 
   @Override
index 382461fbc5a64b507418f82535aa96608cd7a784..fe4c116bebd5a82cbe259da78de45ea4e0baa818 100644 (file)
@@ -177,6 +177,15 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile
     return indexedFile.absolutePath();
   }
 
+  @CheckForNull
+  public String oldPath() {
+    return indexedFile.oldPath();
+  }
+  
+  public boolean isMovedFile() {
+    return indexedFile.isMovedFile();
+  }
+
   @Override
   public File file() {
     return indexedFile.file();
index 19bb51e1695d4a0d00f0bd4ca2d7be5c7afc5f04..56737d1e06874eed368a8d68f6b65f424f923b32 100644 (file)
@@ -39,6 +39,7 @@ import org.sonar.scanner.repository.ReferenceBranchSupplier;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonar.scanner.scan.filesystem.InputComponentStore;
 import org.sonar.scanner.scm.ScmConfiguration;
+import org.sonar.scm.git.GitScmProvider;
 
 import static java.util.Optional.empty;
 
@@ -89,7 +90,7 @@ public class ChangedLinesPublisher implements ReportPublisherStep {
     Map<Path, DefaultInputFile> changedFiles = StreamSupport.stream(inputComponentStore.allChangedFilesToPublish().spliterator(), false)
       .collect(Collectors.toMap(DefaultInputFile::path, f -> f));
 
-    Map<Path, Set<Integer>> pathSetMap = provider.branchChangedLines(targetScmBranch, rootBaseDir, changedFiles.keySet());
+    Map<Path, Set<Integer>> pathSetMap = ((GitScmProvider) provider).branchChangedLines(targetScmBranch, rootBaseDir, changedFiles); // TODO: Extend ScmProvider abstract
     int count = 0;
 
     if (pathSetMap == null) {
index 4ca7c8be7804b14a3912fae3c6f1e343331bc9eb..b21da5f2f94c804f4dc14386dc91c3f1269081c8 100644 (file)
@@ -68,6 +68,10 @@ public class ComponentsPublisher implements ReportPublisherStep {
       fileBuilder.setStatus(convert(file.status()));
       fileBuilder.setMarkedAsUnchanged(file.isMarkedAsUnchanged());
 
+      if (file.isMovedFile()) {
+        fileBuilder.setOldRelativeFilePath(file.oldPath());
+      }
+
       String lang = getLanguageKey(file);
       if (lang != null) {
         fileBuilder.setLanguage(lang);
index 7ea8799c855ae0588c46a42eb079419626590fd3..4653c24d171f6933eaaef8d146c4ca27c3804ca9 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.api.utils.log.Loggers;
 import org.sonar.scanner.issue.ignore.scanner.IssueExclusionsLoader;
 import org.sonar.scanner.repository.language.Language;
 import org.sonar.scanner.scan.ScanProperties;
+import org.sonar.scanner.scm.ScmChangedFiles;
 import org.sonar.scanner.util.ProgressReport;
 
 import static java.lang.String.format;
@@ -66,6 +67,7 @@ public class FileIndexer {
   private final InputComponentStore componentStore;
   private final SensorStrategy sensorStrategy;
   private final LanguageDetection langDetection;
+  private final ScmChangedFiles scmChangedFiles;
 
   private boolean warnInclusionsAlreadyLogged;
   private boolean warnExclusionsAlreadyLogged;
@@ -75,7 +77,7 @@ public class FileIndexer {
   public FileIndexer(DefaultInputProject project, ScannerComponentIdGenerator scannerComponentIdGenerator, InputComponentStore componentStore,
     ProjectExclusionFilters projectExclusionFilters, ProjectCoverageAndDuplicationExclusions projectCoverageAndDuplicationExclusions, IssueExclusionsLoader issueExclusionsLoader,
     MetadataGenerator metadataGenerator, SensorStrategy sensorStrategy, LanguageDetection languageDetection, AnalysisWarnings analysisWarnings, ScanProperties properties,
-    InputFileFilter[] filters) {
+    InputFileFilter[] filters, ScmChangedFiles scmChangedFiles) {
     this.project = project;
     this.scannerComponentIdGenerator = scannerComponentIdGenerator;
     this.componentStore = componentStore;
@@ -88,6 +90,7 @@ public class FileIndexer {
     this.properties = properties;
     this.filters = filters;
     this.projectExclusionFilters = projectExclusionFilters;
+    this.scmChangedFiles = scmChangedFiles;
   }
 
   void indexFile(DefaultInputModule module, ModuleExclusionFilters moduleExclusionFilters, ModuleCoverageAndDuplicationExclusions moduleCoverageAndDuplicationExclusions,
@@ -124,10 +127,18 @@ public class FileIndexer {
       return;
     }
 
-    DefaultIndexedFile indexedFile = new DefaultIndexedFile(realAbsoluteFile, project.key(),
+    DefaultIndexedFile indexedFile = new DefaultIndexedFile(
+      realAbsoluteFile,
+      project.key(),
       projectRelativePath.toString(),
       moduleRelativePath.toString(),
-      type, language != null ? language.key() : null, scannerComponentIdGenerator.getAsInt(), sensorStrategy);
+      type,
+      language != null ? language.key() : null,
+      scannerComponentIdGenerator.getAsInt(),
+      sensorStrategy,
+      scmChangedFiles.getFileOldPath(realAbsoluteFile)
+    );
+
     DefaultInputFile inputFile = new DefaultInputFile(indexedFile, f -> metadataGenerator.setMetadata(module.key(), f, module.getEncoding()));
     if (language != null && language.isPublishAllFiles()) {
         inputFile.setPublished(true);
index 0491d6b4281aaa1d89752b911afab4645bf642d8..010f2de4cbf3f0ca8de8910f244930098c96bc1a 100644 (file)
@@ -21,17 +21,21 @@ package org.sonar.scanner.scm;
 
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
+import org.sonar.scm.git.ChangedFile;
 
 @Immutable
 public class ScmChangedFiles {
   @Nullable
-  private final Collection<Path> fileCollection;
+  private final Collection<ChangedFile> changedFiles;
 
-  public ScmChangedFiles(@Nullable Collection<Path> changedFiles) {
-    this.fileCollection = changedFiles;
+  public ScmChangedFiles(@Nullable Collection<ChangedFile> changedFiles) {
+    this.changedFiles = changedFiles;
   }
 
   public boolean isChanged(Path file) {
@@ -39,15 +43,33 @@ public class ScmChangedFiles {
       throw new IllegalStateException("Scm didn't provide valid data");
     }
 
-    return fileCollection.contains(file);
+    return this.findFile(file).isPresent();
   }
 
   public boolean isValid() {
-    return fileCollection != null;
+    return changedFiles != null;
   }
 
   @CheckForNull
-  Collection<Path> get() {
-    return fileCollection;
+  public Collection<ChangedFile> get() {
+    return changedFiles;
+  }
+
+  @CheckForNull
+  public String getFileOldPath(Path absoluteFilePath) {
+    return this.findFile(absoluteFilePath)
+      .filter(ChangedFile::isMoved)
+      .map(ChangedFile::getOldFilePath)
+      .orElse(null);
+  }
+
+  private Optional<ChangedFile> findFile(Path absoluteFilePath) {
+    Predicate<ChangedFile> isTargetFile = file -> file.getAbsolutFilePath().equals(absoluteFilePath);
+
+    return Optional.ofNullable(this.get())
+      .orElseGet(List::of)
+      .stream()
+      .filter(isTargetFile)
+      .findFirst();
   }
 }
index 5a46e41a90f43b730e15c9c9f7036628e8df45c1..392996d7e7438b03445dd97fd03d7df4baf3f652 100644 (file)
@@ -21,14 +21,17 @@ package org.sonar.scanner.scm;
 
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
 import org.sonar.api.batch.fs.internal.DefaultInputProject;
-import org.sonar.api.batch.scm.ScmProvider;
 import org.sonar.api.impl.utils.ScannerUtils;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
+import org.sonar.scm.git.ChangedFile;
+import org.sonar.scm.git.GitScmProvider;
 import org.springframework.context.annotation.Bean;
 
 public class ScmChangedFilesProvider {
@@ -38,25 +41,36 @@ public class ScmChangedFilesProvider {
   @Bean("ScmChangedFiles")
   public ScmChangedFiles provide(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, DefaultInputProject project) {
     Path rootBaseDir = project.getBaseDir();
-    Collection<Path> changedFiles = loadChangedFilesIfNeeded(scmConfiguration, branchConfiguration, rootBaseDir);
-    validatePaths(changedFiles);
+    Collection<ChangedFile> changedFiles = loadChangedFilesIfNeeded(scmConfiguration, branchConfiguration, rootBaseDir);
+
+    if (changedFiles != null) {
+      validatePaths(getFilePaths(changedFiles));
+    }
+
     return new ScmChangedFiles(changedFiles);
   }
 
-  private static void validatePaths(@javax.annotation.Nullable Collection<Path> paths) {
-    if (paths != null && paths.stream().anyMatch(p -> !p.isAbsolute())) {
+  private static void validatePaths(Set<Path> changedFilePaths) {
+    if (changedFilePaths != null && changedFilePaths.stream().anyMatch(p -> !p.isAbsolute())) {
       throw new IllegalStateException("SCM provider returned a changed file with a relative path but paths must be absolute. Please fix the provider.");
     }
   }
 
+  private static Set<Path> getFilePaths(Collection<ChangedFile> changedFiles) {
+    return changedFiles
+      .stream()
+      .map(ChangedFile::getAbsolutFilePath)
+      .collect(Collectors.toSet());
+  }
+
   @CheckForNull
-  private static Collection<Path> loadChangedFilesIfNeeded(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, Path rootBaseDir) {
+  private static Collection<ChangedFile> loadChangedFilesIfNeeded(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, Path rootBaseDir) {
     final String targetBranchName = branchConfiguration.targetBranchName();
     if (branchConfiguration.isPullRequest() && targetBranchName != null) {
-      ScmProvider scmProvider = scmConfiguration.provider();
+      GitScmProvider scmProvider = (GitScmProvider) scmConfiguration.provider();
       if (scmProvider != null) {
         Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
-        Collection<Path> changedFiles = scmProvider.branchChangedFiles(targetBranchName, rootBaseDir);
+        Collection<ChangedFile> changedFiles = scmProvider.branchModifiedFiles(targetBranchName, rootBaseDir);
         profiler.stopInfo();
         if (changedFiles != null) {
           LOG.debug("SCM reported {} {} changed in the branch", changedFiles.size(), ScannerUtils.pluralize("file", changedFiles.size()));
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/ChangedFile.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/ChangedFile.java
new file mode 100644 (file)
index 0000000..6f30d24
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scm.git;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class ChangedFile {
+  @Nullable
+  private final String oldFilePath;
+  private final String filePath;
+  private final Path absoluteFilePath;
+
+  public ChangedFile(String filePath, Path absoluteFilePath) {
+    this(filePath, absoluteFilePath, null);
+  }
+
+  public ChangedFile(String filePath, Path absoluteFilePath, @Nullable String oldFilePath) {
+    this.filePath = filePath;
+    this.oldFilePath = oldFilePath;
+    this.absoluteFilePath =  absoluteFilePath;
+  }
+
+  @CheckForNull
+  public String getOldFilePath() {
+    return oldFilePath;
+  }
+
+  public boolean isMoved() {
+    return Objects.nonNull(this.getOldFilePath());
+  }
+
+  public String getFilePath() {
+    return filePath;
+  }
+
+  public Path getAbsolutFilePath() {
+    return absoluteFilePath;
+  }
+}
index bef15b669bc3a80ee3865fee9ba1dbfe6464d318..c51035ee8f1a90a36bfe0d0d25aa5247e6c96fe8 100644 (file)
@@ -24,11 +24,17 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
@@ -36,8 +42,10 @@ import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.diff.RenameDetector;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -52,6 +60,9 @@ import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
 import org.sonar.api.batch.scm.BlameCommand;
 import org.sonar.api.batch.scm.ScmProvider;
 import org.sonar.api.notifications.AnalysisWarnings;
@@ -60,6 +71,10 @@ import org.sonar.api.utils.System2;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
+
 public class GitScmProvider extends ScmProvider {
 
   private static final Logger LOG = Loggers.get(GitScmProvider.class);
@@ -141,6 +156,87 @@ public class GitScmProvider extends ScmProvider {
     return null;
   }
 
+  // TODO: Adjust ScmProvider abstract
+  @CheckForNull
+  public Collection<ChangedFile> branchModifiedFiles(String targetBranchName, Path rootBaseDir) {
+    try (Repository repo = buildRepo(rootBaseDir)) {
+      Ref targetRef = resolveTargetRef(targetBranchName, repo);
+      if (targetRef == null) {
+        addWarningTargetNotFound(targetBranchName);
+        return null;
+      }
+
+      if (isDiffAlgoInvalid(repo.getConfig())) {
+        LOG.warn("The diff algorithm configured in git is not supported. "
+          + "No information regarding changes in the branch will be collected, which can lead to unexpected results.");
+        return null;
+      }
+
+      Optional<RevCommit> mergeBaseCommit = findMergeBase(repo, targetRef);
+      if (mergeBaseCommit.isEmpty()) {
+        LOG.warn("No merge base found between HEAD and " + targetRef.getName());
+        return null;
+      }
+      AbstractTreeIterator mergeBaseTree = prepareTreeParser(repo, mergeBaseCommit.get());
+
+      // we compare a commit with HEAD, so no point ignoring line endings (it will be whatever is committed)
+      try (Git git = newGit(repo)) {
+        List<DiffEntry> diffEntries = git.diff()
+          .setShowNameAndStatusOnly(true)
+          .setOldTree(mergeBaseTree)
+          .setNewTree(prepareNewTree(repo))
+          .call();
+
+        return computeChangedFiles(repo, diffEntries);
+      }
+    } catch (IOException | GitAPIException e) {
+      LOG.warn(e.getMessage(), e);
+    }
+    return null;
+  }
+
+  private static List<ChangedFile> computeChangedFiles(Repository repository, List<DiffEntry> diffEntries) throws IOException {
+    Path workingDirectory = repository.getWorkTree().toPath();
+
+    Map<String, String> renamedFilePaths = computeRenamedFilePaths(repository, diffEntries);
+    Set<String> changedFilePaths = computeChangedFilePaths(diffEntries);
+
+    List<ChangedFile> changedFiles = new LinkedList<>();
+
+    Consumer<String> collectChangedFiles = filePath -> changedFiles.add(new ChangedFile(filePath, workingDirectory.resolve(filePath), renamedFilePaths.getOrDefault(filePath, null)));
+    changedFilePaths.forEach(collectChangedFiles);
+
+    return changedFiles;
+  }
+
+  private static Map<String, String> computeRenamedFilePaths(Repository repository, List<DiffEntry> diffEntries) throws IOException {
+    RenameDetector renameDetector = new RenameDetector(repository);
+    renameDetector.addAll(diffEntries);
+
+    return renameDetector
+      .compute()
+      .stream()
+      .filter(entry -> RENAME.equals(entry.getChangeType()))
+      .collect(Collectors.toUnmodifiableMap(DiffEntry::getNewPath, DiffEntry::getOldPath));
+  }
+
+  private static Set<String> computeChangedFilePaths(List<DiffEntry> diffEntries) {
+    return diffEntries
+      .stream()
+      .filter(isAllowedChangeType(ADD, MODIFY))
+      .map(DiffEntry::getNewPath)
+      .collect(Collectors.toSet());
+  }
+
+  private static Predicate<DiffEntry> isAllowedChangeType(ChangeType ...changeTypes) {
+    Function<ChangeType, Predicate<DiffEntry>> isChangeType = type -> entry -> type.equals(entry.getChangeType());
+
+    return Arrays
+      .stream(changeTypes)
+      .map(isChangeType)
+      .reduce(x -> false, Predicate::or);
+  }
+
   @CheckForNull
   @Override
   public Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path projectBaseDir, Set<Path> changedFiles) {
@@ -177,6 +273,43 @@ public class GitScmProvider extends ScmProvider {
     return null;
   }
 
+  // TODO: Adjust ScmProvider abstract
+  public Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path projectBaseDir,  Map<Path, DefaultInputFile> changedFiles) {
+    try (Repository repo = buildRepo(projectBaseDir)) {
+      Ref targetRef = resolveTargetRef(targetBranchName, repo);
+      if (targetRef == null) {
+        addWarningTargetNotFound(targetBranchName);
+        return null;
+      }
+
+      if (isDiffAlgoInvalid(repo.getConfig())) {
+        // we already print a warning when branchChangedFiles is called
+        return null;
+      }
+
+      // force ignore different line endings when comparing a commit with the workspace
+      repo.getConfig().setBoolean("core", null, "autocrlf", true);
+
+      Optional<RevCommit> mergeBaseCommit = findMergeBase(repo, targetRef);
+
+      if (mergeBaseCommit.isEmpty()) {
+        LOG.warn("No merge base found between HEAD and " + targetRef.getName());
+        return null;
+      }
+
+      Map<Path, Set<Integer>> changedLines = new HashMap<>();
+
+      for (Map.Entry<Path, DefaultInputFile> entry : changedFiles.entrySet()) {
+        collectChangedLines(repo, mergeBaseCommit.get(), changedLines, entry.getKey(), entry.getValue());
+      }
+
+      return changedLines;
+    } catch (Exception e) {
+      LOG.warn("Failed to get changed lines from git", e);
+    }
+    return null;
+  }
+
   private void addWarningTargetNotFound(String targetBranchName) {
     analysisWarnings.addUnique(String.format(COULD_NOT_FIND_REF
       + ". You may see unexpected issues and changes. "
@@ -211,6 +344,36 @@ public class GitScmProvider extends ScmProvider {
     }
   }
 
+  // TODO: Adjust ScmProvider abstract
+  private void collectChangedLines(Repository repo, RevCommit mergeBaseCommit, Map<Path, Set<Integer>> changedLines, Path changedFilePath, DefaultInputFile changedFileData) {
+    ChangedLinesComputer computer = new ChangedLinesComputer();
+
+    try (DiffFormatter diffFmt = new DiffFormatter(new BufferedOutputStream(computer.receiver()))) {
+      diffFmt.setRepository(repo);
+      diffFmt.setProgressMonitor(NullProgressMonitor.INSTANCE);
+      diffFmt.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
+
+      diffFmt.setDetectRenames(changedFileData.isMovedFile());
+
+      Path workTree = repo.getWorkTree().toPath();
+      TreeFilter treeFilter = getTreeFilter(changedFileData, workTree);
+      diffFmt.setPathFilter(treeFilter);
+
+      AbstractTreeIterator mergeBaseTree = prepareTreeParser(repo, mergeBaseCommit);
+      List<DiffEntry> diffEntries = diffFmt.scan(mergeBaseTree, new FileTreeIterator(repo));
+
+      diffFmt.format(diffEntries);
+      diffFmt.flush();
+
+      diffEntries.stream()
+        .filter(isAllowedChangeType(ADD, MODIFY, RENAME))
+        .findAny()
+        .ifPresent(diffEntry -> changedLines.put(changedFilePath, computer.changedLines()));
+    } catch (Exception e) {
+      LOG.warn("Failed to get changed lines from git for file " + changedFilePath, e);
+    }
+  }
+
   @Override
   @CheckForNull
   public Instant forkDate(String referenceBranchName, Path projectBaseDir) {
@@ -221,6 +384,17 @@ public class GitScmProvider extends ScmProvider {
     return path.replaceAll(Pattern.quote(File.separator), "/");
   }
 
+  private TreeFilter getTreeFilter(DefaultInputFile changedFile, Path baseDir) {
+    String oldPath = toGitPath(changedFile.oldPath());
+    String path = toGitPath(relativizeFilePath(baseDir, changedFile.path()));
+
+    if (changedFile.isMovedFile()) {
+      return PathFilterGroup.createFromStrings(path, oldPath);
+    }
+
+    return PathFilter.create(path);
+  }
+
   @CheckForNull
   private Ref resolveTargetRef(String targetBranchName, Repository repo) throws IOException {
     String localRef = "refs/heads/" + targetBranchName;
@@ -332,6 +506,10 @@ public class GitScmProvider extends ScmProvider {
     }
   }
 
+  private static String relativizeFilePath(Path baseDirectory, Path filePath) {
+    return baseDirectory.relativize(filePath).toString();
+  }
+
   AbstractTreeIterator prepareTreeParser(Repository repo, RevCommit commit) throws IOException {
     CanonicalTreeParser treeParser = new CanonicalTreeParser();
     try (ObjectReader objectReader = repo.newObjectReader()) {
index 73c736b224b86da3466bef422646984ca5b79889..94629f59b60856df56e70ae358da4fdb4d162818 100644 (file)
  */
 package org.sonar.scanner.scan.filesystem;
 
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.junit.Test;
 import org.sonar.api.batch.fs.InputFile;
@@ -31,6 +33,7 @@ import org.sonar.scanner.repository.FileData;
 import org.sonar.scanner.repository.ProjectRepositories;
 import org.sonar.scanner.repository.SingleProjectRepository;
 import org.sonar.scanner.scm.ScmChangedFiles;
+import org.sonar.scm.git.ChangedFile;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -61,9 +64,10 @@ public class StatusDetectionTest {
 
   @Test
   public void detect_status_branches_confirm() {
-    ScmChangedFiles changedFiles = new ScmChangedFiles(Collections.singletonList(Paths.get("module", "src", "Foo.java")));
-    StatusDetection statusDetection = new StatusDetection(projectRepositories, changedFiles);
+    Path filePath = Paths.get("module", "src", "Foo.java");
+    ScmChangedFiles changedFiles = new ScmChangedFiles(List.of(new ChangedFile(filePath.toString(), filePath)));
 
+    StatusDetection statusDetection = new StatusDetection(projectRepositories, changedFiles);
     assertThat(statusDetection.status("foo", createFile("src/Foo.java"), "XXXXX")).isEqualTo(InputFile.Status.CHANGED);
   }
 
index ec319e1c931db03549bf47dd04e08943ef315a02..abbc78a38193f33c64000daa1ce7b98e3f2b8160 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.batch.fs.internal.DefaultInputProject;
 import org.sonar.api.batch.scm.ScmProvider;
 import org.sonar.scanner.fs.InputModuleHierarchy;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
+import org.sonar.scm.git.ChangedFile;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -130,7 +131,9 @@ public class ScmChangedFilesProviderTest {
     when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singleton(Paths.get("changedFile").toAbsolutePath()));
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, project);
 
-    assertThat(scmChangedFiles.get()).containsOnly(Paths.get("changedFile").toAbsolutePath());
+    Path filePath = Paths.get("changedFile").toAbsolutePath();
+    ChangedFile changedFile = new ChangedFile(filePath.toString(), filePath);
+    assertThat(scmChangedFiles.get()).containsOnly(changedFile);
     verify(scmProvider).branchChangedFiles("target", rootBaseDir);
   }
 
index 40d3930713f74a32505030120d251f75919df239..856d057312408fcdfd440059d190952ed24a6939 100644 (file)
@@ -24,6 +24,7 @@ import java.nio.file.Paths;
 import java.util.Collection;
 import java.util.Collections;
 import org.junit.Test;
+import org.sonar.scm.git.ChangedFile;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -33,9 +34,11 @@ public class ScmChangedFilesTest {
 
   @Test
   public void testGetter() {
-    Collection<Path> files = Collections.singletonList(Paths.get("files"));
+    Path filePath = Paths.get("files");
+    ChangedFile file = new ChangedFile(filePath.toString(), filePath);
+    Collection<ChangedFile> files = Collections.singletonList(file);
     scmChangedFiles = new ScmChangedFiles(files);
-    assertThat(scmChangedFiles.get()).containsOnly(Paths.get("files"));
+    assertThat(scmChangedFiles.get()).containsOnly(file);
   }
 
   @Test
@@ -50,7 +53,8 @@ public class ScmChangedFilesTest {
 
   @Test
   public void testConfirm() {
-    Collection<Path> files = Collections.singletonList(Paths.get("files"));
+    Path filePath = Paths.get("files");
+    Collection<ChangedFile> files = Collections.singletonList(new ChangedFile(filePath.toString(), filePath));
     scmChangedFiles = new ScmChangedFiles(files);
     assertThat(scmChangedFiles.isValid()).isTrue();
     assertThat(scmChangedFiles.isChanged(Paths.get("files"))).isTrue();
index 8e2be64f50f4a705e9e4da151fa8cd2270d81d33..bcb9f6e5d527c6752c10e7183cf3a965fcc4f051 100644 (file)
@@ -132,6 +132,7 @@ message Component {
   // Path relative to project base directory
   string project_relative_path = 14;
   bool markedAsUnchanged = 15;
+  string old_relative_file_path = 16;
 
   enum ComponentType {
     UNSET = 0;