Browse Source

SONAR-12026 Migrate existing hotspots statuses

tags/7.8
Julien Lancelot 5 years ago
parent
commit
09ab6b574f

+ 3
- 0
server/sonar-db-core/src/test/java/org/sonar/db/AbstractDbTester.java View File

@@ -244,6 +244,9 @@ public class AbstractDbTester<T extends CoreTestDb> extends ExternalResource {
} else if (value instanceof Integer) {
// To be consistent, all INTEGER types are mapped as Long
value = ((Integer) value).longValue();
} else if (value instanceof Byte) {
Byte byteValue = (Byte) value;
value = byteValue.intValue();
} else if (value instanceof Timestamp) {
value = new Date(((Timestamp) value).getTime());
}

+ 2
- 1
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v78/DbVersion78.java View File

@@ -31,6 +31,7 @@ public class DbVersion78 implements DbVersion {
.add(2701, "Add index to org_qprofile.parent_uuid", AddIndexToOrgQProfileParentUuid.class)
.add(2702, "Add column webhooks.secret", AddWebhooksSecret.class)
.add(2703, "Add security fields to Elasticsearch indices", AddSecurityFieldsToElasticsearchIndices.class)
.add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class);
.add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class)
.add(2707, "Update statuses of Security Hotspots", UpdateSecurityHotspotsStatuses.class);
}
}

+ 242
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v78/UpdateSecurityHotspotsStatuses.java View File

@@ -0,0 +1,242 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.platform.db.migration.version.v78;

import com.google.common.collect.Maps;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.Map;
import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.config.Configuration;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.SupportsBlueGreen;
import org.sonar.server.platform.db.migration.es.MigrationEsClient;
import org.sonar.server.platform.db.migration.step.DataChange;
import org.sonar.server.platform.db.migration.step.MassUpdate;

