]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14257 Pull requests should inherit issue state from its target branch
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Tue, 12 Jan 2021 21:22:28 +0000 (15:22 -0600)
committersonartech <sonartech@sonarsource.com>
Thu, 21 Jan 2021 20:30:29 +0000 (20:30 +0000)
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestTrackerExecution.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ReferenceBranchTrackerExecution.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuids.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactory.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/PullRequestTrackerExecutionTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuidsTest.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactoryTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java
sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java

index 52637abdf53c40bac103575dae09cc01c3f64c85..89f83b97594ea7dbb1b2f79f1b6030b2bd594a75 100644 (file)
@@ -81,11 +81,13 @@ import org.sonar.ce.task.projectanalysis.issue.ScmAccountToUserLoader;
 import org.sonar.ce.task.projectanalysis.issue.SiblingsIssueMerger;
 import org.sonar.ce.task.projectanalysis.issue.SiblingsIssuesLoader;
 import org.sonar.ce.task.projectanalysis.issue.SourceBranchComponentUuids;
+import org.sonar.ce.task.projectanalysis.issue.TargetBranchComponentUuids;
 import org.sonar.ce.task.projectanalysis.issue.TrackerBaseInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerExecution;
 import org.sonar.ce.task.projectanalysis.issue.TrackerRawInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerReferenceBranchInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerSourceBranchInputFactory;
+import org.sonar.ce.task.projectanalysis.issue.TrackerTargetBranchInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
 import org.sonar.ce.task.projectanalysis.issue.commonrule.BranchCoverageRule;
 import org.sonar.ce.task.projectanalysis.issue.commonrule.CommentDensityRule;
@@ -273,9 +275,10 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       NewSecurityReviewMeasuresVisitor.class,
       LastCommitVisitor.class,
       MeasureComputersVisitor.class,
-
+      TargetBranchComponentUuids.class,
       UpdateConflictResolver.class,
       TrackerBaseInputFactory.class,
+      TrackerTargetBranchInputFactory.class,
       TrackerRawInputFactory.class,
       TrackerReferenceBranchInputFactory.class,
       TrackerSourceBranchInputFactory.class,
index eb9dbfe9e3b5099ce9e12fcca5dd0e09dcbf414d..5d51346180e1bd445cc5a5efbbfd9aa4897ceda4 100644 (file)
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.sonar.api.issue.Issue;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.source.NewLinesRepository;
 import org.sonar.core.issue.DefaultIssue;
