From 269c963c54bd2234bc52ec3a30ba73ebd30b44ed Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Fri, 10 Aug 2018 11:12:01 +0200 Subject: [PATCH] SONAR-11135 SONAR-11136 Load changed lines from SCM plugins and write in the scanner report --- .../org/sonar/api/batch/scm/ScmProvider.java | 29 +++- .../scanner/report/ChangedLinesPublisher.java | 101 ++++++++++++ .../scanner/report/ComponentsPublisher.java | 5 +- .../scanner/scan/ProjectScanContainer.java | 29 +++- .../scanner/scm/ScmChangedFilesProvider.java | 10 +- .../org/sonar/scanner/scm/ScmPublisher.java | 9 +- .../org/sonar/scanner/util/ScannerUtils.java | 7 + .../report/ChangedLinesPublisherTest.java | 151 ++++++++++++++++++ .../protocol/output/FileStructure.java | 3 +- .../protocol/output/ScannerReportWriter.java | 6 + .../src/main/protobuf/scanner_report.proto | 4 + .../output/ScannerReportWriterTest.java | 17 ++ 12 files changed, 343 insertions(+), 28 deletions(-) create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ChangedLinesPublisherTest.java diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java index b698b8974fe..8f7a0435d92 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java @@ -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 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> branchChangedLines(String targetBranchName, Path rootBaseDir, Set 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 index 00000000000..ef6dcc60278 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ChangedLinesPublisher.java @@ -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 allPublishedFiles = StreamSupport.stream(inputComponentStore.allFilesToPublish().spliterator(), false) + .collect(Collectors.toMap(DefaultInputFile::path, f -> f)); + Map> pathSetMap = provider.branchChangedLines(branchConfiguration.branchTarget(), rootBaseDir, allPublishedFiles.keySet()); + int count = 0; + + if (pathSetMap == null) { + return count; + } + + for (Map.Entry e : allPublishedFiles.entrySet()) { + Set 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 changedLines) { + ScannerReport.ChangedLines.Builder builder = ScannerReport.ChangedLines.newBuilder(); + builder.addAllLine(changedLines); + writer.writeComponentChangedLines(fileRef, builder.build()); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java index c36b8a196e6..8ec6f7af82a 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java @@ -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 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()) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java index 0bb31674dd4..b2c105c2722 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java @@ -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); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java index a04b4f5021a..af13e28575c 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java @@ -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 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"; - } - } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java index bb587269fd9..cd852fce2ef 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java @@ -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 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; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/util/ScannerUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/util/ScannerUtils.java index 3358904661a..5fa46f186ea 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/util/ScannerUtils.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/util/ScannerUtils.java @@ -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 index 00000000000..78413d95a07 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ChangedLinesPublisherTest.java @@ -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 paths = new HashSet<>(Arrays.asList(BASE_DIR.resolve("path1"), BASE_DIR.resolve("path2"))); + Set 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 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(); + } + +} diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java index 44ae856a6c2..bab729b95b0 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java @@ -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; diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java index 7df46f355cc..1ccb6876e88 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java @@ -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))) { diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index 8a1e78dde6a..ac4c89980b8 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -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; diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java index 6d0223be28d..ffcd97aaa8f 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java @@ -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(); -- 2.39.5