@SupportsBlueGreen
public class UpdateSecurityHotspotsStatuses extends DataChange {

private static final String RESOLUTION_FIXED = "FIXED";
private static final String RESOLUTION_WONT_FIX = "WONTFIX";

private static final String STATUS_OPEN = "OPEN";
private static final String STATUS_REOPENED = "REOPENED";
private static final String STATUS_RESOLVED = "RESOLVED";

private static final String STATUS_TO_REVIEW = "TOREVIEW";
private static final String STATUS_IN_REVIEW = "INREVIEW";
private static final String STATUS_REVIEWED = "REVIEWED";

private static final int RULE_TYPE_SECURITY_HOTSPOT = 4;

private final Configuration configuration;
private final System2 system2;
private final MigrationEsClient esClient;
private final UuidFactory uuidFactory;

public UpdateSecurityHotspotsStatuses(Database db, Configuration configuration, System2 system2, MigrationEsClient esClient, UuidFactory uuidFactory) {
super(db);
this.configuration = configuration;
this.system2 = system2;
this.esClient = esClient;
this.uuidFactory = uuidFactory;
}

@Override
public void execute(Context context) throws SQLException {
if (configuration.getBoolean("sonar.sonarcloud.enabled").orElse(false)) {
return;
}
long now = system2.now();
MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("security hotspots");
massUpdate.select("select i.kee, i.status, i.resolution, i.issue_type from issues i " +
"inner join rules r on r.id = i.rule_id and r.rule_type = ? " +
"where (i.resolution is null or i.resolution in (?, ?)) and i.issue_type=? " +
// Add status check for the re-entrance, in order to not reload already migrated issues
"and i.status not in (?, ?, ?)")
.setInt(1, RULE_TYPE_SECURITY_HOTSPOT)
.setString(2, RESOLUTION_FIXED)
.setString(3, RESOLUTION_WONT_FIX)
.setInt(4, RULE_TYPE_SECURITY_HOTSPOT)
.setString(5, STATUS_TO_REVIEW)
.setString(6, STATUS_IN_REVIEW)
.setString(7, STATUS_REVIEWED);
massUpdate.update("update issues set status=?, resolution=?, updated_at=? where kee=? ");
massUpdate.update("insert into issue_changes (kee, issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date) values (?, ?, 'diff', ?, ?, ?, ?)");
massUpdate.execute((row, update, updateIndex) -> {
String issueKey = row.getString(1);
String status = row.getString(2);
String resolution = row.getNullableString(3);

IssueUpdate issueUpdate = new IssueUpdate(status, resolution);
FieldDiffs fieldDiffs = issueUpdate.process();
if (!issueUpdate.isUpdated()) {
return false;
}
if (updateIndex == 0) {
update.setString(1, issueUpdate.getNewStatus());
update.setString(2, issueUpdate.getNewResolution());
update.setLong(3, now);
update.setString(4, issueKey);
return true;
} else {
// No changelog on OPEN issue as there was no previous state
if (!status.equals(STATUS_OPEN)) {
update.setString(1, uuidFactory.create());
update.setString(2, issueKey);
update.setString(3, fieldDiffs.toEncodedString());
update.setLong(4, now);
update.setLong(5, now);
update.setLong(6, now);
return true;
}
return false;
}
});
esClient.deleteIndexes("issues");
}

private static class IssueUpdate {

private static final String RESOLUTION_FIELD = "resolution";
private static final String STATUS_FIELD = "status";

private final String status;
private final String resolution;

private String newStatus;
private String newResolution;
private boolean updated;

IssueUpdate(String status, @CheckForNull String resolution) {
this.status = status;
this.resolution = resolution;
}

FieldDiffs process() {
if ((status.equals(STATUS_OPEN) || (status.equals(STATUS_REOPENED))) && resolution == null) {
newStatus = STATUS_TO_REVIEW;
newResolution = null;
updated = true;
} else if (status.equals(STATUS_RESOLVED) && resolution != null) {
if (resolution.equals(RESOLUTION_FIXED)) {
newStatus = STATUS_IN_REVIEW;
newResolution = null;
updated = true;
} else if (resolution.equals(RESOLUTION_WONT_FIX)) {
newStatus = STATUS_REVIEWED;
newResolution = RESOLUTION_FIXED;
updated = true;
}
}
FieldDiffs fieldDiffs = new FieldDiffs();
fieldDiffs.setDiff(STATUS_FIELD, status, newStatus);
fieldDiffs.setDiff(RESOLUTION_FIELD, resolution, newResolution);
return fieldDiffs;
}

String getNewStatus() {
return newStatus;
}

String getNewResolution() {
return newResolution;
}

boolean isUpdated() {
return updated;
}
}

/**
* Inspired and simplified from {@link org.sonar.core.issue.FieldDiffs}
*/
static class FieldDiffs implements Serializable {

private final Map<String, Diff> diffs = Maps.newLinkedHashMap();

void setDiff(String field, @Nullable String oldValue, @Nullable String newValue) {
diffs.put(field, new Diff(oldValue, newValue));
}

String toEncodedString() {
StringBuilder sb = new StringBuilder();
boolean notFirst = false;
for (Map.Entry<String, FieldDiffs.Diff> entry : diffs.entrySet()) {
if (notFirst) {
sb.append(',');
} else {
notFirst = true;
}
sb.append(entry.getKey());
sb.append('=');
sb.append(entry.getValue().toEncodedString());
}
return sb.toString();
}

static class Diff implements Serializable {
private String oldValue;
private String newValue;

Diff(@Nullable String oldValue, @Nullable String newValue) {
this.oldValue = oldValue;
this.newValue = newValue;
}

private String toEncodedString() {
StringBuilder sb = new StringBuilder();
if (oldValue != null) {
sb.append(oldValue);
sb.append('|');
}
if (newValue != null) {
sb.append(newValue);
}
return sb.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FieldDiffs.Diff diff = (FieldDiffs.Diff) o;
return Objects.equals(oldValue, diff.oldValue) &&
Objects.equals(newValue, diff.newValue);
}

@Override
public int hashCode() {
return Objects.hash(oldValue, newValue);
}
}

}

}

+ 1
- 1
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v78/DbVersion78Test.java View File

@@ -35,7 +35,7 @@ public class DbVersion78Test {

@Test
public void verify_migration_count() {
verifyMigrationCount(underTest, 5);
verifyMigrationCount(underTest, 6);
}

}

+ 233
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v78/UpdateSecurityHotspotsStatusesTest.java View File

@@ -0,0 +1,233 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.platform.db.migration.version.v78;

