]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9970 Copy issue changes when creating an long living branch
authorJulien HENRY <julien.henry@sonarsource.com>
Fri, 13 Oct 2017 09:11:45 +0000 (11:11 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Fri, 20 Oct 2017 08:45:15 +0000 (18:45 +1000)
16 files changed:
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
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/ComponentIssuesLoader.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/ShortBranchIssueMerger.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopier.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssuesLoader.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.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/IssueLifecycleTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueMergerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopierTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java

index 8fc59751ee0cb4f88e451ea1b80a5ab862587415..5201db84197c9250e84070e3488ac265a853e800 100644 (file)
@@ -28,6 +28,7 @@ import org.apache.ibatis.session.ResultHandler;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
@@ -186,12 +187,12 @@ public class IssueDaoTest {
     RuleDefinitionDto rule = db.rules().insert();
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    IssueDto openIssue = db.issues().insert(rule, project, file, i -> i.setStatus("OPEN").setResolution(null));
-    IssueDto closedIssue = db.issues().insert(rule, project, file, i -> i.setStatus("CLOSED").setResolution("FIXED"));
-    IssueDto reopenedIssue = db.issues().insert(rule, project, file, i -> i.setStatus("REOPENED").setResolution(null));
-    IssueDto confirmedIssue = db.issues().insert(rule, project, file, i -> i.setStatus("CONFIRMED").setResolution(null));
-    IssueDto wontfixIssue = db.issues().insert(rule, project, file, i -> i.setStatus("RESOLVED").setResolution("WONTFIX"));
-    IssueDto fpIssue = db.issues().insert(rule, project, file, i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE"));
+    IssueDto openIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_OPEN).setResolution(null));
+    IssueDto closedIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED));
+    IssueDto reopenedIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_REOPENED).setResolution(null));
+    IssueDto confirmedIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_CONFIRMED).setResolution(null));
+    IssueDto wontfixIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_WONT_FIX));
+    IssueDto fpIssue = db.issues().insert(rule, project, file, i -> i.setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE));
 
     assertThat(underTest.selectResolvedOrConfirmedByComponentUuids(db.getSession(), Collections.singletonList(file.uuid())))
       .extracting("kee")
index 2e5be26254332696c564659c9e6788624fbcc951..a68aa9e53403e72dce2ec3c58e53cce99fa3827f 100644 (file)
@@ -64,7 +64,7 @@ import org.sonar.server.computation.task.projectanalysis.issue.IssueCache;
 import org.sonar.server.computation.task.projectanalysis.issue.IssueCounter;
 import org.sonar.server.computation.task.projectanalysis.issue.IssueCreationDateCalculator;
 import org.sonar.server.computation.task.projectanalysis.issue.IssueLifecycle;
-import org.sonar.server.computation.task.projectanalysis.issue.ShortBranchIssueStatusCopier;
+import org.sonar.server.computation.task.projectanalysis.issue.ShortBranchIssueMerger;
 import org.sonar.server.computation.task.projectanalysis.issue.IssueTrackingDelegator;
 import org.sonar.server.computation.task.projectanalysis.issue.IssueVisitors;
 import org.sonar.server.computation.task.projectanalysis.issue.IssuesRepositoryVisitor;
@@ -253,7 +253,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       IssueTrackingDelegator.class,
       BranchPersister.class,
       ShortBranchIssuesLoader.class,
-      ShortBranchIssueStatusCopier.class,
+      ShortBranchIssueMerger.class,
 
       // filemove
       SourceSimilarityImpl.class,
index 2ae00cdd1539484b9294135c2a71e9de8b0756a5..91a45d7bc547dca5e16f992939d9873a168aaa15 100644 (file)
@@ -21,15 +21,20 @@ package org.sonar.server.computation.task.projectanalysis.issue;
 
 import java.util.ArrayList;
 import java.util.List;
