]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9720 Issue tracking for long branches
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 16 Aug 2017 14:03:25 +0000 (16:03 +0200)
committerJanos Gyerik <janos.gyerik@sonarsource.com>
Tue, 12 Sep 2017 08:59:56 +0000 (10:59 +0200)
21 files changed:
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycle.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchTrackerExecution.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStep.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueAssignerTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueAdapterForFilter.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueWrapper.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/TrackedIssueAdapter.java

index 414920d19a3ac61d66b6440db15ba9eda2d5a247..f2be5a852fae5c93369acc8ad910aae978707281 100644 (file)
@@ -67,6 +67,7 @@ import org.sonar.server.computation.task.projectanalysis.issue.IssueTrackingDele
 import org.sonar.server.computation.task.projectanalysis.issue.IssueVisitors;
 import org.sonar.server.computation.task.projectanalysis.issue.IssuesRepositoryVisitor;
 import org.sonar.server.computation.task.projectanalysis.issue.LoadComponentUuidsHavingOpenIssuesVisitor;
+import org.sonar.server.computation.task.projectanalysis.issue.MergeBranchTrackerExecution;
 import org.sonar.server.computation.task.projectanalysis.issue.MovedIssueVisitor;
 import org.sonar.server.computation.task.projectanalysis.issue.NewEffortAggregator;
 import org.sonar.server.computation.task.projectanalysis.issue.NewEffortCalculator;
@@ -239,6 +240,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       Tracker.class,
       TrackerExecution.class,
       ShortBranchTrackerExecution.class,
+      MergeBranchTrackerExecution.class,
       ComponentIssuesLoader.class,
       BaseIssuesLoader.class,
       IssueTrackingDelegator.class,
index 528372d82e36eb418c2c0a2a1760bb56ae270d47..3a282e0663ce0506c5c9171c77d748486cbd634f 100644 (file)
@@ -26,7 +26,6 @@ import java.util.List;
 import java.util.Map;
 
 import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.tracking.Tracking;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 import org.sonar.server.computation.task.projectanalysis.component.Component.Status;
@@ -60,13 +59,15 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
       issueVisitors.beforeComponent(component);
 
       if (isIncremental(component)) {
+        // no tracking needed, simply re-use existing issues
         List<DefaultIssue> issues = issuesLoader.loadForComponentUuid(component.getUuid());
-        fillIncrementalOpenIssues(component, issues, cacheAppender);
+        reuseOpenIssues(component, issues, cacheAppender);
       } else {
-        Tracking<DefaultIssue, DefaultIssue> tracking = issueTracking.track(component);
-        fillNewOpenIssues(component, tracking.getUnmatchedRaws(), cacheAppender);
-        fillExistingOpenIssues(component, tracking.getMatchedRaws(), cacheAppender);
-        closeUnmatchedBaseIssues(component, tracking.getUnmatchedBases(), cacheAppender);
+        TrackingResult tracking = issueTracking.track(component);
+        fillNewOpenIssues(component, tracking.newIssues(), cacheAppender);
+        fillExistingOpenIssues(component, tracking.issuesToMerge(), cacheAppender);
+        closeIssues(component, tracking.issuesToClose(), cacheAppender);
+        copyIssues(component, tracking.issuesToCopy(), cacheAppender);
       }
       issueVisitors.afterComponent(component);
     } catch (Exception e) {
@@ -85,7 +86,16 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
     }
   }
 
-  private void fillIncrementalOpenIssues(Component component, Collection<DefaultIssue> issues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
+  private void copyIssues(Component component, Map<DefaultIssue, DefaultIssue> matched, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
+    for (Map.Entry<DefaultIssue, DefaultIssue> entry : matched.entrySet()) {
+      DefaultIssue raw = entry.getKey();
+      DefaultIssue base = entry.getValue();
+      issueLifecycle.copyExistingOpenIssue(raw, base);
+      process(component, raw, cacheAppender);
+    }
+  }
+
+  private void reuseOpenIssues(Component component, Collection<DefaultIssue> issues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
     for (DefaultIssue issue : issues) {
       process(component, issue, cacheAppender);
     }
@@ -100,7 +110,7 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
     }
   }
 