import java.sql.SQLException;
import javax.annotation.Nullable;
import org.apache.commons.lang.math.RandomUtils;
import org.assertj.core.groups.Tuple;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.utils.internal.TestSystem2;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.CoreDbTester;
import org.sonar.server.platform.db.migration.es.MigrationEsClient;
import org.sonar.server.platform.db.migration.step.DataChange;

import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class UpdateSecurityHotspotsStatusesTest {

private static final long PAST = 5_000_000_000L;
private static final long NOW = 10_000_000_000L;

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(UpdateSecurityHotspotsStatusesTest.class, "schema.sql");

private MapSettings settings = new MapSettings();
private TestSystem2 system2 = new TestSystem2().setNow(NOW);
private MigrationEsClient esClient = mock(MigrationEsClient.class);

private DataChange underTest = new UpdateSecurityHotspotsStatuses(db.database(), settings.asConfig(), system2, esClient, UuidFactoryFast.getInstance());

@Test
public void migrate_open_and_reopen_hotspots() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("OPEN", null, 4, rule);
String issue2 = insertIssue("REOPENED", null, 4, rule);
// Other type of issues should not be updated
String issue3 = insertIssue("OPEN", null, 1, rule);
String issue4 = insertIssue("REOPENED", null, 2, rule);
String issue5 = insertIssue("OPEN", null, 3, rule);

underTest.execute();

assertIssues(
tuple(issue1, "TOREVIEW", null, 4, NOW),
tuple(issue2, "TOREVIEW", null, 4, NOW),
// Not updated
tuple(issue3, "OPEN", null, 1, PAST),
tuple(issue4, "REOPENED", null, 2, PAST),
tuple(issue5, "OPEN", null, 3, PAST));
}

@Test
public void migrate_resolved_as_fixed_and_wont_fix_hotspots() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("RESOLVED", "FIXED", 4, rule);
String issue2 = insertIssue("RESOLVED", "WONTFIX", 4, rule);
// Other type of issues should not be updated
String issue3 = insertIssue("RESOLVED", "FIXED", 1, rule);
String issue4 = insertIssue("RESOLVED", "WONTFIX", 2, rule);
String issue5 = insertIssue("RESOLVED", "WONTFIX", 3, rule);

underTest.execute();

assertIssues(
tuple(issue1, "INREVIEW", null, 4, NOW),
tuple(issue2, "REVIEWED", "FIXED", 4, NOW),
// Not updated
tuple(issue3, "RESOLVED", "FIXED", 1, PAST),
tuple(issue4, "RESOLVED", "WONTFIX", 2, PAST),
tuple(issue5, "RESOLVED", "WONTFIX", 3, PAST));
}

@Test
public void insert_issue_changes() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("REOPENED", null, 4, rule);
// No changelog on OPEN issue as there was no previous state
String issue2 = insertIssue("OPEN", null, 4, rule);
String issue3 = insertIssue("RESOLVED", "FIXED", 4, rule);
String issue4 = insertIssue("RESOLVED", "WONTFIX", 4, rule);

underTest.execute();

assertIssueChanges(
tuple(issue1, "diff", "status=REOPENED|TOREVIEW,resolution=", NOW, NOW, NOW),
tuple(issue3, "diff", "status=RESOLVED|INREVIEW,resolution=FIXED|", NOW, NOW, NOW),
tuple(issue4, "diff", "status=RESOLVED|REVIEWED,resolution=WONTFIX|FIXED", NOW, NOW, NOW));
}

@Test
public void do_not_update_vulnerabilities_coming_from_hotspot() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("OPEN", null, 3, rule);

underTest.execute();

assertIssues(tuple(issue1, "OPEN", null, 3, PAST));
assertNoIssueChanges();
}

@Test
public void do_not_update_closed_hotspots() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("CLOSED", "FIXED", 4, rule);
String issue2 = insertIssue("CLOSED", "REMOVED", 4, rule);

underTest.execute();

assertIssues(
tuple(issue1, "CLOSED", "FIXED", 4, PAST),
tuple(issue2, "CLOSED", "REMOVED", 4, PAST));
assertNoIssueChanges();
}

@Test
public void do_nothing_on_sonarcloud() throws SQLException {
settings.setProperty("sonar.sonarcloud.enabled", "true");
int rule = insertRule(4);
String issue1 = insertIssue("OPEN", null, 4, rule);

underTest.execute();

assertIssues(tuple(issue1, "OPEN", null, 4, PAST));
assertNoIssueChanges();
}