-
+import java.util.Map;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.issue.IssueChangeDto;
 import org.sonar.db.issue.IssueMapper;
 import org.sonar.server.computation.task.projectanalysis.qualityprofile.ActiveRulesHolder;
 
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
+
 public class ComponentIssuesLoader {
   private final DbClient dbClient;
   private final RuleRepository ruleRepository;
@@ -43,24 +48,59 @@ public class ComponentIssuesLoader {
 
   public List<DefaultIssue> loadForComponentUuid(String componentUuid) {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      List<DefaultIssue> result = new ArrayList<>();
-      dbSession.getMapper(IssueMapper.class).scrollNonClosedByComponentUuid(componentUuid, resultContext -> {
-        DefaultIssue issue = (resultContext.getResultObject()).toDefaultIssue();
+      return loadForComponentUuid(componentUuid, dbSession);
+    }
+  }
+
+  public List<DefaultIssue> loadForComponentUuidWithChanges(String componentUuid) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      List<DefaultIssue> result = loadForComponentUuid(componentUuid, dbSession);
 
-        // TODO this field should be set outside this class
-        if (!isActive(issue.ruleKey()) || ruleRepository.getByKey(issue.ruleKey()).getStatus() == RuleStatus.REMOVED) {
-          issue.setOnDisabledRule(true);
-          // TODO to be improved, why setOnDisabledRule(true) is not enough ?
-          issue.setBeingClosed(true);
-        }
-        // FIXME
-        issue.setSelectedAt(System.currentTimeMillis());
-        result.add(issue);
-      });
-      return result;
+      Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao()
+        .selectByIssueKeys(dbSession, result.stream().map(DefaultIssue::key).collect(toList()))
+        .stream()
+        .collect(groupingBy(IssueChangeDto::getIssueKey));
+
+      return result
+        .stream()
+        .peek(i -> setChanges(changeDtoByIssueKey, i))
+        .collect(toList());
     }
   }
 
+  private List<DefaultIssue> loadForComponentUuid(String componentUuid, DbSession dbSession) {
+    List<DefaultIssue> result = new ArrayList<>();
+    dbSession.getMapper(IssueMapper.class).scrollNonClosedByComponentUuid(componentUuid, resultContext -> {
+      DefaultIssue issue = (resultContext.getResultObject()).toDefaultIssue();
+
+      // TODO this field should be set outside this class
+      if (!isActive(issue.ruleKey()) || ruleRepository.getByKey(issue.ruleKey()).getStatus() == RuleStatus.REMOVED) {
+        issue.setOnDisabledRule(true);
+        // TODO to be improved, why setOnDisabledRule(true) is not enough ?
+        issue.setBeingClosed(true);
+      }
+      // FIXME
+      issue.setSelectedAt(System.currentTimeMillis());
+      result.add(issue);
+    });
+    return result;
+  }
+
+  public static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
+    changeDtoByIssueKey.computeIfAbsent(i.key(), k -> emptyList()).forEach(c -> {
+      switch (c.getChangeType()) {
+        case IssueChangeDto.TYPE_FIELD_CHANGE:
+          i.addChange(c.toFieldDiffs());
+          break;
+        case IssueChangeDto.TYPE_COMMENT:
+          i.addComment(c.toComment());
+          break;
+        default:
+          throw new IllegalStateException("Unknow change type: " + c.getChangeType());
+      }
+    });
+  }
+
   private boolean isActive(RuleKey ruleKey) {
     return activeRulesHolder.get(ruleKey).isPresent();
   }
index a96d8f2d7e9d840d48c2986d9636b58b31a40fdc..e0c9bd92fc1eea07a682ed8bed13e80add1b492e 100644 (file)
@@ -37,11 +37,11 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
   private final IssueLifecycle issueLifecycle;
   private final IssueVisitors issueVisitors;
   private final IssueTrackingDelegator issueTracking;