-  private void closeUnmatchedBaseIssues(Component component, Iterable<DefaultIssue> issues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
+  private void closeIssues(Component component, Iterable<DefaultIssue> issues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
     for (DefaultIssue issue : issues) {
       // TODO should replace flag "beingClosed" by express call to transition "automaticClose"
       issue.setBeingClosed(true);
index 6ee0a12e76c0be91a7a85913a5e4308f438ce044..15c8e0017c9d7f6f20697191233ef0c6d6d57624 100644 (file)
@@ -65,6 +65,29 @@ public class IssueLifecycle {
     issue.setEffort(debtCalculator.calculate(issue));
   }
 
+  public void copyExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
+    raw.setKey(Uuids.create());
+    raw.setNew(false);
+    raw.setCopied(true);
+    raw.setType(base.type());
+    raw.setCreationDate(base.creationDate());
+    raw.setUpdateDate(base.updateDate());
+    raw.setCloseDate(base.closeDate());
+    raw.setResolution(base.resolution());
+    raw.setStatus(base.status());
+    raw.setAssignee(base.assignee());
+    raw.setAuthorLogin(base.authorLogin());
+    raw.setTags(base.tags());
+    raw.setAttributes(base.attributes());
+    raw.setEffort(debtCalculator.calculate(raw));
+    raw.setOnDisabledRule(base.isOnDisabledRule());
+    if (base.manualSeverity()) {
+      raw.setManualSeverity(true);
+      raw.setSeverity(base.severity());
+    }
+    raw.setSelectedAt(base.selectedAt());
+  }
+
   public void mergeExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
     raw.setNew(false);
     raw.setKey(base.key());
index c8ce36312841870c8afc6108dfc681f19be11333..655b482c58db82d82b08aea5811aeb3b92fe3a9d 100644 (file)
  */
 package org.sonar.server.computation.task.projectanalysis.issue;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+
+import java.util.Optional;
 
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.db.component.BranchType;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.server.computation.task.projectanalysis.analysis.Branch;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 
 public class IssueTrackingDelegator {
   private final ShortBranchTrackerExecution shortBranchTracker;
   private final TrackerExecution tracker;
   private final AnalysisMetadataHolder analysisMetadataHolder;
+  private final MergeBranchTrackerExecution mergeBranchTracker;
 
-  public IssueTrackingDelegator(ShortBranchTrackerExecution shortBranchTracker, TrackerExecution tracker, AnalysisMetadataHolder analysisMetadataHolder) {
+  public IssueTrackingDelegator(ShortBranchTrackerExecution shortBranchTracker, MergeBranchTrackerExecution longBranchTracker,
+    TrackerExecution tracker, AnalysisMetadataHolder analysisMetadataHolder) {
     this.shortBranchTracker = shortBranchTracker;
+    this.mergeBranchTracker = longBranchTracker;
     this.tracker = tracker;
     this.analysisMetadataHolder = analysisMetadataHolder;
   }
 
-  public Tracking<DefaultIssue, DefaultIssue> track(Component component) {
+  public TrackingResult track(Component component) {
     if (analysisMetadataHolder.isShortLivingBranch()) {
-      return shortBranchTracker.track(component);
+      return standardResult(shortBranchTracker.track(component));
+    } else if (isFirstAnalysisSecondaryLongLivingBranch()) {
+      Tracking<DefaultIssue, DefaultIssue> tracking = mergeBranchTracker.track(component);
+      return new TrackingResult(tracking.getMatchedRaws(), emptyMap(), emptyList(), tracking.getUnmatchedRaws());
     } else {
-      return tracker.track(component);
+      return standardResult(tracker.track(component));
+    }
+  }
+
+  private static TrackingResult standardResult(Tracking<DefaultIssue, DefaultIssue> tracking) {
+    return new TrackingResult(emptyMap(), tracking.getMatchedRaws(), tracking.getUnmatchedBases(), tracking.getUnmatchedRaws());
+  }
+
+  /**
+   * Special case where we want to do the issue tracking with the merge branch, and copy matched issue to the current branch.
+   */
+  private boolean isFirstAnalysisSecondaryLongLivingBranch() {
+    if (analysisMetadataHolder.isFirstAnalysis()) {
+      Optional<Branch> branch = analysisMetadataHolder.getBranch();
+      if (branch.isPresent()) {
+        return !branch.get().isMain() && branch.get().getType() == BranchType.LONG;
+      }
     }
+    return false;
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java
new file mode 100644 (file)
index 0000000..ed84ffd
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.server.computation.task.projectanalysis.issue;
+
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.Tracker;
+import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.server.computation.task.projectanalysis.component.Component;
+
+public class MergeBranchTrackerExecution {
+  private final TrackerRawInputFactory rawInputFactory;
+  private final TrackerMergeBranchInputFactory mergeInputFactory;
+  private final Tracker<DefaultIssue, DefaultIssue> tracker;
+
+  public MergeBranchTrackerExecution(TrackerRawInputFactory rawInputFactory, TrackerMergeBranchInputFactory mergeInputFactory,
+    Tracker<DefaultIssue, DefaultIssue> tracker) {
+    this.rawInputFactory = rawInputFactory;
+    this.mergeInputFactory = mergeInputFactory;
+    this.tracker = tracker;
+  }
+
+  public Tracking<DefaultIssue, DefaultIssue> track(Component component) {
+    return tracker.track(rawInputFactory.create(component), mergeInputFactory.create(component));
+  }
+}
index f5888d5c2cb32ffc7c0ccf5255b6646259528157..ebc4d88b3320283c2b6f30598bea1721a1cb04e2 100644 (file)
@@ -29,10 +29,10 @@ import org.sonar.core.issue.tracking.Tracking;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 
 public class ShortBranchTrackerExecution {
-  private TrackerBaseInputFactory baseInputFactory;
-  private TrackerRawInputFactory rawInputFactory;
-  private TrackerMergeBranchInputFactory mergeInputFactory;
-  private Tracker<DefaultIssue, DefaultIssue> tracker;
+  private final TrackerBaseInputFactory baseInputFactory;
+  private final TrackerRawInputFactory rawInputFactory;
+  private final TrackerMergeBranchInputFactory mergeInputFactory;
+  private final Tracker<DefaultIssue, DefaultIssue> tracker;
 
   public ShortBranchTrackerExecution(TrackerBaseInputFactory baseInputFactory, TrackerRawInputFactory rawInputFactory, TrackerMergeBranchInputFactory mergeInputFactory,
     Tracker<DefaultIssue, DefaultIssue> tracker) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java
new file mode 100644 (file)
index 0000000..b469301
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.server.computation.task.projectanalysis.issue;
+
+import java.util.Map;
+
+import org.sonar.core.issue.DefaultIssue;
+
+public class TrackingResult {
+  private final Map<DefaultIssue, DefaultIssue> issuesToCopy;
+  private final Map<DefaultIssue, DefaultIssue> issuesToMerge;
+  private final Iterable<DefaultIssue> issuesToClose;
+  private final Iterable<DefaultIssue> newIssues;
+
+  public TrackingResult(Map<DefaultIssue, DefaultIssue> issuesToCopy, Map<DefaultIssue, DefaultIssue> issuesToMerge,
+    Iterable<DefaultIssue> issuesToClose, Iterable<DefaultIssue> newIssues) {
+    this.issuesToCopy = issuesToCopy;
+    this.issuesToMerge = issuesToMerge;
+    this.issuesToClose = issuesToClose;
+    this.newIssues = newIssues;
+  }
+
+  public Map<DefaultIssue, DefaultIssue> issuesToCopy() {
+    return issuesToCopy;
+  }
+
+  public Map<DefaultIssue, DefaultIssue> issuesToMerge() {
+    return issuesToMerge;
+  }
+
+  public Iterable<DefaultIssue> issuesToClose() {
+    return issuesToClose;
+  }
+
+  public Iterable<DefaultIssue> newIssues() {
+    return newIssues;
+  }
+}
index 25ee9915bf505f172747cd95885867907d0935f8..6276bf44b25fd3751432e0e05d66b136f44bdbe3 100644 (file)
@@ -73,10 +73,11 @@ public class PersistIssuesStep implements ComputationStep {
   }
 
   private boolean persistIssueIfRequired(IssueMapper mapper, DefaultIssue issue) {
-    if (issue.isNew()) {
+    if (issue.isNew() || issue.isCopied()) {
       persistNewIssue(mapper, issue);
       return true;
     }
+
     if (issue.isChanged()) {
       persistChangedIssue(mapper, issue);
       return true;
index 6c663d282500689f6af113b23f0b87b9a37429d2..0ae5dada91c8efe0f3cb37b295a21c1d4fbe9421 100644 (file)
@@ -119,6 +119,7 @@ public class IntegrateIssuesVisitorTest {
   IssueTrackingDelegator trackingDelegator;
   TrackerExecution tracker;
   ShortBranchTrackerExecution shortBranchTracker;
+  MergeBranchTrackerExecution mergeBranchTracker;
   IssueCache issueCache;
 
   TypeAwareVisitor underTest;
@@ -135,8 +136,9 @@ public class IntegrateIssuesVisitorTest {
     TrackerMergeBranchInputFactory mergeInputFactory = new TrackerMergeBranchInputFactory(issuesLoader, analysisMetadataHolder, dbTester.getDbClient());
     tracker = new TrackerExecution(baseInputFactory, rawInputFactory, new Tracker<>());
     shortBranchTracker = new ShortBranchTrackerExecution(baseInputFactory, rawInputFactory, mergeInputFactory, new Tracker<>());
+    mergeBranchTracker = new MergeBranchTrackerExecution(rawInputFactory, mergeInputFactory, new Tracker<>());
 
-    trackingDelegator = new IssueTrackingDelegator(shortBranchTracker, tracker, analysisMetadataHolder);
+    trackingDelegator = new IssueTrackingDelegator(shortBranchTracker, mergeBranchTracker, tracker, analysisMetadataHolder);
     treeRootHolder.setRoot(PROJECT);
     issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
     when(analysisMetadataHolder.isIncrementalAnalysis()).thenReturn(false);
@@ -165,11 +167,6 @@ public class IntegrateIssuesVisitorTest {
     assertThat(newArrayList(issueCache.traverse())).hasSize(1);
   }
 
-  @Test
-  public void process_short_branch_issues() {
-    //TODO
-  }
-
   @Test
   public void process_new_issue() throws Exception {
     ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
@@ -268,10 +265,6 @@ public class IntegrateIssuesVisitorTest {
     underTest.visitAny(FILE);
   }
 
-  private void addMergeIssue(RuleKey ruleKey) {
-
-  }
-
   private void addBaseIssue(RuleKey ruleKey) {
     ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), PROJECT_UUID).setDbKey(PROJECT_KEY);
     ComponentDto file = ComponentTesting.newFileDto(project, null, FILE_UUID).setDbKey(FILE_KEY);
index a82ece85b5a34405e45af80c44b1c7f85744a9b3..7780f069ae299cce8ea9d514f5130a330bcceb9a 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.computation.task.projectanalysis.issue;
 
 import org.junit.Test;
+import org.junit.Rule;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
@@ -40,13 +41,13 @@ public class IssueAssignerTest {
   static final int FILE_REF = 1;
   static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build();
 
-  @org.junit.Rule
+  @Rule
   public LogTester logTester = new LogTester();
 
-  @org.junit.Rule
+  @Rule
   public ScmInfoRepositoryRule scmInfoRepository = new ScmInfoRepositoryRule();
 
-  @org.junit.Rule
+  @Rule
   public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule().setAnalysisDate(123456789L);
 
   ScmAccountToUser scmAccountToUser = mock(ScmAccountToUser.class);
@@ -185,7 +186,7 @@ public class IssueAssignerTest {
         "moduleUuid=<null>,moduleUuidPath=<null>,projectUuid=<null>,projectKey=<null>,ruleKey=<null>,language=<null>,severity=<null>," +
         "manualSeverity=false,message=<null>,line=2,gap=<null>,effort=<null>,status=<null>,resolution=<null>," +
         "assignee=<null>,checksum=<null>,attributes=<null>,authorLogin=<null>,comments=<null>,tags=<null>," +
-        "locations=<null>,creationDate=<null>,updateDate=<null>,closeDate=<null>,currentChange=<null>,changes=<null>,isNew=true," +
+        "locations=<null>,creationDate=<null>,updateDate=<null>,closeDate=<null>,currentChange=<null>,changes=<null>,isNew=true,isCopied=false," +
         "beingClosed=false,onDisabledRule=false,isChanged=false,sendNotifications=false,selectedAt=<null>]");
   }
 
index a056ccecddea37cc40e0a1938156fa6c3142118a..f7e3a07c7b136b594b74941291d64f8f7c12618d 100644 (file)
@@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
@@ -70,6 +71,66 @@ public class IssueLifecycleTest {
     assertThat(issue.updateDate()).isNotNull();
     assertThat(issue.status()).isEqualTo(STATUS_OPEN);
     assertThat(issue.debt()).isEqualTo(DEFAULT_DURATION);
+    assertThat(issue.isNew()).isTrue();
+    assertThat(issue.isCopied()).isFalse();
+  }
+
+  @Test
+  public void copiedIssue() throws Exception {
+    DefaultIssue raw = new DefaultIssue()
+      .setNew(true)
+      .setKey("RAW_KEY")
+      .setCreationDate(parseDate("2015-10-01"))
+      .setUpdateDate(parseDate("2015-10-02"))
+      .setCloseDate(parseDate("2015-10-03"));
+
+    DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
+      .setTextRange(DbCommons.TextRange.newBuilder()
+        .setStartLine(10)
+        .setEndLine(12)
+        .build())
+      .build();
+    DefaultIssue base = new DefaultIssue()
+      .setKey("BASE_KEY")
+      .setCreationDate(parseDate("2015-01-01"))
+      .setUpdateDate(parseDate("2015-01-02"))
+      .setCloseDate(parseDate("2015-01-03"))
+      .setResolution(RESOLUTION_FIXED)
+      .setStatus(STATUS_CLOSED)
+      .setSeverity(BLOCKER)
+      .setAssignee("base assignee")
+      .setAuthorLogin("base author")
+      .setTags(newArrayList("base tag"))
+      .setOnDisabledRule(true)
+      .setSelectedAt(1000L)
+      .setLine(10)
+      .setMessage("message")
+      .setGap(15d)
+      .setEffort(Duration.create(15L))
+      .setManualSeverity(false)
+      .setLocations(issueLocations);
+
+    when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
+
+    underTest.copyExistingOpenIssue(raw, base);
+
+    assertThat(raw.isNew()).isFalse();
+    assertThat(raw.isCopied()).isTrue();
+    assertThat(raw.key()).isNotNull();
+    assertThat(raw.key()).isNotEqualTo(base.key());
+    assertThat(raw.creationDate()).isEqualTo(base.creationDate());
+    assertThat(raw.updateDate()).isEqualTo(base.updateDate());
+    assertThat(raw.closeDate()).isEqualTo(base.closeDate());
+    assertThat(raw.resolution()).isEqualTo(RESOLUTION_FIXED);
+    assertThat(raw.status()).isEqualTo(STATUS_CLOSED);
+    assertThat(raw.assignee()).isEqualTo("base assignee");
+    assertThat(raw.authorLogin()).isEqualTo("base author");
+    assertThat(raw.tags()).containsOnly("base tag");
+    assertThat(raw.debt()).isEqualTo(DEFAULT_DURATION);
+    assertThat(raw.isOnDisabledRule()).isTrue();
+    assertThat(raw.selectedAt()).isEqualTo(1000L);
+
+    verifyZeroInteractions(updater);
   }
 
   @Test
index 4eede37eb18a62553be0d14f4a0adec356d8e3f3..966f98ef6235835becdd53b69d8411c8c113f1e6 100644 (file)
@@ -24,10 +24,14 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import java.util.Optional;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.Tracking;
 import org.sonar.db.component.BranchType;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.server.computation.task.projectanalysis.analysis.Branch;
@@ -37,40 +41,66 @@ public class IssueTrackingDelegatorTest {
   @Mock
   private ShortBranchTrackerExecution shortBranchTracker;
   @Mock
+  private MergeBranchTrackerExecution mergeBranchTracker;
+  @Mock
   private TrackerExecution tracker;
   @Mock
   private AnalysisMetadataHolder analysisMetadataHolder;
   @Mock
   private Component component;
+  @Mock
+  private Tracking<DefaultIssue, DefaultIssue> trackingResult;
 
   private IssueTrackingDelegator underTest;
 
   @Before
   public void setUp() {
     MockitoAnnotations.initMocks(this);
-    underTest = new IssueTrackingDelegator(shortBranchTracker, tracker, analysisMetadataHolder);
+    underTest = new IssueTrackingDelegator(shortBranchTracker, mergeBranchTracker, tracker, analysisMetadataHolder);
+    when(tracker.track(component)).thenReturn(trackingResult);
+    when(mergeBranchTracker.track(component)).thenReturn(trackingResult);
+    when(shortBranchTracker.track(component)).thenReturn(trackingResult);
   }
 
   @Test
   public void delegate_regular_tracker() {
     when(analysisMetadataHolder.isShortLivingBranch()).thenReturn(false);
+    when(analysisMetadataHolder.getBranch()).thenReturn(Optional.empty());
 
     underTest.track(component);
 
     verify(tracker).track(component);
     verifyZeroInteractions(shortBranchTracker);
+    verifyZeroInteractions(mergeBranchTracker);
+  }
+
+  @Test
+  public void delegate_merge_tracker() {
+    Branch branch = mock(Branch.class);
+    when(branch.getType()).thenReturn(BranchType.LONG);
+    when(branch.isMain()).thenReturn(false);
+    when(analysisMetadataHolder.getBranch()).thenReturn(Optional.of(branch));
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
+
+    underTest.track(component);
+
+    verify(mergeBranchTracker).track(component);
+    verifyZeroInteractions(tracker);
+    verifyZeroInteractions(shortBranchTracker);
+
   }
 
   @Test
   public void delegate_short_branch_tracker() {
     Branch branch = mock(Branch.class);
     when(branch.getType()).thenReturn(BranchType.SHORT);
+    when(analysisMetadataHolder.getBranch()).thenReturn(Optional.empty());
     when(analysisMetadataHolder.isShortLivingBranch()).thenReturn(true);
 
     underTest.track(component);
 
     verify(shortBranchTracker).track(component);
     verifyZeroInteractions(tracker);
-
+    verifyZeroInteractions(mergeBranchTracker);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java
new file mode 100644 (file)
index 0000000..5dc408b
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.server.computation.task.projectanalysis.issue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.Input;
+import org.sonar.core.issue.tracking.Tracker;
+import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.server.computation.task.projectanalysis.component.Component;
+
+public class MergeBranchTrackerExecutionTest {
+  @Mock
+  private TrackerRawInputFactory rawInputFactory;
+  @Mock
+  private TrackerMergeBranchInputFactory mergeInputFactory;
+  @Mock
+  private Tracker<DefaultIssue, DefaultIssue> tracker;
+  @Mock
+  private Component component;
+
+  private MergeBranchTrackerExecution underTest;
+
+  @Before
+  public void before() {
+    MockitoAnnotations.initMocks(this);
+    underTest = new MergeBranchTrackerExecution(rawInputFactory, mergeInputFactory, tracker);
+  }
+
+  @Test
+  public void testTracking() {
+    Input<DefaultIssue> rawInput = mock(Input.class);
+    Input<DefaultIssue> mergeInput = mock(Input.class);
+    Tracking<DefaultIssue, DefaultIssue> result = mock(Tracking.class);
+    when(rawInputFactory.create(component)).thenReturn(rawInput);
+    when(mergeInputFactory.create(component)).thenReturn(mergeInput);
+    when(tracker.track(rawInput, mergeInput)).thenReturn(result);
+
+    assertThat(underTest.track(component)).isEqualTo(result);
+  }
+}
index 1612e0d333c9592f29893bfe1243be22e1344fae..bbb198d92ab63a16c8d189df5e5dc6f0f25b6e6b 100644 (file)
@@ -93,6 +93,41 @@ public class PersistIssuesStepTest extends BaseStepTest {
     session.close();
   }
 
+  @Test
+  public void insert_copied_issue() {
+    RuleDefinitionDto rule = RuleTesting.newRule(RuleKey.of("xoo", "S01"));
+    dbTester.rules().insert(rule);
+    OrganizationDto organizationDto = dbTester.organizations().insert();
+    ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto);
+    dbClient.componentDao().insert(session, project);
+    ComponentDto file = ComponentTesting.newFileDto(project, null);
+    dbClient.componentDao().insert(session, file);
+    session.commit();
+
+    issueCache.newAppender().append(new DefaultIssue()
+      .setKey("ISSUE")
+      .setType(RuleType.CODE_SMELL)
+      .setRuleKey(rule.getKey())
+      .setComponentUuid(file.uuid())
+      .setProjectUuid(project.uuid())
+      .setSeverity(Severity.BLOCKER)
+      .setStatus(Issue.STATUS_OPEN)
+      .setNew(false)
+      .setCopied(true)
+      .setType(RuleType.BUG)).close();
+
+    step.execute();
+
+    IssueDto result = dbClient.issueDao().selectOrFailByKey(session, "ISSUE");
+    assertThat(result.getKey()).isEqualTo("ISSUE");
+    assertThat(result.getRuleKey()).isEqualTo(rule.getKey());
+    assertThat(result.getComponentUuid()).isEqualTo(file.uuid());
+    assertThat(result.getProjectUuid()).isEqualTo(project.uuid());
+    assertThat(result.getSeverity()).isEqualTo(Severity.BLOCKER);
+    assertThat(result.getStatus()).isEqualTo(Issue.STATUS_OPEN);
+    assertThat(result.getType()).isEqualTo(RuleType.BUG.getDbConstant());
+  }
+
   @Test
   public void insert_new_issue() {
     RuleDefinitionDto rule = RuleTesting.newRule(RuleKey.of("xoo", "S01"));
index a92b911d44f21ae4989bf6d6defa1fe6e5754a21..8ac7a205d584120419858130da0e58033f0ceab1 100644 (file)
@@ -96,10 +96,13 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
   // all changes
   private List<FieldDiffs> changes = null;
 
-  // true if the the issue did not exist in the previous scan.
+  // true if the issue did not exist in the previous scan.
   private boolean isNew = true;
 
-  // True if the the issue did exist in the previous scan but not in the current one. That means
+  // true if the issue is being copied to a different branch
+  private boolean isCopied = false;
+
+  // True if the issue did exist in the previous scan but not in the current one. That means
   // that this issue should be closed.
   private boolean beingClosed = false;
 
@@ -412,6 +415,16 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
     return isNew;
   }
 
+  @Override
+  public boolean isCopied() {
+    return isCopied;
+  }
+
+  public DefaultIssue setCopied(boolean b) {
+    isCopied = b;
+    return this;
+  }
+
   public DefaultIssue setNew(boolean b) {
     isNew = b;
     return this;
index 9a1a2c3e5dc96771fb6bf11b6f6713dccfe61c01..293237409024dcb9776f2bb10646562cdc00561a 100644 (file)
@@ -161,6 +161,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder {
     issue.setStatus(Issue.STATUS_OPEN);
     issue.setCloseDate(null);
     issue.setNew(true);
+    issue.setCopied(false);
     issue.setBeingClosed(false);
     issue.setOnDisabledRule(false);
     return issue;
index f9c36b3bc2a5707463b68959375706bc885ceaa9..247c2d32ebde372f31cf2369d709a593d81f107a 100644 (file)
@@ -59,6 +59,7 @@ public class DefaultIssueTest {
       .setNew(true)
       .setBeingClosed(true)
       .setOnDisabledRule(true)
+      .setCopied(true)
       .setChanged(true)
       .setSendNotifications(true)
       .setCreationDate(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"))
@@ -83,6 +84,7 @@ public class DefaultIssueTest {
     assertThat(issue.authorLogin()).isEqualTo("steph");
     assertThat(issue.checksum()).isEqualTo("c7b5db46591806455cf082bb348631e8");
     assertThat(issue.isNew()).isTrue();
+    assertThat(issue.isCopied()).isTrue();
     assertThat(issue.isBeingClosed()).isTrue();
     assertThat(issue.isOnDisabledRule()).isTrue();
     assertThat(issue.isChanged()).isTrue();
index 4ed5b6750c9c5fe2d95694ec421d05abab24b53f..9d4ffe437632062935f459fea2cccdc133d5ac0e 100644 (file)
@@ -206,6 +206,12 @@ public interface Issue extends Serializable {
    */
   boolean isNew();
 
+  /**
+   * During a scan returns true if the issue is copied from another branch.
+   * @since 6.6
+   */
+  boolean isCopied();
+
   /**
    * @deprecated since 5.5, replaced by {@link #effort()}
    */
index 4b45945147e3f99f7cfd5370af61398a2fb54e39..389ace4a936c3fdc969f15e8d07e74bd658233e2 100644 (file)
@@ -160,6 +160,11 @@ class DeprecatedIssueAdapterForFilter implements Issue {
     throw unsupported();
   }
 
+  @Override
+  public boolean isCopied() {
+    throw unsupported();
+  }
+
   @Deprecated
   @Override
   public Duration debt() {
index 843d7c1e7e4d10ffe42861ce11b1cecd67af26d6..5c487b5bf4b128e019df801fcc2287d2fb56da45 100644 (file)
@@ -160,6 +160,11 @@ public class DeprecatedIssueWrapper implements Issue {
     return false;
   }
 
+  @Override
+  public boolean isCopied() {
+    return false;
+  }
+
   @Override
   public Duration debt() {
     return null;
index 6db8b0abaa639a7bb3e3ca3ac2f302cd3376802c..5d969e3a56e9d85b9cdaeb761c7aba4775d14aa4 100644 (file)
@@ -113,6 +113,11 @@ public class TrackedIssueAdapter implements Issue {
     return issue.isNew();
   }
 
+  @Override
+  public boolean isCopied() {
+    return false;
+  }
+
   @Override
   public Map<String, String> attributes() {
     return new HashMap<>();