@Test
public void migration_is_reentrant() throws SQLException {
int rule = insertRule(4);
String issue1 = insertIssue("OPEN", null, 4, rule);
String issue2 = insertIssue("REOPENED", null, 4, rule);

underTest.execute();
assertIssues(
tuple(issue1, "TOREVIEW", null, 4, NOW),
tuple(issue2, "TOREVIEW", null, 4, NOW));

// Set a new date for NOW in order to check that issues has not been updated again
system2.setNow(NOW + 1_000_000_000L);
underTest.execute();
assertIssues(
tuple(issue1, "TOREVIEW", null, 4, NOW),
tuple(issue2, "TOREVIEW", null, 4, NOW));
}

@Test
public void issues_index_is_removed() throws SQLException {
underTest.execute();

verify(esClient).deleteIndexes("issues");
}

private void assertIssues(Tuple... expectedTuples) {
assertThat(db.select("SELECT kee, status, resolution, issue_type, updated_at FROM issues")
.stream()
.map(map -> new Tuple(map.get("KEE"), map.get("STATUS"), map.get("RESOLUTION"), map.get("ISSUE_TYPE"), map.get("UPDATED_AT")))
.collect(toList()))
.containsExactlyInAnyOrder(expectedTuples);
}

private void assertNoIssueChanges() {
assertThat(db.countRowsOfTable("issue_changes")).isZero();
}

private void assertIssueChanges(Tuple... expectedTuples) {
assertThat(db.select("SELECT issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date FROM issue_changes")
.stream()
.map(map -> new Tuple(map.get("ISSUE_KEY"), map.get("CHANGE_TYPE"), map.get("CHANGE_DATA"), map.get("CREATED_AT"), map.get("UPDATED_AT"),
map.get("ISSUE_CHANGE_CREATION_DATE")))
.collect(toList()))
.containsExactlyInAnyOrder(expectedTuples);
}

private String insertIssue(String status, @Nullable String resolution, int issueType, int ruleId) {
String issueKey = randomAlphabetic(3);
db.executeInsert(
"ISSUES",
"KEE", issueKey,
"STATUS", status,
"RESOLUTION", resolution,
"RULE_ID", ruleId,
"ISSUE_TYPE", issueType,
"COMPONENT_UUID", randomAlphanumeric(10),
"PROJECT_UUID", randomAlphanumeric(10),
"MANUAL_SEVERITY", false,
"CREATED_AT", PAST,
"UPDATED_AT", PAST);
return issueKey;
}

private int insertRule(int ruleType) {
int id = RandomUtils.nextInt();
db.executeInsert("RULES",
"ID", id,
"RULE_TYPE", ruleType,
"IS_EXTERNAL", false,
"PLUGIN_RULE_KEY", randomAlphanumeric(3),
"PLUGIN_NAME", randomAlphanumeric(3),
"SCOPE", "MAIN",
"IS_AD_HOC", false,
"CREATED_AT", PAST,
"UPDATED_AT", PAST);
return id;
}

}

+ 81
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v78/UpdateSecurityHotspotsStatusesTest/schema.sql View File