@@ -36,23 +37,40 @@ public class PullRequestTrackerExecution {
   private final TrackerBaseInputFactory baseInputFactory;
   private final Tracker<DefaultIssue, DefaultIssue> tracker;
   private final NewLinesRepository newLinesRepository;
+  private final TrackerTargetBranchInputFactory targetInputFactory;
 
-  public PullRequestTrackerExecution(TrackerBaseInputFactory baseInputFactory, Tracker<DefaultIssue, DefaultIssue> tracker,
-    NewLinesRepository newLinesRepository) {
+  public PullRequestTrackerExecution(TrackerBaseInputFactory baseInputFactory, TrackerTargetBranchInputFactory targetInputFactory,
+    Tracker<DefaultIssue, DefaultIssue> tracker, NewLinesRepository newLinesRepository) {
     this.baseInputFactory = baseInputFactory;
+    this.targetInputFactory = targetInputFactory;
     this.tracker = tracker;
     this.newLinesRepository = newLinesRepository;
   }
 
   public Tracking<DefaultIssue, DefaultIssue> track(Component component, Input<DefaultIssue> rawInput) {
-    Input<DefaultIssue> previousAnalysisInput = baseInputFactory.create(component);
-
     // Step 1: only keep issues on changed lines
     List<DefaultIssue> filteredRaws = keepIssuesHavingAtLeastOneLocationOnChangedLines(component, rawInput.getIssues());
-    Input<DefaultIssue> unmatchedRawsAfterChangedLineFiltering = new DefaultTrackingInput(filteredRaws, rawInput.getLineHashSequence(), rawInput.getBlockHashSequence());
+    Input<DefaultIssue> unmatchedRawsAfterChangedLineFiltering = createInput(rawInput, filteredRaws);
+
+    // Step 2: remove issues that are resolved in the target branch
+    Input<DefaultIssue> unmatchedRawsAfterTargetResolvedTracking;
+    if (targetInputFactory.hasTargetBranchAnalysis()) {
+      Input<DefaultIssue> targetInput = targetInputFactory.createForTargetBranch(component);
+      List<DefaultIssue> resolvedTargetIssues = targetInput.getIssues().stream().filter(i -> Issue.STATUS_RESOLVED.equals(i.status())).collect(Collectors.toList());
+      Input<DefaultIssue> resolvedTargetInput = createInput(targetInput, resolvedTargetIssues);
+      Tracking<DefaultIssue, DefaultIssue> prResolvedTracking = tracker.trackNonClosed(unmatchedRawsAfterChangedLineFiltering, resolvedTargetInput);
+      unmatchedRawsAfterTargetResolvedTracking = createInput(rawInput, prResolvedTracking.getUnmatchedRaws().collect(Collectors.toList()));
+    } else {
+      unmatchedRawsAfterTargetResolvedTracking = unmatchedRawsAfterChangedLineFiltering;
+    }
+
+    // Step 3: track issues with previous analysis of the current PR
+    Input<DefaultIssue> previousAnalysisInput = baseInputFactory.create(component);
+    return tracker.trackNonClosed(unmatchedRawsAfterTargetResolvedTracking, previousAnalysisInput);
+  }
 
-    // Step 2: track issues with previous analysis of the current PR
-    return tracker.trackNonClosed(unmatchedRawsAfterChangedLineFiltering, previousAnalysisInput);
+  private static Input<DefaultIssue> createInput(Input<DefaultIssue> input, Collection<DefaultIssue> issues) {
+    return new DefaultTrackingInput(issues, input.getLineHashSequence(), input.getBlockHashSequence());
   }
 
   private List<DefaultIssue> keepIssuesHavingAtLeastOneLocationOnChangedLines(Component component, Collection<DefaultIssue> issues) {
index 080f3ed704c334114a0365a6ed8f536cf5436b8e..ab7735f1ff2553162766189739e23f70fbc49bb8 100644 (file)
@@ -29,8 +29,7 @@ public class ReferenceBranchTrackerExecution {
   private final TrackerReferenceBranchInputFactory referenceBranchInputFactory;
   private final Tracker<DefaultIssue, DefaultIssue> tracker;
 
-  public ReferenceBranchTrackerExecution(TrackerReferenceBranchInputFactory referenceBranchInputFactory,
-    Tracker<DefaultIssue, DefaultIssue> tracker) {
+  public ReferenceBranchTrackerExecution(TrackerReferenceBranchInputFactory referenceBranchInputFactory, Tracker<DefaultIssue, DefaultIssue> tracker) {
     this.referenceBranchInputFactory = referenceBranchInputFactory;
     this.tracker = tracker;
   }
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuids.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuids.java
new file mode 100644 (file)
index 0000000..8886670
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.issue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+
+import static org.sonar.db.component.ComponentDto.removeBranchAndPullRequestFromKey;
+
+/**
+ * Cache a map between component keys and uuids in the merge branch and optionally the target branch (for PR and SLB, and only if this target branch is analyzed)
+ */
+public class TargetBranchComponentUuids {
+  private final AnalysisMetadataHolder analysisMetadataHolder;
+  private final DbClient dbClient;
+  private Map<String, String> targetBranchComponentsUuidsByKey;
+  private boolean hasTargetBranchAnalysis;
+  @CheckForNull
+  private String targetBranchUuid;
+
+  public TargetBranchComponentUuids(AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient) {
+    this.analysisMetadataHolder = analysisMetadataHolder;
+    this.dbClient = dbClient;
+  }
+
+  private void lazyInit() {
+    if (targetBranchComponentsUuidsByKey == null) {
+      targetBranchComponentsUuidsByKey = new HashMap<>();
+
+      if (analysisMetadataHolder.isPullRequest()) {
+        try (DbSession dbSession = dbClient.openSession(false)) {
+          initForTargetBranch(dbSession);
+        }
+      } else {
+        hasTargetBranchAnalysis = false;
+      }
+    }
+  }
+
+  private void initForTargetBranch(DbSession dbSession) {
+    Optional<BranchDto> branchDtoOpt = dbClient.branchDao().selectByBranchKey(dbSession, analysisMetadataHolder.getProject().getUuid(),
+      analysisMetadataHolder.getBranch().getTargetBranchName());
+    targetBranchUuid = branchDtoOpt.map(BranchDto::getUuid).orElse(null);
+    hasTargetBranchAnalysis = targetBranchUuid != null && dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, targetBranchUuid).isPresent();
+    if (hasTargetBranchAnalysis) {
+      List<ComponentDto> targetComponents = dbClient.componentDao().selectByProjectUuid(targetBranchUuid, dbSession);
+      for (ComponentDto dto : targetComponents) {
+        targetBranchComponentsUuidsByKey.put(dto.getKey(), dto.uuid());
+      }
+    }
+  }
+
+  public boolean hasTargetBranchAnalysis() {
+    lazyInit();
+    return hasTargetBranchAnalysis;
+  }
+
+  @CheckForNull
+  public String getTargetBranchComponentUuid(String dbKey) {
+    lazyInit();
+    String cleanComponentKey = removeBranchAndPullRequestFromKey(dbKey);
+    return targetBranchComponentsUuidsByKey.get(cleanComponentKey);
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactory.java
new file mode 100644 (file)
index 0000000..d5197a3
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.issue;
+
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.Input;
+import org.sonar.core.issue.tracking.LazyInput;
+import org.sonar.core.issue.tracking.LineHashSequence;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+
+public class TrackerTargetBranchInputFactory {
+  private static final LineHashSequence EMPTY_LINE_HASH_SEQUENCE = new LineHashSequence(Collections.emptyList());
+
+  private final ComponentIssuesLoader componentIssuesLoader;
+  private final DbClient dbClient;
+  private final TargetBranchComponentUuids targetBranchComponentUuids;
+
+  public TrackerTargetBranchInputFactory(ComponentIssuesLoader componentIssuesLoader, TargetBranchComponentUuids targetBranchComponentUuids,
+    DbClient dbClient) {
+    this.componentIssuesLoader = componentIssuesLoader;
+    this.targetBranchComponentUuids = targetBranchComponentUuids;
+    this.dbClient = dbClient;
+    // TODO detect file moves?
+  }
+
+  public boolean hasTargetBranchAnalysis() {
+    return targetBranchComponentUuids.hasTargetBranchAnalysis();
+  }
+
+  public Input<DefaultIssue> createForTargetBranch(Component component) {
+    String targetBranchComponentUuid = targetBranchComponentUuids.getTargetBranchComponentUuid(component.getDbKey());
+    return new TargetLazyInput(component.getType(), targetBranchComponentUuid);
+  }
+
+  private class TargetLazyInput extends LazyInput<DefaultIssue> {
+    private final Component.Type type;
+    private final String targetBranchComponentUuid;
+
+    private TargetLazyInput(Component.Type type, @Nullable String targetBranchComponentUuid) {
+      this.type = type;
+      this.targetBranchComponentUuid = targetBranchComponentUuid;
+    }
+
+    @Override
+    protected LineHashSequence loadLineHashSequence() {
+      if (targetBranchComponentUuid == null || type != Component.Type.FILE) {
+        return EMPTY_LINE_HASH_SEQUENCE;
+      }
+
+      try (DbSession session = dbClient.openSession(false)) {
+        List<String> hashes = dbClient.fileSourceDao().selectLineHashes(session, targetBranchComponentUuid);
+        if (hashes == null || hashes.isEmpty()) {
+          return EMPTY_LINE_HASH_SEQUENCE;
+        }
+        return new LineHashSequence(hashes);
+      }
+    }
+
+    @Override
+    protected List<DefaultIssue> loadIssues() {
+      if (targetBranchComponentUuid == null) {
+        return Collections.emptyList();
+      }
+      return componentIssuesLoader.loadOpenIssuesWithChanges(targetBranchComponentUuid);
+    }
+  }
+
+}
index 618e7ab7a0f4a44954f923717b1f0340502ee2e2..b68348ecc4195c965594b2dde0ab2af2ec09d769 100644 (file)
@@ -127,7 +127,7 @@ public class IntegrateIssuesVisitorTest {
   private final ReferenceBranchComponentUuids referenceBranchComponentUuids = mock(ReferenceBranchComponentUuids.class);
   private final SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class);
   private final NewLinesRepository newLinesRepository = mock(NewLinesRepository.class);
-
+  private TargetBranchComponentUuids targetBranchComponentUuids = mock(TargetBranchComponentUuids.class);
   private ArgumentCaptor<DefaultIssue> defaultIssueCaptor;
 
   private final ComponentIssuesLoader issuesLoader = new ComponentIssuesLoader(dbTester.getDbClient(), ruleRepositoryRule, activeRulesHolderRule, new MapSettings().asConfig(),
@@ -153,11 +153,12 @@ public class IntegrateIssuesVisitorTest {
       issueFilter, ruleRepositoryRule, activeRulesHolder);
     TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbClient, movedFilesRepository, mock(ReportModulesPath.class), analysisMetadataHolder,
       new IssueFieldsSetter(), mock(ComponentsWithUnprocessedIssues.class));
+    TrackerTargetBranchInputFactory targetInputFactory = new TrackerTargetBranchInputFactory(issuesLoader, targetBranchComponentUuids, dbClient);
     TrackerReferenceBranchInputFactory mergeInputFactory = new TrackerReferenceBranchInputFactory(issuesLoader, mergeBranchComponentsUuids, dbClient);
     ClosedIssuesInputFactory closedIssuesInputFactory = new ClosedIssuesInputFactory(issuesLoader, dbClient, movedFilesRepository);
     tracker = new TrackerExecution(baseInputFactory, closedIssuesInputFactory, new Tracker<>(), issuesLoader, analysisMetadataHolder);
-    prBranchTracker = new PullRequestTrackerExecution(baseInputFactory, new Tracker<>(), newLinesRepository);
     mergeBranchTracker = new ReferenceBranchTrackerExecution(mergeInputFactory, new Tracker<>());
+    prBranchTracker = new PullRequestTrackerExecution(baseInputFactory, targetInputFactory, new Tracker<>(), newLinesRepository);
     trackingDelegator = new IssueTrackingDelegator(prBranchTracker, mergeBranchTracker, tracker, analysisMetadataHolder);
     treeRootHolder.setRoot(PROJECT);
     protoIssueCache = new ProtoIssueCache(temp.newFile(), System2.INSTANCE);
index 0f0f13015b8463489388f815314dc8ae9c2eadc9..07591fe172bed609c04abbb6406481fd34d165cc 100644 (file)
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.source.NewLinesRepository;
@@ -60,26 +61,26 @@ public class PullRequestTrackerExecutionTest {
   private final NewLinesRepository newLinesRepository = mock(NewLinesRepository.class);
   private final List<DefaultIssue> rawIssues = new ArrayList<>();
   private final List<DefaultIssue> baseIssues = new ArrayList<>();
+  private final List<DefaultIssue> targetIssues = new ArrayList<>();
 
   private PullRequestTrackerExecution underTest;
 
+  private TrackerTargetBranchInputFactory targetFactory = mock(TrackerTargetBranchInputFactory.class);
+
   @Before
   public void setUp() {
     when(baseFactory.create(FILE)).thenReturn(createInput(baseIssues));
+    when(targetFactory.createForTargetBranch(FILE)).thenReturn(createInput(targetIssues));
 
     Tracker<DefaultIssue, DefaultIssue> tracker = new Tracker<>();
-    underTest = new PullRequestTrackerExecution(baseFactory, tracker, newLinesRepository);
+    underTest = new PullRequestTrackerExecution(baseFactory, targetFactory, tracker, newLinesRepository);
   }
 
   @Test
   public void simple_tracking_keep_only_issues_having_location_on_changed_lines() {
-    final DefaultIssue issue1 = createIssue(2, RuleTesting.XOO_X1);
-    issue1.setLocations(DbIssues.Locations.newBuilder()
-      .setTextRange(DbCommons.TextRange.newBuilder().setStartLine(2).setEndLine(3)).build());
+    final DefaultIssue issue1 = createIssue(2, 3, RuleTesting.XOO_X1);
     rawIssues.add(issue1);
-    final DefaultIssue issue2 = createIssue(2, RuleTesting.XOO_X1);
-    issue2.setLocations(DbIssues.Locations.newBuilder().setTextRange(DbCommons.TextRange.newBuilder().setStartLine(2).setEndLine(2)).build());
-    rawIssues.add(issue2);
+    rawIssues.add(createIssue(2, RuleTesting.XOO_X1));
 
     when(newLinesRepository.getNewLines(FILE)).thenReturn(Optional.of(new HashSet<>(Arrays.asList(1, 3))));
 
@@ -127,16 +128,36 @@ public class PullRequestTrackerExecutionTest {
   }
 
   @Test
-  public void track_and_ignore_issues_from_previous_analysis() {
+  public void track_and_ignore_resolved_issues_from_target_branch() {
     when(newLinesRepository.getNewLines(FILE)).thenReturn(Optional.of(new HashSet<>(Arrays.asList(1, 2, 3))));
 
-    rawIssues.add(createIssue(1, RuleTesting.XOO_X1)
-      .setLocations(DbIssues.Locations.newBuilder().setTextRange(DbCommons.TextRange.newBuilder().setStartLine(1).setEndLine(1)).build()));
-    rawIssues.add(createIssue(2, RuleTesting.XOO_X2)
-      .setLocations(DbIssues.Locations.newBuilder().setTextRange(DbCommons.TextRange.newBuilder().setStartLine(2).setEndLine(2)).build()));
-    rawIssues.add(createIssue(3, RuleTesting.XOO_X3)
-      .setLocations(DbIssues.Locations.newBuilder().setTextRange(DbCommons.TextRange.newBuilder().setStartLine(3).setEndLine(3)).build()));
+    rawIssues.add(createIssue(1, RuleTesting.XOO_X1));
+    rawIssues.add(createIssue(2, RuleTesting.XOO_X2));
+    rawIssues.add(createIssue(3, RuleTesting.XOO_X3));
+
+    when(targetFactory.hasTargetBranchAnalysis()).thenReturn(true);
+    DefaultIssue resolvedIssue = createIssue(1, RuleTesting.XOO_X1).setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE);
+    // will cause rawIssue0 to be ignored
+    targetIssues.add(resolvedIssue);
+    // not ignored since it's not resolved
+    targetIssues.add(rawIssues.get(1));
 
+    baseIssues.add(rawIssues.get(0));
+    // should be matched
+    baseIssues.add(rawIssues.get(1));
+
+    Tracking<DefaultIssue, DefaultIssue> tracking = underTest.track(FILE, createInput(rawIssues));
+    assertThat(tracking.getMatchedRaws()).isEqualTo(Collections.singletonMap(rawIssues.get(1), rawIssues.get(1)));
+    assertThat(tracking.getUnmatchedRaws()).containsOnly(rawIssues.get(2));
+  }
+
+  @Test
+  public void track_and_ignore_issues_from_previous_analysis() {
+    when(newLinesRepository.getNewLines(FILE)).thenReturn(Optional.of(new HashSet<>(Arrays.asList(1, 2, 3))));
+
+    rawIssues.add(createIssue(1, RuleTesting.XOO_X1));
+    rawIssues.add(createIssue(2, RuleTesting.XOO_X2));
+    rawIssues.add(createIssue(3, RuleTesting.XOO_X3));
     baseIssues.add(rawIssues.get(0));
 
     Tracking<DefaultIssue, DefaultIssue> tracking = underTest.track(FILE, createInput(rawIssues));
@@ -145,13 +166,18 @@ public class PullRequestTrackerExecutionTest {
   }
 
   private DefaultIssue createIssue(int line, RuleKey ruleKey) {
+    return createIssue(line, line, ruleKey);
+  }
+
+  private DefaultIssue createIssue(int startLine, int endLine, RuleKey ruleKey) {
     return new DefaultIssue()
       .setRuleKey(ruleKey)
-      .setLine(line)
-      .setMessage("msg" + line);
+      .setLine(startLine)
+      .setLocations(DbIssues.Locations.newBuilder().setTextRange(DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine)).build())
+      .setMessage("msg" + startLine);
   }
 
-  private Input<DefaultIssue> createInput(Collection<DefaultIssue> issues) {
+  private static Input<DefaultIssue> createInput(Collection<DefaultIssue> issues) {
     return new Input<DefaultIssue>() {
       @Override
       public LineHashSequence getLineHashSequence() {
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuidsTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TargetBranchComponentUuidsTest.java
new file mode 100644 (file)
index 0000000..23bcc02
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.issue;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
+import org.sonar.ce.task.projectanalysis.analysis.Branch;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.protobuf.DbProjectBranches;
+import org.sonar.server.project.Project;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
+
+public class TargetBranchComponentUuidsTest {
+  private static final String BRANCH_KEY = "branch1";
+  private static final String PR_KEY = "pr1";
+
+  @org.junit.Rule
+  public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private TargetBranchComponentUuids underTest;
+  private final Branch branch = mock(Branch.class);
+  private ComponentDto branch1;
+  private ComponentDto branch1File;
+  private ComponentDto pr1File;
+
+  @Before
+  public void setup() {
+    underTest = new TargetBranchComponentUuids(analysisMetadataHolder, db.getDbClient());
+    Project project = mock(Project.class);
+    analysisMetadataHolder.setProject(project);
+    analysisMetadataHolder.setBranch(branch);
+
+    ComponentDto projectDto = db.components().insertPublicProject();
+    when(project.getUuid()).thenReturn(projectDto.uuid());
+    branch1 = db.components().insertProjectBranch(projectDto, b -> b.setKey(BRANCH_KEY));
+    ComponentDto pr1branch = db.components().insertProjectBranch(projectDto, b -> b.setKey(PR_KEY)
+      .setBranchType(BranchType.PULL_REQUEST)
+      .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setTarget(BRANCH_KEY).build())
+      .setMergeBranchUuid(projectDto.getMainBranchProjectUuid()));
+    branch1File = ComponentTesting.newFileDto(branch1, null, "file").setUuid("branch1File");
+    pr1File = ComponentTesting.newFileDto(pr1branch, null, "file").setUuid("file1");
+    db.components().insertComponents(branch1File, pr1File);
+  }
+
+  @Test
+  public void should_support_db_key_when_looking_for_target_branch_component() {
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    when(branch.getName()).thenReturn("prBranch");
+    when(branch.getTargetBranchName()).thenReturn(BRANCH_KEY);
+
+    when(branch.getPullRequestKey()).thenReturn(PR_KEY);
+    db.components().insertSnapshot(newAnalysis(branch1));
+
+    assertThat(underTest.getTargetBranchComponentUuid(pr1File.getDbKey())).isEqualTo(branch1File.uuid());
+    assertThat(underTest.hasTargetBranchAnalysis()).isTrue();
+  }
+
+  @Test
+  public void should_support_key_when_looking_for_target_branch_component() {
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    when(branch.getName()).thenReturn("prBranch");
+    when(branch.getTargetBranchName()).thenReturn(BRANCH_KEY);
+    when(branch.getPullRequestKey()).thenReturn(PR_KEY);
+    db.components().insertSnapshot(newAnalysis(branch1));
+
+    assertThat(underTest.getTargetBranchComponentUuid(pr1File.getKey())).isEqualTo(branch1File.uuid());
+  }
+
+  @Test
+  public void return_null_if_file_doesnt_exist() {
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    when(branch.getName()).thenReturn("prBranch");
+    when(branch.getTargetBranchName()).thenReturn(BRANCH_KEY);
+    when(branch.getPullRequestKey()).thenReturn(PR_KEY);
+    db.components().insertSnapshot(newAnalysis(branch1));
+
+    assertThat(underTest.getTargetBranchComponentUuid("doesnt exist")).isNull();
+  }
+
+  @Test
+  public void skip_init_if_not_a_pull_request() {
+    when(branch.getType()).thenReturn(BranchType.BRANCH);
+    when(branch.getName()).thenReturn("prBranch");
+    when(branch.getTargetBranchName()).thenReturn(BRANCH_KEY);
+
+    assertThat(underTest.getTargetBranchComponentUuid(pr1File.getDbKey())).isNull();
+  }
+
+  @Test
+  public void skip_init_if_no_target_branch_analysis() {
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    when(branch.getName()).thenReturn("prBranch");
+    when(branch.getTargetBranchName()).thenReturn(BRANCH_KEY);
+
+    assertThat(underTest.getTargetBranchComponentUuid(pr1File.getDbKey())).isNull();
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerTargetBranchInputFactoryTest.java
new file mode 100644 (file)
index 0000000..3d23679
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.issue;
+
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.Input;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class TrackerTargetBranchInputFactoryTest {
+  private final static String COMPONENT_KEY = "file1";
+  private final static String COMPONENT_UUID = "uuid1";
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private final ComponentIssuesLoader componentIssuesLoader = mock(ComponentIssuesLoader.class);
+  private final TargetBranchComponentUuids targetBranchComponentUuids = mock(TargetBranchComponentUuids.class);
+  private TrackerTargetBranchInputFactory underTest;
+
+  @Before
+  public void setUp() {
+    underTest = new TrackerTargetBranchInputFactory(componentIssuesLoader, targetBranchComponentUuids, db.getDbClient());
+  }
+
+  @Test
+  public void gets_issues_and_hashes_in_matching_component() {
+    DefaultIssue issue1 = new DefaultIssue();
+    when(targetBranchComponentUuids.getTargetBranchComponentUuid(COMPONENT_KEY)).thenReturn(COMPONENT_UUID);
+    when(componentIssuesLoader.loadOpenIssuesWithChanges(COMPONENT_UUID)).thenReturn(Collections.singletonList(issue1));
+    ComponentDto fileDto = ComponentTesting.newFileDto(ComponentTesting.newPublicProjectDto()).setUuid(COMPONENT_UUID);
+    db.fileSources().insertFileSource(fileDto, 3);
+
+    Component component = mock(Component.class);
+    when(component.getDbKey()).thenReturn(COMPONENT_KEY);
+    when(component.getType()).thenReturn(Component.Type.FILE);
+    Input<DefaultIssue> input = underTest.createForTargetBranch(component);
+
+    assertThat(input.getIssues()).containsOnly(issue1);
+    assertThat(input.getLineHashSequence().length()).isEqualTo(3);
+  }
+
+  @Test
+  public void get_issues_without_line_hashes() {
+    DefaultIssue issue1 = new DefaultIssue();
+    when(targetBranchComponentUuids.getTargetBranchComponentUuid(COMPONENT_KEY)).thenReturn(COMPONENT_UUID);
+    when(componentIssuesLoader.loadOpenIssuesWithChanges(COMPONENT_UUID)).thenReturn(Collections.singletonList(issue1));
+    ComponentDto fileDto = ComponentTesting.newFileDto(ComponentTesting.newPublicProjectDto()).setUuid(COMPONENT_UUID);
+    db.fileSources().insertFileSource(fileDto, 0);
+
+    Component component = mock(Component.class);
+    when(component.getDbKey()).thenReturn(COMPONENT_KEY);
+    when(component.getType()).thenReturn(Component.Type.FILE);
+    Input<DefaultIssue> input = underTest.createForTargetBranch(component);
+
+    assertThat(input.getIssues()).containsOnly(issue1);
+    assertThat(input.getLineHashSequence().length()).isZero();
+  }
+
+  @Test
+  public void gets_nothing_when_there_is_no_matching_component() {
+    Component component = mock(Component.class);
+    when(component.getDbKey()).thenReturn(COMPONENT_KEY);
+    when(component.getType()).thenReturn(Component.Type.FILE);
+    Input<DefaultIssue> input = underTest.createForTargetBranch(component);
+
+    assertThat(input.getIssues()).isEmpty();
+    assertThat(input.getLineHashSequence().length()).isZero();
+  }
+
+  @Test
+  public void hasTargetBranchAnalysis_returns_true_if_source_branch_of_pr_was_analysed() {
+    when(targetBranchComponentUuids.hasTargetBranchAnalysis()).thenReturn(true);
+
+    assertThat(underTest.hasTargetBranchAnalysis()).isTrue();
+  }
+
+  @Test
+  public void hasTargetBranchAnalysis_returns_false_if_no_target_branch_analysis() {
+    when(targetBranchComponentUuids.hasTargetBranchAnalysis()).thenReturn(false);
+
+    assertThat(underTest.hasTargetBranchAnalysis()).isFalse();
+  }
+}
index de82e76dd4759a0d4916e3276ef41f9b6990a68b..429017f1f0553a675f2a20a9f68d0980f7cba54a 100644 (file)
@@ -76,9 +76,7 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
       }
       LineAndLineHashKey that = (LineAndLineHashKey) o;
       // start with most discriminant field
-      return Objects.equals(line, that.line)
-        && lineHash.equals(that.lineHash)
-        && ruleKey.equals(that.ruleKey);
+      return Objects.equals(line, that.line) && lineHash.equals(that.lineHash) && ruleKey.equals(that.ruleKey);
     }
 
     @Override
@@ -110,10 +108,7 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
       }
       LineAndLineHashAndMessage that = (LineAndLineHashAndMessage) o;
       // start with most discriminant field
-      return Objects.equals(line, that.line)
-        && lineHash.equals(that.lineHash)
-        && message.equals(that.message)
-        && ruleKey.equals(that.ruleKey);
+      return Objects.equals(line, that.line) && lineHash.equals(that.lineHash) && message.equals(that.message) && ruleKey.equals(that.ruleKey);
     }
 
     @Override
@@ -143,9 +138,7 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
       }
       LineHashAndMessageKey that = (LineHashAndMessageKey) o;
       // start with most discriminant field
-      return lineHash.equals(that.lineHash)
-        && message.equals(that.message)
-        && ruleKey.equals(that.ruleKey);
+      return lineHash.equals(that.lineHash) && message.equals(that.message) && ruleKey.equals(that.ruleKey);
     }
 
     @Override
@@ -175,9 +168,7 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
       }
       LineAndMessageKey that = (LineAndMessageKey) o;
       // start with most discriminant field
-      return Objects.equals(line, that.line)
-        && message.equals(that.message)
-        && ruleKey.equals(that.ruleKey);
+      return Objects.equals(line, that.line) && message.equals(that.message) && ruleKey.equals(that.ruleKey);
     }
 
     @Override
@@ -205,8 +196,7 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
       }
       LineHashKey that = (LineHashKey) o;
       // start with most discriminant field
-      return lineHash.equals(that.lineHash)
-        && ruleKey.equals(that.ruleKey);
+      return lineHash.equals(that.lineHash) && ruleKey.equals(that.ruleKey);
     }
 
     @Override
index a0cd8ea1ce8b6082180a740bed092fdffcc1e336..c876377bd91eb6fc3e00efd916b789dcd86687d4 100644 (file)
@@ -22,8 +22,8 @@ package org.sonar.core.issue.tracking;
 import java.util.Collection;
 import java.util.List;
 import java.util.stream.Stream;
-import org.sonar.api.scanner.ScannerSide;
 import org.sonar.api.issue.Issue;
+import org.sonar.api.scanner.ScannerSide;
 
 import static org.sonar.core.util.stream.MoreCollectors.toList;
 
@@ -59,7 +59,7 @@ public class Tracker<RAW extends Trackable, BASE extends Trackable> extends Abst
     ClosedTracking<RAW, BASE> closedTracking = ClosedTracking.of(nonClosedTracking, baseInput);
     match(closedTracking, LineAndLineHashAndMessage::new);
 
-    return new MergedTracking<>(nonClosedTracking, closedTracking);
+    return mergedTracking(nonClosedTracking, closedTracking);
   }
 
   private void detectCodeMoves(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) {
@@ -86,20 +86,17 @@ public class Tracker<RAW extends Trackable, BASE extends Trackable> extends Abst
     }
   }
 
-  private static class MergedTracking<RAW extends Trackable, BASE extends Trackable> extends Tracking<RAW, BASE> {
-    private MergedTracking(NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) {
-      super(
-        nonClosedTracking.getRawInput().getIssues(),
-        concatIssues(nonClosedTracking, closedTracking),
-        closedTracking.rawToBase, closedTracking.baseToRaw);
-    }
+  private Tracking<RAW, BASE> mergedTracking(NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) {
+    return new Tracking<>(nonClosedTracking.getRawInput().getIssues(),
+      concatIssues(nonClosedTracking, closedTracking),
+      closedTracking.rawToBase, closedTracking.baseToRaw);
+  }
 
-    private static <RAW extends Trackable, BASE extends Trackable> List<BASE> concatIssues(
-      NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) {
-      Collection<BASE> nonClosedIssues = nonClosedTracking.getBaseInput().getIssues();
-      Collection<BASE> closeIssues = closedTracking.getBaseInput().getIssues();
-      return Stream.concat(nonClosedIssues.stream(), closeIssues.stream())
-        .collect(toList(nonClosedIssues.size() + closeIssues.size()));
-    }
+  private static <RAW extends Trackable, BASE extends Trackable> List<BASE> concatIssues(
+    NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) {
+    Collection<BASE> nonClosedIssues = nonClosedTracking.getBaseInput().getIssues();
+    Collection<BASE> closeIssues = closedTracking.getBaseInput().getIssues();
+    return Stream.concat(nonClosedIssues.stream(), closeIssues.stream())
+      .collect(toList(nonClosedIssues.size() + closeIssues.size()));
   }
 }