-  private final ShortBranchIssueStatusCopier issueStatusCopier;
+  private final ShortBranchIssueMerger issueStatusCopier;
   private final AnalysisMetadataHolder analysisMetadataHolder;
 
   public IntegrateIssuesVisitor(IssueCache issueCache, IssueLifecycle issueLifecycle, IssueVisitors issueVisitors,
-    AnalysisMetadataHolder analysisMetadataHolder, IssueTrackingDelegator issueTracking, ShortBranchIssueStatusCopier issueStatusCopier) {
+    AnalysisMetadataHolder analysisMetadataHolder, IssueTrackingDelegator issueTracking, ShortBranchIssueMerger issueStatusCopier) {
     super(CrawlerDepthLimit.FILE, POST_ORDER);
     this.issueCache = issueCache;
     this.issueLifecycle = issueLifecycle;
@@ -80,7 +80,7 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
     }
 
     if (analysisMetadataHolder.isLongLivingBranch()) {
-      issueStatusCopier.updateStatus(component, list);
+      issueStatusCopier.tryMerge(component, list);
     }
 
     for (DefaultIssue issue : list) {
index 7f88d3404e0819737513b1ab75c88b1dc718bb48..6b40c04feb47b13929df7d1ca37b4ebc356748ac 100644 (file)
@@ -69,30 +69,22 @@ public class IssueLifecycle {
   public void copyExistingOpenIssueFromLongLivingBranch(DefaultIssue raw, DefaultIssue base) {
     raw.setKey(Uuids.create());
     raw.setNew(false);
-    raw.setCopied(true);
-    copyFields(raw, base);
+    copyIssueAttributes(raw, base);
+  }
 
-    if (base.manualSeverity()) {
-      raw.setManualSeverity(true);
-      raw.setSeverity(base.severity());
+  public void copyIssueAttributes(DefaultIssue to, DefaultIssue from) {
+    to.setCopied(true);
+    copyFields(to, from);
+    if (from.manualSeverity()) {
+      to.setManualSeverity(true);
+      to.setSeverity(from.severity());
     }
+    copyChanges(to, from);
   }
 
-  public void mergeIssueFromShortLivingBranch(DefaultIssue raw, DefaultIssue fromShortLiving) {
-    raw.setCopied(true);
-    raw.setType(fromShortLiving.type());
-    raw.setResolution(fromShortLiving.resolution());
-    raw.setStatus(fromShortLiving.status());
-    raw.setAssignee(fromShortLiving.assignee());
-    raw.setAuthorLogin(fromShortLiving.authorLogin());
-    raw.setTags(fromShortLiving.tags());
-    raw.setAttributes(fromShortLiving.attributes());
-    if (fromShortLiving.manualSeverity()) {
-      raw.setManualSeverity(true);
-      raw.setSeverity(fromShortLiving.severity());
-    }
-    fromShortLiving.comments().forEach(c -> raw.addComment(DefaultIssueComment.copy(raw.key(), c)));
-    fromShortLiving.changes().forEach(c -> raw.addChange(FieldDiffs.copy(raw.key(), c)));
+  private static void copyChanges(DefaultIssue raw, DefaultIssue base) {
+    base.comments().forEach(c -> raw.addComment(DefaultIssueComment.copy(raw.key(), c)));
+    base.changes().forEach(c -> raw.addChange(FieldDiffs.copy(raw.key(), c)));
   }
 
   public void mergeExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueMerger.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueMerger.java
new file mode 100644 (file)
index 0000000..79d5e5c
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.Collection;
+import java.util.Map;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.ShortBranchIssue;
+import org.sonar.core.issue.tracking.SimpleTracker;
+import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.server.computation.task.projectanalysis.component.Component;
+
+public class ShortBranchIssueMerger {
+  private final ShortBranchIssuesLoader shortBranchIssuesLoader;
+  private final SimpleTracker<DefaultIssue, ShortBranchIssue> tracker;
+  private final IssueLifecycle issueLifecycle;
+
+  public ShortBranchIssueMerger(ShortBranchIssuesLoader resolvedShortBranchIssuesLoader, IssueLifecycle issueLifecycle) {
+    this(resolvedShortBranchIssuesLoader, new SimpleTracker<>(), issueLifecycle);
+  }
+
+  public ShortBranchIssueMerger(ShortBranchIssuesLoader shortBranchIssuesLoader, SimpleTracker<DefaultIssue, ShortBranchIssue> tracker, IssueLifecycle issueLifecycle) {
+    this.shortBranchIssuesLoader = shortBranchIssuesLoader;
+    this.tracker = tracker;
+    this.issueLifecycle = issueLifecycle;
+  }
+
+  /**
+   * Look for all resolved/confirmed issues in short living branches targeting the current long living branch, and run
+   * a light issue tracking to find matches. Then merge issue attributes in the new issues. 
+   */
+  public void tryMerge(Component component, Collection<DefaultIssue> newIssues) {
+    Collection<ShortBranchIssue> shortBranchIssues = shortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component);
+    Tracking<DefaultIssue, ShortBranchIssue> tracking = tracker.track(newIssues, shortBranchIssues);
+
+    Map<DefaultIssue, ShortBranchIssue> matchedRaws = tracking.getMatchedRaws();
+
+    Map<ShortBranchIssue, DefaultIssue> defaultIssues = shortBranchIssuesLoader.loadDefaultIssuesWithChanges(matchedRaws.values());
+
+    for (Map.Entry<DefaultIssue, ShortBranchIssue> e : matchedRaws.entrySet()) {
+      ShortBranchIssue issue = e.getValue();
+      issueLifecycle.copyIssueAttributes(e.getKey(), defaultIssues.get(issue));
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopier.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopier.java
deleted file mode 100644 (file)
index 3944af7..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.Collection;
-import java.util.Map;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.ShortBranchIssue;
-import org.sonar.core.issue.tracking.SimpleTracker;
-import org.sonar.core.issue.tracking.Tracking;
-import org.sonar.server.computation.task.projectanalysis.component.Component;
-
-public class ShortBranchIssueStatusCopier {
-  private final ShortBranchIssuesLoader shortBranchIssuesLoader;
-  private final SimpleTracker<DefaultIssue, ShortBranchIssue> tracker;
-  private final IssueLifecycle issueLifecycle;
-
-  public ShortBranchIssueStatusCopier(ShortBranchIssuesLoader resolvedShortBranchIssuesLoader, IssueLifecycle issueLifecycle) {
-    this(resolvedShortBranchIssuesLoader, new SimpleTracker<>(), issueLifecycle);
-  }
-
-  public ShortBranchIssueStatusCopier(ShortBranchIssuesLoader shortBranchIssuesLoader, SimpleTracker<DefaultIssue, ShortBranchIssue> tracker, IssueLifecycle issueLifecycle) {
-    this.shortBranchIssuesLoader = shortBranchIssuesLoader;
-    this.tracker = tracker;
-    this.issueLifecycle = issueLifecycle;
-  }
-
-  public void updateStatus(Component component, Collection<DefaultIssue> newIssues) {
-    Collection<ShortBranchIssue> shortBranchIssues = shortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component);
-    Tracking<DefaultIssue, ShortBranchIssue> tracking = tracker.track(newIssues, shortBranchIssues);
-
-    Map<DefaultIssue, ShortBranchIssue> matchedRaws = tracking.getMatchedRaws();
-
-    Map<ShortBranchIssue, DefaultIssue> defaultIssues = shortBranchIssuesLoader.loadDefaultIssuesWithChanges(matchedRaws.values());
-
-    for (Map.Entry<DefaultIssue, ShortBranchIssue> e : matchedRaws.entrySet()) {
-      ShortBranchIssue issue = e.getValue();
-      issueLifecycle.mergeIssueFromShortLivingBranch(e.getKey(), defaultIssues.get(issue));
-    }
-  }
-}
index acc598059a6d1cf7ffbc34ec8a57b40c61924b29..1beca5a1b642c2ef9a4eac569e6d245f0c9babd6 100644 (file)
@@ -76,23 +76,9 @@ public class ShortBranchIssuesLoader {
       return dbClient.issueDao().selectByKeys(session, issuesByKey.keySet())
         .stream()
         .map(IssueDto::toDefaultIssue)
-        .peek(i -> setChanges(changeDtoByIssueKey, i))
+        .peek(i -> ComponentIssuesLoader.setChanges(changeDtoByIssueKey, i))
         .collect(toMap(i -> issuesByKey.get(i.key()), i -> i));
     }
   }
 
-  private static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
-    changeDtoByIssueKey.get(i.key()).forEach(c -> {
-      switch (c.getChangeType()) {
-        case IssueChangeDto.TYPE_FIELD_CHANGE:
-          i.addChange(c.toFieldDiffs());
-          break;
-        case IssueChangeDto.TYPE_COMMENT:
-          i.addComment(c.toComment());
-          break;
-        default:
-          throw new IllegalStateException("Unknow change type: " + c.getChangeType());
-      }
-    });
-  }
 }
index 85717967e56b6b0146512da69d3ef04b39720949..8a3746fb3f40de69c1860e882674cabd4fc3c88e 100644 (file)
@@ -79,7 +79,7 @@ public class TrackerMergeBranchInputFactory {
       if (mergeBranchComponentUuid == null) {
         return Collections.emptyList();
       }
-      return mergeIssuesLoader.loadForComponentUuid(mergeBranchComponentUuid);
+      return mergeIssuesLoader.loadForComponentUuidWithChanges(mergeBranchComponentUuid);
     }
   }
 
index 6b186a724119e56b987c63b9a61fe055a3993699..ddb9f58af7978107f757c954f4f8e6ad022c430d 100644 (file)
@@ -161,14 +161,14 @@ public abstract class IssueStorage {
       }
     }
     FieldDiffs diffs = issue.currentChange();
