]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11135 SONAR-11136 Load changed lines from SCM plugins and write in the scanner...
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Fri, 10 Aug 2018 09:12:01 +0000 (11:12 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Sep 2018 08:51:38 +0000 (10:51 +0200)
12 files changed:
sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/util/ScannerUtils.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ChangedLinesPublisherTest.java [new file with mode: 0644]
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java

index b698b8974fe9216ade3ff7e4c50e6564bc5e12c0..8f7a0435d9290a3ff7a9791cb47eaf4f95450866 100644 (file)
@@ -21,15 +21,15 @@ package org.sonar.api.batch.scm;
 
 import java.io.File;
 import java.nio.file.Path;
+import java.util.Map;
 import java.util.Set;
-import javax.annotation.Nullable;
+import javax.annotation.CheckForNull;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.ExtensionPoint;
 import org.sonar.api.batch.InstantiationStrategy;
 import org.sonar.api.batch.ScannerSide;
 
 /**
- * See {@link LINKS_SOURCES_DEV#LINKS_SOURCES_DEV} to get old Maven URL format.
  * @since 5.0
  */
 @ScannerSide
@@ -46,6 +46,7 @@ public abstract class ScmProvider {
   /**
    * Whether this provider is able to manage files located in this directory.
    * Used by autodetection. Not considered if user has forced the provider key.
+   *
    * @return false by default
    */
   public boolean supports(File baseDir) {
@@ -58,16 +59,32 @@ public abstract class ScmProvider {
 
   /**
    * Return absolute path of the files changed in the current branch, compared to the provided target branch.
-   * @return null if SCM provider was not able to compute the list of files.
+   *
+   * @return null if the SCM provider was not able to compute the list of files.
+   * @since 7.0
    */
-  @Nullable
+  @CheckForNull
   public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) {
     return null;
   }
 
   /**
-  * The relative path from SCM root
-  */
+   * Return a map between paths given as argument and the corresponding line numbers which are new compared to the provided target branch.
+   * If null is returned or if a path is not included in the map, an imprecise fallback mechanism will be used to detect which lines
+   * are new (based on SCM dates).
+   *
+   * @param files Absolute path of files of interest
+   * @return null if the SCM provider was not able to compute the new lines
+   * @since 7.4
+   */
+  @CheckForNull
+  public Map<Path, Set<Integer>> branchChangedLines(String targetBranchName, Path rootBaseDir, Set<Path> files) {
+    return null;
+  }
+
+  /**
+   * The relative path from SCM root
+   */
   public Path relativePathFromScmRoot(Path path) {
     throw new UnsupportedOperationException(formatUnsupportedMessage("Getting relative path from SCM root"));
   }
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java
new file mode 100644 (file)
index 0000000..ef6dcc6
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.report;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.InputModuleHierarchy;
+import org.sonar.api.batch.scm.ScmProvider;
+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.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.scan.branch.BranchConfiguration;
+import org.sonar.scanner.scan.filesystem.InputComponentStore;
+import org.sonar.scanner.scm.ScmConfiguration;
+import org.sonar.scanner.util.ScannerUtils;
+
+public class ChangedLinesPublisher implements ReportPublisherStep {
+  private static final Logger LOG = Loggers.get(ChangedLinesPublisher.class);
+  private static final String LOG_MSG = "SCM writing changed lines";
+
+  private final ScmConfiguration scmConfiguration;
+  private final InputModuleHierarchy inputModuleHierarchy;
+  private final InputComponentStore inputComponentStore;
+  private final BranchConfiguration branchConfiguration;
+
+  public ChangedLinesPublisher(ScmConfiguration scmConfiguration, InputModuleHierarchy inputModuleHierarchy, InputComponentStore inputComponentStore,
+    BranchConfiguration branchConfiguration) {
+    this.scmConfiguration = scmConfiguration;
+    this.inputModuleHierarchy = inputModuleHierarchy;
+    this.inputComponentStore = inputComponentStore;
+    this.branchConfiguration = branchConfiguration;
+  }
+
+  @Override public void publish(ScannerReportWriter writer) {
+    if (scmConfiguration.isDisabled() || !branchConfiguration.isShortOrPullRequest() || branchConfiguration.branchTarget() == null) {
+      return;
+    }
+
+    ScmProvider provider = scmConfiguration.provider();
+    if (provider == null) {
+      return;
+    }
+
+    Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+    int count = writeChangedLines(provider, writer);
+    LOG.debug("SCM reported changed lines for {} {} in the branch", count, ScannerUtils.pluralize("file", count));
+    profiler.stopInfo();
+  }
+
+  private int writeChangedLines(ScmProvider provider, ScannerReportWriter writer) {
+    Path rootBaseDir = inputModuleHierarchy.root().getBaseDir();
+    Map<Path, DefaultInputFile> allPublishedFiles = StreamSupport.stream(inputComponentStore.allFilesToPublish().spliterator(), false)
+      .collect(Collectors.toMap(DefaultInputFile::path, f -> f));
+    Map<Path, Set<Integer>> pathSetMap = provider.branchChangedLines(branchConfiguration.branchTarget(), rootBaseDir, allPublishedFiles.keySet());
+    int count = 0;
+
+    if (pathSetMap == null) {
+      return count;
+    }
+
+    for (Map.Entry<Path, DefaultInputFile> e : allPublishedFiles.entrySet()) {
+      Set<Integer> changedLines = pathSetMap.get(e.getKey());
+      // if the file got no information returned by the SCM, we write nothing in the report and
+      // the compute engine will use SCM dates to estimate which lines are new
+      if (changedLines != null) {
+        count++;
+        writeChangedLines(writer, e.getValue().batchId(), changedLines);
+      }
+    }
+    return count;
+  }
+
+  private static void writeChangedLines(ScannerReportWriter writer, int fileRef, Set<Integer> changedLines) {
+    ScannerReport.ChangedLines.Builder builder = ScannerReport.ChangedLines.newBuilder();
+    builder.addAllLine(changedLines);
+    writer.writeComponentChangedLines(fileRef, builder.build());
+  }
+}
index c36b8a196e67565ad418d2ed4b4400ebb3f22a44..8ec6f7af82aa8bd4622b668bc86abeccf5f4a0ed 100644 (file)
@@ -69,7 +69,7 @@ public class ComponentsPublisher implements ReportPublisherStep {
   public void publish(ScannerReportWriter writer) {
     this.reader = new ScannerReportReader(writer.getFileStructure().root());
     this.writer = writer;
-    recursiveWriteComponent((DefaultInputComponent) moduleHierarchy.root());
+    recursiveWriteComponent(moduleHierarchy.root());
   }
 
   /**
@@ -152,8 +152,7 @@ public class ComponentsPublisher implements ReportPublisherStep {
   }
 
   private boolean shouldSkipComponent(DefaultInputComponent component, Collection<InputComponent> children) {
-    if (component instanceof InputModule && children.isEmpty()
-      && (branchConfiguration.isShortOrPullRequest())) {
+    if (component instanceof InputModule && children.isEmpty() && (branchConfiguration.isShortOrPullRequest())) {
       // no children on a module in short branch analysis -> skip it (except root)
       return !moduleHierarchy.isRoot((InputModule) component);
     } else if (component instanceof InputDir && children.isEmpty()) {
index 0bb31674dd4f7b9d858e2c13d0c4bf726667d2f4..b2c105c2722b1151c5cfded32368db469b54fe00 100644 (file)
@@ -57,6 +57,7 @@ import org.sonar.scanner.issue.tracking.ServerLineHashesLoader;
 import org.sonar.scanner.mediumtest.ScanTaskObservers;
 import org.sonar.scanner.report.ActiveRulesPublisher;
 import org.sonar.scanner.report.AnalysisContextReportPublisher;
+import org.sonar.scanner.report.ChangedLinesPublisher;
 import org.sonar.scanner.report.ComponentsPublisher;
 import org.sonar.scanner.report.ContextPropertiesPublisher;
 import org.sonar.scanner.report.CoveragePublisher;
@@ -118,9 +119,13 @@ public class ProjectScanContainer extends ComponentContainer {
     ProjectLock lock = getComponentByType(ProjectLock.class);
     lock.tryLock();
     getComponentByType(WorkDirectoriesInitializer.class).execute();
-    if (isTherePreviousAnalysis()) {
+
+    if (!isIssuesMode()) {
+      addReportPublishSteps();
+    } else if (isTherePreviousAnalysis()) {
       addIssueTrackingComponents();
     }
+
   }
 
   private void addBatchComponents() {
@@ -196,11 +201,6 @@ public class ProjectScanContainer extends ComponentContainer {
       AnalysisContextReportPublisher.class,
       MetadataPublisher.class,
       ActiveRulesPublisher.class,
-      ComponentsPublisher.class,
-      MeasuresPublisher.class,
-      CoveragePublisher.class,
-      SourcePublisher.class,
-      TestExecutionAndCoveragePublisher.class,
 
       // Cpd
       CpdExecutor.class,
@@ -216,6 +216,17 @@ public class ProjectScanContainer extends ComponentContainer {
     addIfMissing(DefaultProjectRepositoriesLoader.class, ProjectRepositoriesLoader.class);
   }
 
+  private void addReportPublishSteps() {
+    add(
+      ComponentsPublisher.class,
+      MeasuresPublisher.class,
+      CoveragePublisher.class,
+      SourcePublisher.class,
+      ChangedLinesPublisher.class,
+      TestExecutionAndCoveragePublisher.class
+    );
+  }
+
   private void addIssueTrackingComponents() {
     add(
       LocalIssueTracking.class,
@@ -228,6 +239,10 @@ public class ProjectScanContainer extends ComponentContainer {
     return getComponentByType(ProjectRepositories.class).lastAnalysisDate() != null;
   }
 
+  private boolean isIssuesMode() {
+    return getComponentByType(GlobalAnalysisMode.class).isIssues();
+  }
+
   private void addBatchExtensions() {
     getComponentByType(CoreExtensionsInstaller.class)
       .install(this, noExtensionFilter(), extension -> isInstantiationStrategy(extension, PER_BATCH));
@@ -250,7 +265,7 @@ public class ProjectScanContainer extends ComponentContainer {
     LOG.info("Project key: {}", tree.root().key());
     LOG.info("Project base dir: {}", tree.root().getBaseDir());
     properties.organizationKey().ifPresent(k -> LOG.info("Organization key: {}", k));
-    
+
     String branch = tree.root().definition().getBranch();
     if (branch != null) {
       LOG.info("Branch key: {}", branch);
index a04b4f5021a9a4205bf781459eb62ec3bdc85e04..af13e28575cedd5601ac9b78ac31595201a5a9b1 100644 (file)
@@ -30,6 +30,7 @@ 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.scanner.util.ScannerUtils;
 
 public class ScmChangedFilesProvider extends ProviderAdapter {
   private static final Logger LOG = Loggers.get(ScmChangedFilesProvider.class);
@@ -69,7 +70,7 @@ public class ScmChangedFilesProvider extends ProviderAdapter {
         Collection<Path> changedFiles = scmProvider.branchChangedFiles(branchConfiguration.branchTarget(), rootBaseDir);
         profiler.stopInfo();
         if (changedFiles != null) {
-          LOG.debug("SCM reported {} {} changed in the branch", changedFiles.size(), pluralize("file", changedFiles.size()));
+          LOG.debug("SCM reported {} {} changed in the branch", changedFiles.size(), ScannerUtils.pluralize("file", changedFiles.size()));
           return changedFiles;
         }
       }
@@ -79,11 +80,4 @@ public class ScmChangedFilesProvider extends ProviderAdapter {
     return null;
   }
 
-  private static String pluralize(String str, int i) {
-    if (i == 1) {
-      return str;
-    }
-    return str + "s";
-  }
-
 }
index bb587269fd9bd6b71bb2efe70c8bf537717e4705..cd852fce2ef47f58199064746ce0191e7af0e330 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.fs.InputFile.Status;
 import org.sonar.api.batch.fs.internal.DefaultInputFile;
 import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.batch.scm.ScmProvider;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.scanner.protocol.output.ScannerReport;
@@ -71,18 +72,20 @@ public final class ScmPublisher {
       LOG.info("SCM Publisher is disabled");
       return;
     }
-    if (configuration.provider() == null) {
+
+    ScmProvider provider = configuration.provider();
+    if (provider == null) {
       LOG.info("No SCM system was detected. You can use the '" + CoreProperties.SCM_PROVIDER_KEY + "' property to explicitly specify it.");
       return;
     }
 
     List<InputFile> filesToBlame = collectFilesToBlame(writer);
     if (!filesToBlame.isEmpty()) {
-      String key = configuration.provider().key();
+      String key = provider.key();
       LOG.info("SCM provider for this project is: " + key);
       DefaultBlameOutput output = new DefaultBlameOutput(writer, filesToBlame);
       try {
-        configuration.provider().blameCommand().blame(new DefaultBlameInput(fs, filesToBlame), output);
+        provider.blameCommand().blame(new DefaultBlameInput(fs, filesToBlame), output);
       } catch (Exception e) {
         output.finish(false);
         throw e;
index 3358904661a8a01f73e65724fce79c3751a0e17a..5fa46f186ea9e87594b81ec67e7e61bbbb14afb1 100644 (file)
@@ -63,4 +63,11 @@ public class ScannerUtils {
     return o.getClass().getName();
   }
 
+  public static String pluralize(String str, int i) {
+    if (i == 1) {
+      return str;
+    }
+    return str + "s";
+  }
+
 }
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ChangedLinesPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ChangedLinesPublisherTest.java
new file mode 100644 (file)
index 0000000..78413d9
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.report;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.batch.fs.internal.InputModuleHierarchy;
+import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.scm.ScmProvider;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.scan.branch.BranchConfiguration;
+import org.sonar.scanner.scan.filesystem.InputComponentStore;
+import org.sonar.scanner.scm.ScmConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class ChangedLinesPublisherTest {
+  private static final String TARGET_BRANCH = "target";
+  private static final Path BASE_DIR = Paths.get("/root");
+
+  private ScmConfiguration scmConfiguration = mock(ScmConfiguration.class);
+  private InputModuleHierarchy inputModuleHierarchy = mock(InputModuleHierarchy.class);
+  private InputComponentStore inputComponentStore = mock(InputComponentStore.class);
+  private BranchConfiguration branchConfiguration = mock(BranchConfiguration.class);
+  private ScannerReportWriter writer;
+  private ScmProvider provider = mock(ScmProvider.class);
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  private ChangedLinesPublisher publisher = new ChangedLinesPublisher(scmConfiguration, inputModuleHierarchy, inputComponentStore, branchConfiguration);
+
+  @Before
+  public void setUp() {
+    writer = new ScannerReportWriter(temp.getRoot());
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
+    when(scmConfiguration.isDisabled()).thenReturn(false);
+    when(scmConfiguration.provider()).thenReturn(provider);
+    when(branchConfiguration.branchTarget()).thenReturn(TARGET_BRANCH);
+    DefaultInputModule root = mock(DefaultInputModule.class);
+    when(root.getBaseDir()).thenReturn(BASE_DIR);
+    when(inputModuleHierarchy.root()).thenReturn(root);
+  }
+
+  @Test
+  public void skip_if_scm_is_disabled() {
+    when(scmConfiguration.isDisabled()).thenReturn(true);
+    publisher.publish(writer);
+    verifyZeroInteractions(inputComponentStore, inputModuleHierarchy, provider);
+    assertNotPublished();
+  }
+
+  @Test
+  public void skip_if_not_pr_or_slb() {
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(false);
+    publisher.publish(writer);
+    verifyZeroInteractions(inputComponentStore, inputModuleHierarchy, provider);
+    assertNotPublished();
+  }
+
+  @Test
+  public void skip_if_target_branch_is_null() {
+    when(branchConfiguration.branchTarget()).thenReturn(null);
+    publisher.publish(writer);
+    verifyZeroInteractions(inputComponentStore, inputModuleHierarchy, provider);
+    assertNotPublished();
+  }
+
+  @Test
+  public void skip_if_no_scm_provider() {
+    when(scmConfiguration.provider()).thenReturn(null);
+    publisher.publish(writer);
+    verifyZeroInteractions(inputComponentStore, inputModuleHierarchy, provider);
+    assertNotPublished();
+  }
+
+  @Test
+  public void skip_if_scm_provider_returns_null() {
+    publisher.publish(writer);
+    assertNotPublished();
+  }
+
+  @Test
+  public void write_changed_files() {
+    DefaultInputFile fileWithChangedLines = createInputFile("path1");
+    DefaultInputFile fileWithoutChangedLines = createInputFile("path2");
+    Set<Path> paths = new HashSet<>(Arrays.asList(BASE_DIR.resolve("path1"), BASE_DIR.resolve("path2")));
+    Set<Integer> lines = new HashSet<>(Arrays.asList(1, 10));
+    when(provider.branchChangedLines(TARGET_BRANCH, BASE_DIR, paths)).thenReturn(Collections.singletonMap(BASE_DIR.resolve("path1"), lines));
+    when(inputComponentStore.allFilesToPublish()).thenReturn(Arrays.asList(fileWithChangedLines, fileWithoutChangedLines));
+
+    publisher.publish(writer);
+
+    assertPublished(fileWithChangedLines, lines);
+    assertNotPublished(fileWithoutChangedLines);
+  }
+
+  private DefaultInputFile createInputFile(String path) {
+    return new TestInputFileBuilder("module", path)
+      .setProjectBaseDir(BASE_DIR)
+      .setModuleBaseDir(BASE_DIR)
+      .build();
+  }
+
+  private void assertPublished(DefaultInputFile file, Set<Integer> lines) {
+    assertThat(new File(temp.getRoot(), "changed-lines-" + file.batchId() + ".pb")).exists();
+    ScannerReportReader reader = new ScannerReportReader(temp.getRoot());
+    assertThat(reader.readComponentChangedLines(file.batchId()).getLineList()).containsExactlyElementsOf(lines);
+  }
+
+  private void assertNotPublished(DefaultInputFile file) {
+    assertThat(new File(temp.getRoot(), "changed-lines-" + file.batchId() + ".pb")).doesNotExist();
+  }
+
+  private void assertNotPublished() {
+    assertThat(temp.getRoot().list()).isEmpty();
+  }
+
+}
index 44ae856a6c2c8b79a9f877315c4ea80e519c210f..bab729b95b0a4831dc52107b11e6c30013f0fb5d 100644 (file)
@@ -43,7 +43,8 @@ public class FileStructure {
     TESTS("tests-", Domain.PB),
     COVERAGE_DETAILS("coverage-details-", Domain.PB),
     SOURCE("source-", ".txt"),
-    SGNIFICANT_CODE("sgnificant-code-", Domain.PB);
+    SGNIFICANT_CODE("sgnificant-code-", Domain.PB),
+    CHANGED_LINES("changed-lines-", Domain.PB);
 
     private static final String PB = ".pb";
     private final String filePrefix;
index 7df46f355cc1fea5888c9350cd136820f29eb3c9..1ccb6876e887875d36c37edc201217434cadecd9 100644 (file)
@@ -94,6 +94,12 @@ public class ScannerReportWriter {
     return file;
   }
 
+  public File writeComponentChangedLines(int componentRef, ScannerReport.ChangedLines changedLines) {
+    File file = fileStructure.fileFor(FileStructure.Domain.CHANGED_LINES, componentRef);
+    Protobuf.write(changedLines, file);
+    return file;
+  }
+
   public void appendComponentExternalIssue(int componentRef, ScannerReport.ExternalIssue issue) {
     File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
     try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true))) {
index 8a1e78dde6ac2b9b3e364385b14899bd4bf7814d..ac4c89980b8d5120a16c59c214df89e0a0dd4f85 100644 (file)
@@ -271,6 +271,10 @@ message LineSgnificantCode {
   int32 end_offset = 3;
 }
 
+message ChangedLines {
+  repeated int32 line = 1;
+}
+
 message Symbol {
   TextRange declaration = 1;
   repeated TextRange reference = 2;
index 6d0223be28d5edf14488465f8cf3e442f25a4950..ffcd97aaa8f88e026055cdad4662a7d6b1412885 100644 (file)
@@ -136,6 +136,23 @@ public class ScannerReportWriterTest {
     }
   }
 
+  @Test
+  public void write_changed_lines() {
+    assertThat(underTest.hasComponentData(FileStructure.Domain.CHANGED_LINES, 1)).isFalse();
+
+    ScannerReport.ChangedLines changedLines = ScannerReport.ChangedLines.newBuilder()
+      .addLine(1)
+      .addLine(3)
+      .build();
+    underTest.writeComponentChangedLines(1, changedLines);
+
+    assertThat(underTest.hasComponentData(FileStructure.Domain.CHANGED_LINES, 1)).isTrue();
+    File file = underTest.getFileStructure().fileFor(FileStructure.Domain.CHANGED_LINES, 1);
+    assertThat(file).exists().isFile();
+    ScannerReport.ChangedLines loadedChangedLines = Protobuf.read(file, ScannerReport.ChangedLines.parser());
+    assertThat(loadedChangedLines.getLineList()).containsExactly(1, 3);
+  }
+
   @Test
   public void write_measures() {
     assertThat(underTest.hasComponentData(FileStructure.Domain.MEASURES, 1)).isFalse();