@@ -0,0 +1,81 @@
CREATE TABLE "ISSUES" (
"ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"KEE" VARCHAR(50) UNIQUE NOT NULL,
"COMPONENT_UUID" VARCHAR(50),
"PROJECT_UUID" VARCHAR(50),
"RULE_ID" INTEGER,
"SEVERITY" VARCHAR(10),
"MANUAL_SEVERITY" BOOLEAN NOT NULL,
"MESSAGE" VARCHAR(4000),
"LINE" INTEGER,
"GAP" DOUBLE,
"EFFORT" INTEGER,
"STATUS" VARCHAR(20),
"RESOLUTION" VARCHAR(20),
"CHECKSUM" VARCHAR(1000),
"REPORTER" VARCHAR(255),
"ASSIGNEE" VARCHAR(255),
"AUTHOR_LOGIN" VARCHAR(255),
"ACTION_PLAN_KEY" VARCHAR(50) NULL,
"ISSUE_ATTRIBUTES" VARCHAR(4000),
"TAGS" VARCHAR(4000),
"ISSUE_CREATION_DATE" BIGINT,
"ISSUE_CLOSE_DATE" BIGINT,
"ISSUE_UPDATE_DATE" BIGINT,
"CREATED_AT" BIGINT,
"UPDATED_AT" BIGINT,
"LOCATIONS" BLOB,
"ISSUE_TYPE" TINYINT,
"FROM_HOTSPOT" BOOLEAN NULL
);
CREATE UNIQUE INDEX "ISSUES_KEE" ON "ISSUES" ("KEE");
CREATE INDEX "ISSUES_COMPONENT_UUID" ON "ISSUES" ("COMPONENT_UUID");
CREATE INDEX "ISSUES_PROJECT_UUID" ON "ISSUES" ("PROJECT_UUID");
CREATE INDEX "ISSUES_RULE_ID" ON "ISSUES" ("RULE_ID");
CREATE INDEX "ISSUES_RESOLUTION" ON "ISSUES" ("RESOLUTION");
CREATE INDEX "ISSUES_ASSIGNEE" ON "ISSUES" ("ASSIGNEE");
CREATE INDEX "ISSUES_CREATION_DATE" ON "ISSUES" ("ISSUE_CREATION_DATE");
CREATE INDEX "ISSUES_UPDATED_AT" ON "ISSUES" ("UPDATED_AT");

CREATE TABLE "ISSUE_CHANGES" (
"ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"KEE" VARCHAR(50),
"ISSUE_KEY" VARCHAR(50) NOT NULL,
"USER_LOGIN" VARCHAR(255),
"CHANGE_TYPE" VARCHAR(40),
"CHANGE_DATA" VARCHAR(16777215),
"CREATED_AT" BIGINT,
"UPDATED_AT" BIGINT,
"ISSUE_CHANGE_CREATION_DATE" BIGINT
);
CREATE INDEX "ISSUE_CHANGES_KEE" ON "ISSUE_CHANGES" ("KEE");
CREATE INDEX "ISSUE_CHANGES_ISSUE_KEY" ON "ISSUE_CHANGES" ("ISSUE_KEY");

CREATE TABLE "RULES" (
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
"PLUGIN_KEY" VARCHAR(200),
"PLUGIN_RULE_KEY" VARCHAR(200) NOT NULL,
"PLUGIN_NAME" VARCHAR(255) NOT NULL,
"DESCRIPTION" VARCHAR(16777215),
"DESCRIPTION_FORMAT" VARCHAR(20),
"PRIORITY" INTEGER,
"IS_TEMPLATE" BOOLEAN DEFAULT FALSE,
"IS_EXTERNAL" BOOLEAN NOT NULL,
"IS_AD_HOC" BOOLEAN NOT NULL,
"TEMPLATE_ID" INTEGER,
"PLUGIN_CONFIG_KEY" VARCHAR(200),
"NAME" VARCHAR(200),
"STATUS" VARCHAR(40),
"LANGUAGE" VARCHAR(20),
"SCOPE" VARCHAR(20) NOT NULL,
"DEF_REMEDIATION_FUNCTION" VARCHAR(20),
"DEF_REMEDIATION_GAP_MULT" VARCHAR(20),
"DEF_REMEDIATION_BASE_EFFORT" VARCHAR(20),
"GAP_DESCRIPTION" VARCHAR(4000),
"SYSTEM_TAGS" VARCHAR(4000),
"SECURITY_STANDARDS" VARCHAR(4000),
"RULE_TYPE" TINYINT,
"CREATED_AT" BIGINT,
"UPDATED_AT" BIGINT
);
CREATE UNIQUE INDEX "RULES_REPO_KEY" ON "RULES" ("PLUGIN_NAME", "PLUGIN_RULE_KEY");

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/issue/WebIssueStorageTest.java View File

@@ -204,7 +204,7 @@ public class WebIssueStorageTest {
.containsEntry("COMPONENT_UUID", issue.componentUuid())
.containsEntry("EFFORT", updated.effortInMinutes())
.containsEntry("ISSUE_ATTRIBUTES", "fox=bax")
.containsEntry("ISSUE_TYPE", (byte) 3)
.containsEntry("ISSUE_TYPE", 3)
.containsEntry("KEE", issue.key())
.containsEntry("LINE", (long) updated.line())
.containsEntry("PROJECT_UUID", updated.projectUuid())

Loading…
Cancel
Save