-    if (!issue.isNew() && diffs != null) {
-      IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs);
-      mapper.insert(changeDto);
-    } else if (issue.isCopied()) {
+    if (issue.isCopied()) {
       for (FieldDiffs d : issue.changes()) {
         IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), d);
         mapper.insert(changeDto);
       }
+    } else if (!issue.isNew() && diffs != null) {
+      IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs);
+      mapper.insert(changeDto);
     }
   }
 
index 3adcf83d6ca246d70e22ef8c8fef0c2645735170..52d282523af65e7fec21eeff8fa34c67b27c1fa8 100644 (file)
@@ -57,7 +57,8 @@ public class CloseIssuesOnRemovedComponentsVisitorTest {
   @Before
   public void setUp() throws Exception {
     issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
-    underTest = new VisitorsCrawler(Arrays.<ComponentVisitor>asList(new CloseIssuesOnRemovedComponentsVisitor(issuesLoader, componentsWithUnprocessedIssues, issueCache, issueLifecycle)));
+    underTest = new VisitorsCrawler(
+      Arrays.<ComponentVisitor>asList(new CloseIssuesOnRemovedComponentsVisitor(issuesLoader, componentsWithUnprocessedIssues, issueCache, issueLifecycle)));
   }
 
   @Test
index 8f30a43833326e3f8e5a4c69371f4a79d077f3a6..1427302612c78569572bc8f8783eb733041c9858 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.tracking.Tracker;
 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.issue.IssueDto;
@@ -45,6 +46,7 @@ import org.sonar.db.rule.RuleTesting;
 import org.sonar.scanner.protocol.Constants;
 import org.sonar.scanner.protocol.output.ScannerReport;
 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.batch.BatchReportReaderRule;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 import org.sonar.server.computation.task.projectanalysis.component.MergeBranchComponentUuids;
@@ -61,6 +63,7 @@ import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.builder;
@@ -68,6 +71,7 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report
 public class IntegrateIssuesVisitorTest {
 
   static final String FILE_UUID = "FILE_UUID";
+  static final String FILE_UUID_ON_BRANCH = "FILE_UUID_BRANCH";
   static final String FILE_KEY = "FILE_KEY";
   static final int FILE_REF = 2;
 
@@ -78,6 +82,7 @@ public class IntegrateIssuesVisitorTest {
 
   static final String PROJECT_KEY = "PROJECT_KEY";
   static final String PROJECT_UUID = "PROJECT_UUID";
+  static final String PROJECT_UUID_ON_BRANCH = "PROJECT_UUID_BRANCH";
   static final int PROJECT_REF = 1;
   static final Component PROJECT = builder(Component.Type.PROJECT, PROJECT_REF)
     .setKey(PROJECT_KEY)
@@ -113,7 +118,7 @@ public class IntegrateIssuesVisitorTest {
   @Mock
   private MergeBranchComponentUuids mergeBranchComponentsUuids;
   @Mock
-  private ShortBranchIssueStatusCopier issueStatusCopier;
+  private ShortBranchIssueMerger issueStatusCopier;
 
   ArgumentCaptor<DefaultIssue> defaultIssueCaptor;
 
@@ -166,7 +171,7 @@ public class IntegrateIssuesVisitorTest {
     DefaultIssue capturedIssue = defaultIssueCaptor.getValue();
     assertThat(capturedIssue.ruleKey().rule()).isEqualTo("S001");
 
-    verify(issueStatusCopier).updateStatus(FILE, Collections.singletonList(capturedIssue));
+    verify(issueStatusCopier).tryMerge(FILE, Collections.singletonList(capturedIssue));
 
     verify(issueLifecycle).doAutomaticTransition(capturedIssue);
 
@@ -249,6 +254,47 @@ public class IntegrateIssuesVisitorTest {
     underTest.visitAny(FILE);
   }
 
+  @Test
+  public void copy_issues_when_creating_new_long_living_branch() throws Exception {
+
+    when(mergeBranchComponentsUuids.getUuid(FILE_KEY)).thenReturn(FILE_UUID_ON_BRANCH);
+
+    when(analysisMetadataHolder.isLongLivingBranch()).thenReturn(true);
+    when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
+    Branch branch = mock(Branch.class);
+    when(branch.isMain()).thenReturn(false);
+    when(branch.getType()).thenReturn(BranchType.LONG);
+    when(analysisMetadataHolder.getBranch()).thenReturn(java.util.Optional.of(branch));
+
+    RuleKey ruleKey = RuleTesting.XOO_X1;
+    // Issue from main branch has severity major
+    addBaseIssueOnBranch(ruleKey);
+
+    // Issue from report has severity blocker
+    ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
+      .setMsg("the message")
+      .setRuleRepository(ruleKey.repository())
+      .setRuleKey(ruleKey.rule())
+      .setSeverity(Constants.Severity.BLOCKER)
+      .build();
+    reportReader.putIssues(FILE_REF, asList(reportIssue));
+    fileSourceRepository.addLine(FILE_REF, "line1");
+
+    underTest.visitAny(FILE);
+
+    ArgumentCaptor<DefaultIssue> rawIssueCaptor = ArgumentCaptor.forClass(DefaultIssue.class);
+    ArgumentCaptor<DefaultIssue> baseIssueCaptor = ArgumentCaptor.forClass(DefaultIssue.class);
+    verify(issueLifecycle).copyExistingOpenIssueFromLongLivingBranch(rawIssueCaptor.capture(), baseIssueCaptor.capture());
+    assertThat(rawIssueCaptor.getValue().severity()).isEqualTo(Severity.BLOCKER);
+    assertThat(baseIssueCaptor.getValue().severity()).isEqualTo(Severity.MAJOR);
+
+    verify(issueLifecycle).doAutomaticTransition(defaultIssueCaptor.capture());
+    assertThat(defaultIssueCaptor.getValue().ruleKey()).isEqualTo(ruleKey);
+    List<DefaultIssue> issues = newArrayList(issueCache.traverse());
+    assertThat(issues).hasSize(1);
+    assertThat(issues.get(0).severity()).isEqualTo(Severity.BLOCKER);
+  }
+
   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);
@@ -265,4 +311,21 @@ public class IntegrateIssuesVisitorTest {
     dbTester.getDbClient().issueDao().insert(dbTester.getSession(), issue);
     dbTester.getSession().commit();
   }
+
+  private void addBaseIssueOnBranch(RuleKey ruleKey) {
+    ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), PROJECT_UUID_ON_BRANCH).setDbKey(PROJECT_KEY);
+    ComponentDto file = ComponentTesting.newFileDto(project, null, FILE_UUID_ON_BRANCH).setDbKey(FILE_KEY);
+    dbTester.getDbClient().componentDao().insert(dbTester.getSession(), project, file);
+
+    RuleDto ruleDto = RuleTesting.newDto(ruleKey);
+    dbTester.rules().insertRule(ruleDto);
+    ruleRepositoryRule.add(ruleKey);
+
+    IssueDto issue = IssueTesting.newDto(ruleDto, file, project)
+      .setKee("ISSUE")
+      .setStatus(Issue.STATUS_OPEN)
+      .setSeverity(Severity.MAJOR);
+    dbTester.getDbClient().issueDao().insert(dbTester.getSession(), issue);
+    dbTester.getSession().commit();
+  }
 }
index a588db622b50fcced977eabeeb7248c2151f33b0..ecd79be561777a2db56cfb2d1474344bac8bf47a 100644 (file)
@@ -81,7 +81,7 @@ public class IssueLifecycleTest {
     DefaultIssue fromShort = new DefaultIssue();
     fromShort.setResolution("resolution");
     fromShort.setStatus("status");
-    underTest.mergeIssueFromShortLivingBranch(raw, fromShort);
+    underTest.copyIssueAttributes(raw, fromShort);
     assertThat(raw.resolution()).isEqualTo("resolution");
     assertThat(raw.status()).isEqualTo("status");
   }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueMergerTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueMergerTest.java
new file mode 100644 (file)
index 0000000..fcda03e
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.ShortBranchIssue;
+import org.sonar.core.issue.tracking.SimpleTracker;
+import org.sonar.server.computation.task.projectanalysis.component.Component;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyListOf;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class ShortBranchIssueMergerTest {
+  @Mock
+  private ShortBranchIssuesLoader resolvedShortBranchIssuesLoader;
+  @Mock
+  private IssueLifecycle issueLifecycle;
+  @Mock
+  private Component component;
+
+  private SimpleTracker<DefaultIssue, ShortBranchIssue> tracker = new SimpleTracker<>();
+  private ShortBranchIssueMerger copier;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    copier = new ShortBranchIssueMerger(resolvedShortBranchIssuesLoader, tracker, issueLifecycle);
+  }
+
+  @Test
+  public void do_nothing_if_no_match() {
+    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.emptyList());
+    DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+    copier.tryMerge(component, Collections.singleton(i));
+
+    verify(resolvedShortBranchIssuesLoader).loadCandidateIssuesForMergingInTargetBranch(component);
+    verifyZeroInteractions(issueLifecycle);
+  }
+
+  @Test
+  public void do_nothing_if_no_new_issue() {
+    DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(newShortBranchIssue(i)));
+    copier.tryMerge(component, Collections.emptyList());
+
+    verify(resolvedShortBranchIssuesLoader).loadCandidateIssuesForMergingInTargetBranch(component);
+    verifyZeroInteractions(issueLifecycle);
+  }
+
+  @Test
+  public void update_status_on_matches() {
+    DefaultIssue issue1 = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+    ShortBranchIssue shortBranchIssue = newShortBranchIssue(issue1);
+    DefaultIssue newIssue = createIssue("issue2", "rule1", Issue.STATUS_OPEN, null);
+
+    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(shortBranchIssue));
+    when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue, issue1));
+    copier.tryMerge(component, Collections.singleton(newIssue));
+    ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+    verify(resolvedShortBranchIssuesLoader).loadDefaultIssuesWithChanges(captor.capture());
+    assertThat(captor.getValue()).containsOnly(shortBranchIssue);
+    verify(issueLifecycle).copyIssueAttributes(newIssue, issue1);
+  }
+
+  @Test
+  public void prefer_resolved_issues() {
+    ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null));
+    ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_CONFIRMED, null));
+    DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE);
+    ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3);
+    DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null);
+
+    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
+    when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
+    copier.tryMerge(component, Collections.singleton(newIssue));
+    verify(issueLifecycle).copyIssueAttributes(newIssue, issue3);
+  }
+
+  private static DefaultIssue createIssue(String key, String ruleKey, String status, @Nullable String resolution) {
+    DefaultIssue issue = new DefaultIssue();
+    issue.setKey(key);
+    issue.setRuleKey(RuleKey.of("repo", ruleKey));
+    issue.setMessage("msg");
+    issue.setLine(1);
+    issue.setStatus(status);
+    issue.setResolution(resolution);
+    return issue;
+  }
+
+  private ShortBranchIssue newShortBranchIssue(DefaultIssue i) {
+    return new ShortBranchIssue(i.key(), i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status());
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopierTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssueStatusCopierTest.java
deleted file mode 100644 (file)
index d244d44..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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 com.google.common.collect.ImmutableMap;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import javax.annotation.Nullable;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.ShortBranchIssue;
-import org.sonar.core.issue.tracking.SimpleTracker;
-import org.sonar.server.computation.task.projectanalysis.component.Component;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.anyListOf;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class ShortBranchIssueStatusCopierTest {
-  @Mock
-  private ShortBranchIssuesLoader resolvedShortBranchIssuesLoader;
-  @Mock
-  private IssueLifecycle issueLifecycle;
-  @Mock
-  private Component component;
-
-  private SimpleTracker<DefaultIssue, ShortBranchIssue> tracker = new SimpleTracker<>();
-  private ShortBranchIssueStatusCopier copier;
-
-  @Before
-  public void setUp() {
-    MockitoAnnotations.initMocks(this);
-    copier = new ShortBranchIssueStatusCopier(resolvedShortBranchIssuesLoader, tracker, issueLifecycle);
-  }
-
-  @Test
-  public void do_nothing_if_no_match() {
-    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.emptyList());
-    DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
-    copier.updateStatus(component, Collections.singleton(i));
-
-    verify(resolvedShortBranchIssuesLoader).loadCandidateIssuesForMergingInTargetBranch(component);
-    verifyZeroInteractions(issueLifecycle);
-  }
-
-  @Test
-  public void do_nothing_if_no_new_issue() {
-    DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
-    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(newShortBranchIssue(i)));
-    copier.updateStatus(component, Collections.emptyList());
-
-    verify(resolvedShortBranchIssuesLoader).loadCandidateIssuesForMergingInTargetBranch(component);
-    verifyZeroInteractions(issueLifecycle);
-  }
-
-  @Test
-  public void update_status_on_matches() {
-    DefaultIssue issue1 = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
-    ShortBranchIssue shortBranchIssue = newShortBranchIssue(issue1);
-    DefaultIssue newIssue = createIssue("issue2", "rule1", Issue.STATUS_OPEN, null);
-
-    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(shortBranchIssue));
-    when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue, issue1));
-    copier.updateStatus(component, Collections.singleton(newIssue));
-    ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
-    verify(resolvedShortBranchIssuesLoader).loadDefaultIssuesWithChanges(captor.capture());
-    assertThat(captor.getValue()).containsOnly(shortBranchIssue);
-    verify(issueLifecycle).mergeIssueFromShortLivingBranch(newIssue, issue1);
-  }
-
-  @Test
-  public void prefer_resolved_issues() {
-    ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null));
-    ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_CONFIRMED, null));
-    DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE);
-    ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3);
-    DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null);
-
-    when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
-    when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
-    copier.updateStatus(component, Collections.singleton(newIssue));
-    verify(issueLifecycle).mergeIssueFromShortLivingBranch(newIssue, issue3);
-  }
-
-  private static DefaultIssue createIssue(String key, String ruleKey, String status, @Nullable String resolution) {
-    DefaultIssue issue = new DefaultIssue();
-    issue.setKey(key);
-    issue.setRuleKey(RuleKey.of("repo", ruleKey));
-    issue.setMessage("msg");
-    issue.setLine(1);
-    issue.setStatus(status);
-    issue.setResolution(resolution);
-    return issue;
-  }
-
-  private ShortBranchIssue newShortBranchIssue(DefaultIssue i) {
-    return new ShortBranchIssue(i.key(), i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status());
-  }
-}
index bbb198d92ab63a16c8d189df5e5dc6f0f25b6e6b..1fbc1c9d1a2d7970584e164e3c664a5474166f9e 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.server.computation.task.projectanalysis.step;
 
