aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-ce-task-projectanalysis
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2022-03-29 15:07:36 -0500
committersonartech <sonartech@sonarsource.com>2022-04-07 20:02:52 +0000
commit905a0e4a8b3c2f6b80314c4ac10edbd1c0307ff1 (patch)
treec2b790982bf7fc7297596b15dc4aa48bc2450676 /server/sonar-ce-task-projectanalysis
parent426c4b096c7fe3f9fe9426ea7f07d399bbd195ce (diff)
downloadsonarqube-905a0e4a8b3c2f6b80314c4ac10edbd1c0307ff1.tar.gz
sonarqube-905a0e4a8b3c2f6b80314c4ac10edbd1c0307ff1.zip
SONAR-15321 Large number of entries in 'issue_changes'
Diffstat (limited to 'server/sonar-ce-task-projectanalysis')
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java237
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepository.java37
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestSourceBranchMerger.java3
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStep.java57
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java114
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepositoryTest.java35
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ProjectTrackerBaseLazyInputTest.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/SiblingsIssueMergerTest.java9
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStepTest.java66
12 files changed, 446 insertions, 121 deletions
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
index e236cfdd0b3..c8045d67c54 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
@@ -61,6 +61,7 @@ import org.sonar.ce.task.projectanalysis.issue.DefaultAssignee;
import org.sonar.ce.task.projectanalysis.issue.EffortAggregator;
import org.sonar.ce.task.projectanalysis.issue.IntegrateIssuesVisitor;
import org.sonar.ce.task.projectanalysis.issue.IssueAssigner;
+import org.sonar.ce.task.projectanalysis.issue.IssueChangesToDeleteRepository;
import org.sonar.ce.task.projectanalysis.issue.IssueCounter;
import org.sonar.ce.task.projectanalysis.issue.IssueCreationDateCalculator;
import org.sonar.ce.task.projectanalysis.issue.IssueLifecycle;
@@ -233,6 +234,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
FileSourceDataComputer.class,
SourceLineReadersFactory.class,
QProfileStatusRepositoryImpl.class,
+ IssueChangesToDeleteRepository.class,
// issues
RuleRepositoryImpl.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java
index a3a58c072a6..2242c66eabf 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java
@@ -22,7 +22,7 @@ package org.sonar.ce.task.projectanalysis.issue;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
+import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -50,9 +50,12 @@ import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.sonar.api.issue.Issue.STATUS_CLOSED;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.issue.IssueFieldsSetter.FROM_BRANCH;
+import static org.sonar.server.issue.IssueFieldsSetter.STATUS;
public class ComponentIssuesLoader {
private static final int DEFAULT_CLOSED_ISSUES_MAX_AGE = 30;
+ static final int NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP = 15;
private static final String PROPERTY_CLOSED_ISSUE_MAX_AGE = "sonar.issuetracking.closedissues.maxage";
private final DbClient dbClient;
@@ -60,9 +63,10 @@ public class ComponentIssuesLoader {
private final ActiveRulesHolder activeRulesHolder;
private final System2 system2;
private final int closedIssueMaxAge;
+ private final IssueChangesToDeleteRepository issueChangesToDeleteRepository;
public ComponentIssuesLoader(DbClient dbClient, RuleRepository ruleRepository, ActiveRulesHolder activeRulesHolder,
- Configuration configuration, System2 system2) {
+ Configuration configuration, System2 system2, IssueChangesToDeleteRepository issueChangesToDeleteRepository) {
this.dbClient = dbClient;
this.activeRulesHolder = activeRulesHolder;
this.ruleRepository = ruleRepository;
@@ -71,16 +75,7 @@ public class ComponentIssuesLoader {
.map(ComponentIssuesLoader::safelyParseClosedIssueMaxAge)
.filter(i -> i >= 0)
.orElse(DEFAULT_CLOSED_ISSUES_MAX_AGE);
- }
-
- private static Integer safelyParseClosedIssueMaxAge(String str) {
- try {
- return Integer.parseInt(str);
- } catch (NumberFormatException e) {
- Loggers.get(ComponentIssuesLoader.class)
- .warn("Value of property {} should be an integer >= 0: {}", PROPERTY_CLOSED_ISSUE_MAX_AGE, str);
- return null;
- }
+ this.issueChangesToDeleteRepository = issueChangesToDeleteRepository;
}
public List<DefaultIssue> loadOpenIssues(String componentUuid) {
@@ -92,24 +87,25 @@ public class ComponentIssuesLoader {
public List<DefaultIssue> loadOpenIssuesWithChanges(String componentUuid) {
try (DbSession dbSession = dbClient.openSession(false)) {
List<DefaultIssue> result = loadOpenIssues(componentUuid, dbSession);
-
return loadChanges(dbSession, result);
}
}
+ /**
+ * Loads all comments and changes EXCEPT old changes involving a status change or a move between branches.
+ */
public List<DefaultIssue> loadChanges(DbSession dbSession, Collection<DefaultIssue> issues) {
Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao()
.selectByIssueKeys(dbSession, issues.stream().map(DefaultIssue::key).collect(toList()))
.stream()
.collect(groupingBy(IssueChangeDto::getIssueKey));
- issues.forEach(i -> setChanges(changeDtoByIssueKey, i));
+ issues.forEach(i -> setFilteredChanges(changeDtoByIssueKey, i));
return new ArrayList<>(issues);
}
/**
- * Loads the most recent diff changes of the specified issues which contain the latest status and resolution of the
- * issue.
+ * Loads the most recent diff changes of the specified issues which contain the latest status and resolution of the issue.
*/
public void loadLatestDiffChangesForReopeningOfClosedIssues(Collection<DefaultIssue> issues) {
if (issues.isEmpty()) {
@@ -122,42 +118,62 @@ public class ComponentIssuesLoader {
}
/**
+ * Load closed issues for the specified Component, which have at least one line diff in changelog AND are
+ * not manual vulnerabilities.
+ * <p>
+ * Closed issues do not have a line number in DB (it is unset when the issue is closed), this method
+ * returns {@link DefaultIssue} objects which line number is populated from the most recent diff logging
+ * the removal of the line. Closed issues which do not have such diff are not loaded.
+ * <p>
+ * To not depend on purge configuration of closed issues, only issues which close date is less than 30 days ago at
+ * 00H00 are returned.
+ */
+ public List<DefaultIssue> loadClosedIssues(String componentUuid) {
+ if (closedIssueMaxAge == 0) {
+ return emptyList();
+ }
+
+ Date date = new Date(system2.now());
+ long closeDateAfter = date.toInstant()
+ .minus(closedIssueMaxAge, ChronoUnit.DAYS)
+ .truncatedTo(ChronoUnit.DAYS)
+ .toEpochMilli();
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return loadClosedIssues(dbSession, componentUuid, closeDateAfter);
+ }
+ }
+
+ private static Integer safelyParseClosedIssueMaxAge(String str) {
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+ Loggers.get(ComponentIssuesLoader.class)
+ .warn("Value of property {} should be an integer >= 0: {}", PROPERTY_CLOSED_ISSUE_MAX_AGE, str);
+ return null;
+ }
+ }
+
+ /**
* To be efficient both in term of memory and speed:
* <ul>
* <li>only diff changes are loaded from DB, sorted by issue and then change creation date</li>
* <li>data from DB is streamed</li>
* <li>only the latest change(s) with status and resolution are added to the {@link DefaultIssue} objects</li>
* </ul>
+ *
+ * While loading the changes for the issues, this class will also collect old status changes that should be deleted.
*/
private void loadLatestDiffChangesForReopeningOfClosedIssues(DbSession dbSession, Collection<DefaultIssue> issues) {
Map<String, DefaultIssue> issuesByKey = issues.stream().collect(uniqueIndex(DefaultIssue::key));
+ CollectIssueChangesToDeleteResultHandler collectChangesToDelete = new CollectIssueChangesToDeleteResultHandler(issueChangesToDeleteRepository);
+ CollectLastStatusAndResolution collectLastStatusAndResolution = new CollectLastStatusAndResolution(issuesByKey);
- dbClient.issueChangeDao()
- .scrollDiffChangesOfIssues(dbSession, issuesByKey.keySet(), new ResultHandler<IssueChangeDto>() {
- private DefaultIssue currentIssue = null;
- private boolean previousStatusFound = false;
- private boolean previousResolutionFound = false;
-
- @Override
- public void handleResult(ResultContext<? extends IssueChangeDto> resultContext) {
- IssueChangeDto issueChangeDto = resultContext.getResultObject();
- if (currentIssue == null || !currentIssue.key().equals(issueChangeDto.getIssueKey())) {
- currentIssue = issuesByKey.get(issueChangeDto.getIssueKey());
- previousStatusFound = false;
- previousResolutionFound = false;
- }
+ dbClient.issueChangeDao().scrollDiffChangesOfIssues(dbSession, issuesByKey.keySet(), resultContext -> {
+ IssueChangeDto issueChangeDto = resultContext.getResultObject();
+ FieldDiffs fieldDiffs = issueChangeDto.toFieldDiffs();
- if (currentIssue != null) {
- FieldDiffs fieldDiffs = issueChangeDto.toFieldDiffs();
- boolean hasPreviousStatus = fieldDiffs.get("status") != null;
- boolean hasPreviousResolution = fieldDiffs.get("resolution") != null;
- if ((!previousStatusFound && hasPreviousStatus) || (!previousResolutionFound && hasPreviousResolution)) {
- currentIssue.addChange(fieldDiffs);
- }
- previousStatusFound |= hasPreviousStatus;
- previousResolutionFound |= hasPreviousResolution;
- }
- }
+ collectChangesToDelete.handle(issueChangeDto, fieldDiffs);
+ collectLastStatusAndResolution.handle(issueChangeDto, fieldDiffs);
});
}
@@ -176,24 +192,44 @@ public class ComponentIssuesLoader {
issue.setSelectedAt(System.currentTimeMillis());
result.add(issue);
});
- return Collections.unmodifiableList(result);
+ return unmodifiableList(result);
}
- private static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
- changeDtoByIssueKey.computeIfAbsent(i.key(), k -> emptyList())
- .forEach(c -> addChangeOrComment(i, c));
- }
+ private void setFilteredChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
+ List<IssueChangeDto> sortedIssueChanges = changeDtoByIssueKey.computeIfAbsent(i.key(), k -> emptyList()).stream()
+ .sorted(Comparator.comparing(IssueChangeDto::getIssueChangeCreationDate).reversed())
+ .collect(toList());
- private static void addChangeOrComment(DefaultIssue i, IssueChangeDto 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("Unknown change type: " + c.getChangeType());
+ int statusCount = 0;
+ int branchCount = 0;
+
+ for (IssueChangeDto c : sortedIssueChanges) {
+ switch (c.getChangeType()) {
+ case IssueChangeDto.TYPE_FIELD_CHANGE:
+ FieldDiffs fieldDiffs = c.toFieldDiffs();
+ // To limit the amount of changes that copied issues carry over, we only keep the 15 most recent changes that involve a status change or a move between branches.
+ if (fieldDiffs.get(STATUS) != null) {
+ statusCount++;
+ if (statusCount > NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP) {
+ issueChangesToDeleteRepository.add(c.getUuid());
+ break;
+ }
+ }
+ if (fieldDiffs.get(FROM_BRANCH) != null) {
+ branchCount++;
+ if (branchCount > NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP) {
+ issueChangesToDeleteRepository.add(c.getUuid());
+ break;
+ }
+ }
+ i.addChange(c.toFieldDiffs());
+ break;
+ case IssueChangeDto.TYPE_COMMENT:
+ i.addComment(c.toComment());
+ break;
+ default:
+ throw new IllegalStateException("Unknown change type: " + c.getChangeType());
+ }
}
}
@@ -201,32 +237,6 @@ public class ComponentIssuesLoader {
return activeRulesHolder.get(ruleKey).isPresent();
}
- /**
- * Load closed issues for the specified Component, which have at least one line diff in changelog AND are
- * not manual vulnerabilities.
- * <p>
- * Closed issues do not have a line number in DB (it is unset when the issue is closed), this method
- * returns {@link DefaultIssue} objects which line number is populated from the most recent diff logging
- * the removal of the line. Closed issues which do not have such diff are not loaded.
- * <p>
- * To not depend on purge configuration of closed issues, only issues which close date is less than 30 days ago at
- * 00H00 are returned.
- */
- public List<DefaultIssue> loadClosedIssues(String componentUuid) {
- if (closedIssueMaxAge == 0) {
- return emptyList();
- }
-
- Date date = new Date(system2.now());
- long closeDateAfter = date.toInstant()
- .minus(closedIssueMaxAge, ChronoUnit.DAYS)
- .truncatedTo(ChronoUnit.DAYS)
- .toEpochMilli();
- try (DbSession dbSession = dbClient.openSession(false)) {
- return loadClosedIssues(dbSession, componentUuid, closeDateAfter);
- }
- }
-
private static List<DefaultIssue> loadClosedIssues(DbSession dbSession, String componentUuid, long closeDateAfter) {
ClosedIssuesResultHandler handler = new ClosedIssuesResultHandler();
dbSession.getMapper(IssueMapper.class).scrollClosedByComponentUuid(componentUuid, closeDateAfter, handler);
@@ -266,4 +276,67 @@ public class ComponentIssuesLoader {
issues.add(issue);
}
}
+
+ private static class CollectLastStatusAndResolution {
+ private final Map<String, DefaultIssue> issuesByKey;
+ private DefaultIssue currentIssue = null;
+ private boolean previousStatusFound = false;
+ private boolean previousResolutionFound = false;
+
+ private CollectLastStatusAndResolution(Map<String, DefaultIssue> issuesByKey) {
+ this.issuesByKey = issuesByKey;
+ }
+
+ /**
+ * Assumes that changes are sorted by issue key and date desc
+ */
+ public void handle(IssueChangeDto issueChangeDto, FieldDiffs fieldDiffs) {
+ if (currentIssue == null || !currentIssue.key().equals(issueChangeDto.getIssueKey())) {
+ currentIssue = issuesByKey.get(issueChangeDto.getIssueKey());
+ previousStatusFound = false;
+ previousResolutionFound = false;
+ }
+
+ if (currentIssue != null) {
+ boolean hasPreviousStatus = fieldDiffs.get("status") != null;
+ boolean hasPreviousResolution = fieldDiffs.get("resolution") != null;
+
+ if ((!previousStatusFound && hasPreviousStatus) || (!previousResolutionFound && hasPreviousResolution)) {
+ currentIssue.addChange(fieldDiffs);
+ }
+ previousStatusFound |= hasPreviousStatus;
+ previousResolutionFound |= hasPreviousResolution;
+ }
+ }
+ }
+
+ /**
+ * Collects issue changes related to status changes that should be cleaned up.
+ * If we have more than 15 status changes recorded for an issue, only the 15 most recent ones should be kept.
+ */
+ private static class CollectIssueChangesToDeleteResultHandler {
+ private final IssueChangesToDeleteRepository issueChangesToDeleteRepository;
+ private String currentIssueKey;
+ private int statusChangeCount;
+
+ public CollectIssueChangesToDeleteResultHandler(IssueChangesToDeleteRepository issueChangesToDeleteRepository) {
+ this.issueChangesToDeleteRepository = issueChangesToDeleteRepository;
+ }
+
+ /**
+ * Assumes that changes are sorted by issue key and date desc
+ */
+ public void handle(IssueChangeDto dto, FieldDiffs fieldDiffs) {
+ if (currentIssueKey == null || !currentIssueKey.equals(dto.getIssueKey())) {
+ currentIssueKey = dto.getIssueKey();
+ statusChangeCount = 0;
+ }
+ if (fieldDiffs.get(STATUS) != null) {
+ statusChangeCount++;
+ if (statusChangeCount > NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP) {
+ issueChangesToDeleteRepository.add(dto.getUuid());
+ }
+ }
+ }
+ }
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepository.java
new file mode 100644
index 00000000000..f3ad70f64a4
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepository.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.HashSet;
+import java.util.Set;
+
+import static java.util.Collections.unmodifiableSet;
+
+public class IssueChangesToDeleteRepository {
+ private final Set<String> uuids = new HashSet<>();
+
+ public void add(String uuid) {
+ uuids.add(uuid);
+ }
+
+ public Set<String> getUuids() {
+ return unmodifiableSet(uuids);
+ }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestSourceBranchMerger.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestSourceBranchMerger.java
index c8215601656..ab621aad156 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestSourceBranchMerger.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/PullRequestSourceBranchMerger.java
@@ -32,8 +32,7 @@ public class PullRequestSourceBranchMerger {
private final IssueLifecycle issueLifecycle;
private final TrackerSourceBranchInputFactory sourceBranchInputFactory;
- public PullRequestSourceBranchMerger(Tracker<DefaultIssue, DefaultIssue> tracker, IssueLifecycle issueLifecycle,
- TrackerSourceBranchInputFactory sourceBranchInputFactory) {
+ public PullRequestSourceBranchMerger(Tracker<DefaultIssue, DefaultIssue> tracker, IssueLifecycle issueLifecycle, TrackerSourceBranchInputFactory sourceBranchInputFactory) {
this.tracker = tracker;
this.issueLifecycle = issueLifecycle;
this.sourceBranchInputFactory = sourceBranchInputFactory;
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStep.java
new file mode 100644
index 00000000000..8cbdafa2435
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStep.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.step;
+
+import java.util.Set;
+import org.sonar.ce.task.projectanalysis.issue.IssueChangesToDeleteRepository;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+
+public class CleanIssueChangesStep implements ComputationStep {
+ private final IssueChangesToDeleteRepository issueChangesToDeleteRepository;
+ private final DbClient dbClient;
+
+ public CleanIssueChangesStep(IssueChangesToDeleteRepository issueChangesToDeleteRepository, DbClient dbClient) {
+ this.issueChangesToDeleteRepository = issueChangesToDeleteRepository;
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void execute(Context context) {
+ Set<String> uuids = issueChangesToDeleteRepository.getUuids();
+ context.getStatistics().add("changes", uuids.size());
+
+ if (uuids.isEmpty()) {
+ return;
+ }
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+
+ dbClient.issueChangeDao().deleteByUuids(dbSession, issueChangesToDeleteRepository.getUuids());
+ dbSession.commit();
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return "Delete issue changes";
+ }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
index b7f17b328b8..de5442922b0 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
@@ -96,6 +96,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
PersistDuplicationDataStep.class,
PersistAdHocRulesStep.class,
PersistIssuesStep.class,
+ CleanIssueChangesStep.class,
PersistProjectLinksStep.class,
PersistEventsStep.class,
PersistFileSourcesStep.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java
index 193433a31e3..97c05b04114 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java
@@ -19,7 +19,6 @@
*/
package org.sonar.ce.task.projectanalysis.issue;
-import com.google.common.collect.ImmutableList;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
@@ -29,7 +28,9 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;
+import java.util.function.Consumer;
import java.util.stream.IntStream;
+import java.util.stream.LongStream;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
@@ -39,15 +40,18 @@ import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.issue.Issue;
import org.sonar.api.utils.System2;
import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.DefaultIssueComment;
import org.sonar.core.issue.FieldDiffs;
import org.sonar.db.DbClient;
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.rule.RuleDefinitionDto;
import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -57,6 +61,7 @@ import static org.sonar.api.issue.Issue.STATUS_CLOSED;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.utils.DateUtils.addDays;
import static org.sonar.api.utils.DateUtils.parseDateTime;
+import static org.sonar.ce.task.projectanalysis.issue.ComponentIssuesLoader.NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP;
@RunWith(DataProviderRunner.class)
public class ComponentIssuesLoaderTest {
@@ -66,8 +71,9 @@ public class ComponentIssuesLoaderTest {
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);
- private DbClient dbClient = db.getDbClient();
- private System2 system2 = mock(System2.class);
+ private final DbClient dbClient = db.getDbClient();
+ private final System2 system2 = mock(System2.class);
+ private final IssueChangesToDeleteRepository issueChangesToDeleteRepository = new IssueChangesToDeleteRepository();
@Test
public void loadClosedIssues_returns_single_DefaultIssue_by_issue_based_on_first_row() {
@@ -210,8 +216,7 @@ public class ComponentIssuesLoaderTest {
DbClient dbClient = mock(DbClient.class);
Configuration configuration = newConfiguration("0");
String componentUuid = randomAlphabetic(15);
- ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
- null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */, configuration, system2);
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, configuration, system2, issueChangesToDeleteRepository);
assertThat(underTest.loadClosedIssues(componentUuid)).isEmpty();
@@ -219,10 +224,24 @@ public class ComponentIssuesLoaderTest {
}
@Test
+ public void loadLatestDiffChangesForReopeningOfClosedIssues_collects_issue_changes_to_delete() {
+ IssueDto issue = db.issues().insert();
+ for (long i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 5; i++) {
+ db.issues().insertChange(issue, diffIssueChangeModifier(i, "status"));
+ }
+ // should not be deleted
+ db.issues().insertChange(issue, diffIssueChangeModifier(-1, "other"));
+
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
+
+ underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(new DefaultIssue().setKey(issue.getKey())));
+ assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0", "1", "2", "3", "4");
+ }
+
+ @Test
public void loadLatestDiffChangesForReopeningOfClosedIssues_does_not_query_DB_if_issue_list_is_empty() {
DbClient dbClient = mock(DbClient.class);
- ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
- null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
underTest.loadLatestDiffChangesForReopeningOfClosedIssues(emptyList());
@@ -232,18 +251,14 @@ public class ComponentIssuesLoaderTest {
@Test
@UseDataProvider("statusOrResolutionFieldName")
public void loadLatestDiffChangesForReopeningOfClosedIssues_add_diff_change_with_most_recent_status_or_resolution(String statusOrResolutionFieldName) {
- ComponentDto project = db.components().insertPublicProject();
- ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
- RuleDefinitionDto rule = db.rules().insert();
- IssueDto issue = db.issues().insert(rule, project, file);
+ IssueDto issue = db.issues().insert();
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val1")).setIssueChangeCreationDate(5));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val2")).setIssueChangeCreationDate(20));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith(statusOrResolutionFieldName, "val3")).setIssueChangeCreationDate(13));
- ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
- null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
- underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
+ underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
assertThat(defaultIssue.changes())
.hasSize(1);
@@ -255,19 +270,15 @@ public class ComponentIssuesLoaderTest {
@Test
public void loadLatestDiffChangesForReopeningOfClosedIssues_add_single_diff_change_when_most_recent_status_and_resolution_is_the_same_diff() {
- ComponentDto project = db.components().insertPublicProject();
- ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
- RuleDefinitionDto rule = db.rules().insert();
- IssueDto issue = db.issues().insert(rule, project, file);
+ IssueDto issue = db.issues().insert();
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2")).setIssueChangeCreationDate(19));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3", "resolution", "valRes3")).setIssueChangeCreationDate(20));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
- ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
- null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
- underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
+ underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
assertThat(defaultIssue.changes())
.hasSize(1);
@@ -283,19 +294,16 @@ public class ComponentIssuesLoaderTest {
@Test
public void loadLatestDiffChangesForReopeningOfClosedIssues_adds_2_diff_changes_if_most_recent_status_and_resolution_are_not_the_same_diff() {
- ComponentDto project = db.components().insertPublicProject();
- ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
- RuleDefinitionDto rule = db.rules().insert();
- IssueDto issue = db.issues().insert(rule, project, file);
+ IssueDto issue = db.issues().insert();
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus1")).setIssueChangeCreationDate(5));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus2", "resolution", "valRes2")).setIssueChangeCreationDate(19));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("status", "valStatus3")).setIssueChangeCreationDate(20));
db.issues().insertChange(issue, t -> t.setChangeData(randomDiffWith("resolution", "valRes4")).setIssueChangeCreationDate(13));
- ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient,
- null /* not used in method */, null /* not used in method */, newConfiguration("0"), null /* not used by method */);
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null /* not used in method */, null /* not used in method */,
+ newConfiguration("0"), null /* not used by method */, issueChangesToDeleteRepository);
DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
- underTest.loadLatestDiffChangesForReopeningOfClosedIssues(ImmutableList.of(defaultIssue));
+ underTest.loadLatestDiffChangesForReopeningOfClosedIssues(singleton(defaultIssue));
assertThat(defaultIssue.changes())
.hasSize(2);
@@ -309,6 +317,52 @@ public class ComponentIssuesLoaderTest {
.hasSize(1);
}
+ @Test
+ public void loadChanges_should_filter_out_old_status_changes() {
+ IssueDto issue = db.issues().insert();
+ for (int i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1; i++) {
+ db.issues().insertChange(issue, diffIssueChangeModifier(i, "status"));
+ }
+ // these are kept
+ db.issues().insertChange(issue, diffIssueChangeModifier(NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1, "other"));
+ db.issues().insertChange(issue, t -> t
+ .setChangeType(IssueChangeDto.TYPE_COMMENT)
+ .setKey("comment1"));
+
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
+ DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
+ underTest.loadChanges(db.getSession(), singleton(defaultIssue));
+
+ assertThat(defaultIssue.changes())
+ .extracting(d -> d.creationDate().getTime())
+ .containsOnly(LongStream.rangeClosed(1, NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1).boxed().toArray(Long[]::new));
+ assertThat(defaultIssue.defaultIssueComments()).extracting(DefaultIssueComment::key).containsOnly("comment1");
+ assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0");
+ }
+
+ @Test
+ public void loadChanges_should_filter_out_old_from_branch_changes() {
+ IssueDto issue = db.issues().insert();
+ for (int i = 0; i < NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP + 1; i++) {
+ db.issues().insertChange(issue, diffIssueChangeModifier(i, "from_branch"));
+ }
+
+ ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, null, null, newConfiguration("0"), null, issueChangesToDeleteRepository);
+ DefaultIssue defaultIssue = new DefaultIssue().setKey(issue.getKey());
+ underTest.loadChanges(db.getSession(), singleton(defaultIssue));
+ assertThat(defaultIssue.changes())
+ .extracting(d -> d.creationDate().getTime())
+ .containsOnly(LongStream.rangeClosed(1, NUMBER_STATUS_AND_BRANCH_CHANGES_TO_KEEP).boxed().toArray(Long[]::new));
+ assertThat(issueChangesToDeleteRepository.getUuids()).containsOnly("0");
+ }
+
+ private Consumer<IssueChangeDto> diffIssueChangeModifier(long created, String field) {
+ return issueChangeDto -> issueChangeDto
+ .setChangeData(new FieldDiffs().setDiff(field, "A", "B").toEncodedString())
+ .setIssueChangeCreationDate(created)
+ .setUuid(String.valueOf(created));
+ }
+
private static boolean hasValue(@Nullable FieldDiffs.Diff t, String value) {
if (t == null) {
return false;
@@ -371,8 +425,8 @@ public class ComponentIssuesLoaderTest {
}
private ComponentIssuesLoader newComponentIssuesLoader(Configuration configuration) {
- return new ComponentIssuesLoader(dbClient,
- null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */, configuration, system2);
+ return new ComponentIssuesLoader(dbClient, null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */,
+ configuration, system2, issueChangesToDeleteRepository);
}
private static Configuration newEmptySettings() {
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
index a2f8d33ec5b..986f6d1f9af 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
@@ -131,7 +131,7 @@ public class IntegrateIssuesVisitorTest {
private ArgumentCaptor<DefaultIssue> defaultIssueCaptor;
private final ComponentIssuesLoader issuesLoader = new ComponentIssuesLoader(dbTester.getDbClient(), ruleRepositoryRule, activeRulesHolderRule, new MapSettings().asConfig(),
- System2.INSTANCE);
+ System2.INSTANCE, mock(IssueChangesToDeleteRepository.class));
private IssueTrackingDelegator trackingDelegator;
private TrackerExecution tracker;
private PullRequestTrackerExecution prBranchTracker;
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepositoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepositoryTest.java
new file mode 100644
index 00000000000..f98ad57b909
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueChangesToDeleteRepositoryTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueChangesToDeleteRepositoryTest {
+ private final IssueChangesToDeleteRepository repository = new IssueChangesToDeleteRepository();
+
+ @Test
+ public void get_returns_all_issues_added() {
+ repository.add("a");
+ repository.add("b");
+ assertThat(repository.getUuids()).containsOnly("a", "b");
+ }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ProjectTrackerBaseLazyInputTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ProjectTrackerBaseLazyInputTest.java
index 4ea39c07074..9b93522b552 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ProjectTrackerBaseLazyInputTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ProjectTrackerBaseLazyInputTest.java
@@ -67,7 +67,7 @@ public class ProjectTrackerBaseLazyInputTest {
private RuleDefinitionDto rule;
private ComponentDto rootProjectDto;
private ComponentIssuesLoader issuesLoader = new ComponentIssuesLoader(dbTester.getDbClient(), ruleRepositoryRule, activeRulesHolderRule, new MapSettings().asConfig(),
- System2.INSTANCE);
+ System2.INSTANCE, mock(IssueChangesToDeleteRepository.class));
private ReportModulesPath reportModulesPath;
@Before
@@ -146,7 +146,7 @@ public class ProjectTrackerBaseLazyInputTest {
}
@Test
- public void empty_path_if_module_missing_in_report_and_db_and_for_slash_folder () {
+ public void empty_path_if_module_missing_in_report_and_db_and_for_slash_folder() {
ComponentDto module = dbTester.components().insertComponent(newModuleDto(rootProjectDto).setPath(null));
when(reportModulesPath.get()).thenReturn(Collections.emptyMap());
ComponentDto folder = dbTester.components().insertComponent(newDirectory(module, "/"));
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/SiblingsIssueMergerTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/SiblingsIssueMergerTest.java
index 6c7b7202a65..776b24352f0 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/SiblingsIssueMergerTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/SiblingsIssueMergerTest.java
@@ -82,9 +82,9 @@ public class SiblingsIssueMergerTest {
private static final org.sonar.ce.task.projectanalysis.component.Component FILE_1 = builder(
org.sonar.ce.task.projectanalysis.component.Component.Type.FILE, FILE_1_REF)
- .setKey(FILE_1_KEY)
- .setUuid(FILE_1_UUID)
- .build();
+ .setKey(FILE_1_KEY)
+ .setUuid(FILE_1_UUID)
+ .build();
private final SimpleTracker<DefaultIssue, SiblingIssue> tracker = new SimpleTracker<>();
private SiblingsIssueMerger copier;
@@ -100,7 +100,8 @@ public class SiblingsIssueMergerTest {
@Before
public void setUp() {
DbClient dbClient = db.getDbClient();
- ComponentIssuesLoader componentIssuesLoader = new ComponentIssuesLoader(dbClient, null, null, new MapSettings().asConfig(), System2.INSTANCE);
+ ComponentIssuesLoader componentIssuesLoader = new ComponentIssuesLoader(dbClient, null, null, new MapSettings().asConfig(), System2.INSTANCE,
+ mock(IssueChangesToDeleteRepository.class));
copier = new SiblingsIssueMerger(new SiblingsIssuesLoader(new SiblingComponentsWithOpenIssues(treeRootHolder, metadataHolder, dbClient), dbClient, componentIssuesLoader),
tracker,
issueLifecycle);
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStepTest.java
new file mode 100644
index 00000000000..3a00fb972c1
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/CleanIssueChangesStepTest.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.step;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.ce.task.projectanalysis.issue.IssueChangesToDeleteRepository;
+import org.sonar.ce.task.step.TestComputationStepContext;
+import org.sonar.db.DbTester;
+import org.sonar.db.issue.IssueChangeDto;
+import org.sonar.db.issue.IssueDto;
+
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CleanIssueChangesStepTest {
+ @Rule
+ public DbTester db = DbTester.create();
+ private final IssueChangesToDeleteRepository repository = new IssueChangesToDeleteRepository();
+ private final CleanIssueChangesStep cleanIssueChangesStep = new CleanIssueChangesStep(repository, db.getDbClient());
+ private final TestComputationStepContext context = new TestComputationStepContext();
+
+ @Test
+ public void steps_deletes_all_changes_in_repository() {
+ IssueDto issue1 = db.issues().insert();
+ IssueChangeDto change1 = db.issues().insertChange(issue1);
+ IssueChangeDto change2 = db.issues().insertChange(issue1);
+
+ repository.add(change1.getUuid());
+
+ cleanIssueChangesStep.execute(context);
+ assertThat(db.getDbClient().issueChangeDao().selectByIssueKeys(db.getSession(), singleton(issue1.getKey())))
+ .extracting(IssueChangeDto::getUuid)
+ .containsOnly(change2.getUuid());
+ }
+
+ @Test
+ public void steps_does_nothing_if_no_uuid() {
+ IssueDto issue1 = db.issues().insert();
+ IssueChangeDto change1 = db.issues().insertChange(issue1);
+ IssueChangeDto change2 = db.issues().insertChange(issue1);
+
+ cleanIssueChangesStep.execute(context);
+
+ assertThat(db.getDbClient().issueChangeDao().selectByIssueKeys(db.getSession(), singleton(issue1.getKey())))
+ .extracting(IssueChangeDto::getUuid)
+ .containsOnly(change1.getUuid(), change2.getUuid());
+ }
+}