+import java.util.Arrays;
+import java.util.List;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -37,6 +39,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.issue.IssueChangeDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.rule.RuleDefinitionDto;
@@ -114,7 +117,67 @@ public class PersistIssuesStepTest extends BaseStepTest {
       .setStatus(Issue.STATUS_OPEN)
       .setNew(false)
       .setCopied(true)
-      .setType(RuleType.BUG)).close();
+      .setType(RuleType.BUG)
+      .addComment(new DefaultIssueComment()
+        .setKey("COMMENT")
+        .setIssueKey("ISSUE")
+        .setUserLogin("john")
+        .setMarkdownText("Some text")
+        .setNew(true))
+      .setCurrentChange(new FieldDiffs()
+        .setIssueKey("ISSUE")
+        .setUserLogin("john")
+        .setDiff("technicalDebt", null, 1L)))
+      .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());
+
+    List<IssueChangeDto> changes = dbClient.issueChangeDao().selectByIssueKeys(session, Arrays.asList("ISSUE"));
+    assertThat(changes).extracting(IssueChangeDto::getChangeType).containsExactly(IssueChangeDto.TYPE_COMMENT, IssueChangeDto.TYPE_FIELD_CHANGE);
+  }
+
+  @Test
+  public void insert_merged_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(true)
+      .setCopied(true)
+      .setType(RuleType.BUG)
+      .addComment(new DefaultIssueComment()
+        .setKey("COMMENT")
+        .setIssueKey("ISSUE")
+        .setUserLogin("john")
+        .setMarkdownText("Some text")
+        .setNew(true))
+      .setCurrentChange(new FieldDiffs()
+        .setIssueKey("ISSUE")
+        .setUserLogin("john")
+        .setDiff("technicalDebt", null, 1L)))
+      .close();
 
     step.execute();
 
@@ -126,6 +189,9 @@ public class PersistIssuesStepTest extends BaseStepTest {
     assertThat(result.getSeverity()).isEqualTo(Severity.BLOCKER);
     assertThat(result.getStatus()).isEqualTo(Issue.STATUS_OPEN);
     assertThat(result.getType()).isEqualTo(RuleType.BUG.getDbConstant());
+
+    List<IssueChangeDto> changes = dbClient.issueChangeDao().selectByIssueKeys(session, Arrays.asList("ISSUE"));
+    assertThat(changes).extracting(IssueChangeDto::getChangeType).containsExactly(IssueChangeDto.TYPE_COMMENT, IssueChangeDto.TYPE_FIELD_CHANGE);
   }
 
   @Test