return mapper(dbSession).updateMainBranchName(projectUuid, newBranchKey, now);
}
+ public int updateExcludeFromPurge(DbSession dbSession, String branchUuid, boolean excludeFromPurge) {
+ long now = system2.now();
+ return mapper(dbSession).updateExcludeFromPurge(branchUuid, excludeFromPurge, now);
+ }
+
public Optional<BranchDto> selectByBranchKey(DbSession dbSession, String projectUuid, String key) {
return selectByKey(dbSession, projectUuid, key, KeyType.BRANCH);
}
@Nullable
private byte[] pullRequestBinary;
+ private boolean excludeFromPurge;
+
public String getUuid() {
return uuid;
}
return decodePullRequestData(pullRequestBinary);
}
+ public boolean isExcludeFromPurge() {
+ return excludeFromPurge;
+ }
+
+ public void setExcludeFromPurge(boolean excludeFromPurge) {
+ this.excludeFromPurge = excludeFromPurge;
+ }
+
private static byte[] encodePullRequestData(DbProjectBranches.PullRequestData pullRequestData) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
@Override
public String toString() {
- return new StringBuilder("BranchDto{")
- .append("uuid='").append(uuid).append('\'')
- .append(", projectUuid='").append(projectUuid).append('\'')
- .append(", kee='").append(kee).append('\'')
- .append(", keyType=").append(keyType)
- .append(", branchType=").append(branchType)
- .append(", mergeBranchUuid='").append(mergeBranchUuid).append('\'')
- .append('}')
- .toString();
+ return "BranchDto{" +
+ "uuid='" + uuid + '\'' +
+ ", projectUuid='" + projectUuid + '\'' +
+ ", kee='" + kee + '\'' +
+ ", keyType=" + keyType +
+ ", branchType=" + branchType +
+ ", mergeBranchUuid='" + mergeBranchUuid + '\'' +
+ ", excludeFromPurge=" + excludeFromPurge +
+ '}';
}
}
int updateManualBaseline(@Param("uuid") String uuid, @Nullable @Param("analysisUuid") String analysisUuid, @Param("now") long now);
+ int updateExcludeFromPurge(@Param("uuid") String uuid, @Param("excludeFromPurge") boolean excludeFromPurge,
+ @Param("now") long now);
+
BranchDto selectByKey(@Param("projectUuid") String projectUuid, @Param("key") String key, @Param("keyType") KeyType keyType);
BranchDto selectByUuid(@Param("uuid") String uuid);
long countNonMainBranches();
- long countByTypeAndCreationDate(@Param("branchType")String branchType, @Param("sinceDate") long sinceDate);
+ long countByTypeAndCreationDate(@Param("branchType") String branchType, @Param("sinceDate") long sinceDate);
}
private final String projectUuid;
private final Collection<String> scopesWithoutHistoricalData;
private final int maxAgeInDaysOfClosedIssues;
- private final Optional<Integer> maxAgeInDaysOfInactiveShortLivingBranches;
+ private final Optional<Integer> maxAgeInDaysOfInactiveBranches;
private final System2 system2;
private final Set<String> disabledComponentUuids;
public PurgeConfiguration(String rootUuid, String projectUuid, Collection<String> scopesWithoutHistoricalData, int maxAgeInDaysOfClosedIssues,
- Optional<Integer> maxAgeInDaysOfInactiveShortLivingBranches, System2 system2, Set<String> disabledComponentUuids) {
+ Optional<Integer> maxAgeInDaysOfInactiveBranches, System2 system2, Set<String> disabledComponentUuids) {
this.rootUuid = rootUuid;
this.projectUuid = projectUuid;
this.scopesWithoutHistoricalData = scopesWithoutHistoricalData;
this.maxAgeInDaysOfClosedIssues = maxAgeInDaysOfClosedIssues;
this.system2 = system2;
this.disabledComponentUuids = disabledComponentUuids;
- this.maxAgeInDaysOfInactiveShortLivingBranches = maxAgeInDaysOfInactiveShortLivingBranches;
+ this.maxAgeInDaysOfInactiveBranches = maxAgeInDaysOfInactiveBranches;
}
public static PurgeConfiguration newDefaultPurgeConfiguration(Configuration config, String rootUuid, String projectUuid, Set<String> disabledComponentUuids) {
return new PurgeConfiguration(rootUuid, projectUuid, Arrays.asList(Scopes.DIRECTORY, Scopes.FILE), config.getInt(PurgeConstants.DAYS_BEFORE_DELETING_CLOSED_ISSUES).get(),
- config.getInt(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_SHORT_LIVING_BRANCHES), System2.INSTANCE, disabledComponentUuids);
+ config.getInt(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_BRANCHES), System2.INSTANCE, disabledComponentUuids);
}
/**
return maxLiveDateOfClosedIssues(new Date(system2.now()));
}
- public Optional<Date> maxLiveDateOfInactiveShortLivingBranches() {
- return maxAgeInDaysOfInactiveShortLivingBranches.map(age -> DateUtils.addDays(new Date(system2.now()), -age));
+ public Optional<Date> maxLiveDateOfInactiveBranches() {
+ return maxAgeInDaysOfInactiveBranches.map(age -> DateUtils.addDays(new Date(system2.now()), -age));
}
@VisibleForTesting
import org.sonar.db.component.ComponentTreeQuery.Strategy;
import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
import static org.sonar.api.utils.DateUtils.dateToLong;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;
}
private static void purgeStaleBranches(PurgeCommands commands, PurgeConfiguration conf, PurgeMapper mapper, String rootUuid) {
- Optional<Date> maxDate = conf.maxLiveDateOfInactiveShortLivingBranches();
+ Optional<Date> maxDate = conf.maxLiveDateOfInactiveBranches();
if (!maxDate.isPresent()) {
// not available if branch plugin is not installed
return;
}
LOG.debug("<- Purge stale branches");
- List<String> branchUuids = mapper.selectStaleShortLivingBranchesAndPullRequests(conf.projectUuid(), dateToLong(maxDate.get()));
+ Long maxDateValue = ofNullable(dateToLong(maxDate.get())).orElseThrow(IllegalStateException::new);
+ List<String> branchUuids = mapper.selectStaleBranchesAndPullRequests(conf.projectUuid(), maxDateValue);
for (String branchUuid : branchUuids) {
if (!rootUuid.equals(branchUuid)) {
List<String> selectOldClosedIssueKeys(@Param("projectUuid") String projectUuid, @Nullable @Param("toDate") Long toDate);
- List<String> selectStaleShortLivingBranchesAndPullRequests(@Param("projectUuid") String projectUuid, @Param("toDate") Long toDate);
+ List<String> selectStaleBranchesAndPullRequests(@Param("projectUuid") String projectUuid, @Param("toDate") Long toDate);
@CheckForNull
String selectSpecificAnalysisNewCodePeriod(@Param("projectUuid") String projectUuid);
pb.key_type as keyType,
pb.branch_type as branchType,
pb.merge_branch_uuid as mergeBranchUuid,
- pb.pull_request_binary as pullRequestBinary
+ pb.pull_request_binary as pullRequestBinary,
+ pb.exclude_from_purge as excludeFromPurge
</sql>
<insert id="insert" parameterType="map" useGeneratedKeys="false">
merge_branch_uuid,
pull_request_binary,
created_at,
- updated_at
+ updated_at,
+ exclude_from_purge
) values (
#{dto.uuid, jdbcType=VARCHAR},
#{dto.projectUuid, jdbcType=VARCHAR},
#{dto.mergeBranchUuid, jdbcType=VARCHAR},
#{dto.pullRequestBinary, jdbcType=BINARY},
#{now, jdbcType=BIGINT},
- #{now, jdbcType=BIGINT}
+ #{now, jdbcType=BIGINT},
+ #{dto.excludeFromPurge, jdbcType=BOOLEAN}
)
</insert>
uuid = #{projectUuid, jdbcType=VARCHAR}
</update>
+ <update id="updateExcludeFromPurge">
+ update project_branches
+ set
+ exclude_from_purge = #{excludeFromPurge, jdbcType=BOOLEAN},
+ updated_at = #{now, jdbcType=BIGINT}
+ where
+ uuid = #{uuid, jdbcType=VARCHAR}
+ </update>
+
<update id="update" parameterType="map" useGeneratedKeys="false">
update project_branches
set
AND ncp.branch_uuid=#{projectUuid,jdbcType=VARCHAR}
</select>
- <select id="selectStaleShortLivingBranchesAndPullRequests" parameterType="map" resultType="String">
+ <select id="selectStaleBranchesAndPullRequests" parameterType="map" resultType="String">
select
pb.uuid
from
project_branches pb
where
pb.project_uuid=#{projectUuid,jdbcType=VARCHAR}
- and (pb.branch_type='SHORT' or pb.branch_type='PULL_REQUEST')
+ and pb.exclude_from_purge = ${_false}
and pb.updated_at < #{toDate}
</select>
"PULL_REQUEST_BINARY" BLOB,
"MANUAL_BASELINE_ANALYSIS_UUID" VARCHAR(40),
"CREATED_AT" BIGINT NOT NULL,
- "UPDATED_AT" BIGINT NOT NULL
+ "UPDATED_AT" BIGINT NOT NULL,
+ "EXCLUDE_FROM_PURGE" BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE "PROJECT_BRANCHES" ADD CONSTRAINT "PK_PROJECT_BRANCHES" PRIMARY KEY("UUID");
CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE_KEY_TYPE" ON "PROJECT_BRANCHES"("PROJECT_UUID", "KEE", "KEY_TYPE");
assertThat(loaded.getBranchType()).isEqualTo(BranchType.LONG);
}
+ @Test
+ public void updateExcludeFromPurge() {
+ BranchDto dto = new BranchDto();
+ dto.setProjectUuid("U1");
+ dto.setUuid("U1");
+ dto.setBranchType(BranchType.LONG);
+ dto.setKey("feature");
+ dto.setExcludeFromPurge(false);
+ underTest.insert(dbSession, dto);
+
+ underTest.updateExcludeFromPurge(dbSession, "U1", true);
+
+ BranchDto loaded = underTest.selectByBranchKey(dbSession, "U1", "feature").get();
+ assertThat(loaded.isExcludeFromPurge()).isTrue();
+ }
+
@DataProvider
public static Object[][] nullOrEmpty() {
return new Object[][] {
underTest.setKey("K1");
underTest.setBranchType(BranchType.LONG);
underTest.setMergeBranchUuid("U3");
+ underTest.setExcludeFromPurge(true);
assertThat(underTest.toString()).isEqualTo("BranchDto{uuid='U1', " +
- "projectUuid='U2', kee='K1', keyType=null, branchType=LONG, mergeBranchUuid='U3'}");
+ "projectUuid='U2', kee='K1', keyType=null, branchType=LONG, mergeBranchUuid='U3', excludeFromPurge=true}");
}
@Test
@Test
public void should_have_empty_branch_purge_date() {
PurgeConfiguration conf = new PurgeConfiguration("root", "project", emptySet(), 30, Optional.of(10), System2.INSTANCE, emptySet());
- assertThat(conf.maxLiveDateOfInactiveShortLivingBranches()).isNotEmpty();
+ assertThat(conf.maxLiveDateOfInactiveBranches()).isNotEmpty();
long tenDaysAgo = DateUtils.addDays(new Date(System2.INSTANCE.now()), -10).getTime();
- assertThat(conf.maxLiveDateOfInactiveShortLivingBranches().get().getTime()).isBetween(tenDaysAgo - 5000, tenDaysAgo + 5000);
+ assertThat(conf.maxLiveDateOfInactiveBranches().get().getTime()).isBetween(tenDaysAgo - 5000, tenDaysAgo + 5000);
}
@Test
public void should_calculate_branch_purge_date() {
PurgeConfiguration conf = new PurgeConfiguration("root", "project", emptySet(), 30, Optional.empty(), System2.INSTANCE, emptySet());
- assertThat(conf.maxLiveDateOfInactiveShortLivingBranches()).isEmpty();
+ assertThat(conf.maxLiveDateOfInactiveBranches()).isEmpty();
}
@Test
public void purge_failed_ce_tasks() {
ComponentDto project = db.components().insertPrivateProject();
SnapshotDto pastAnalysis = db.components().insertSnapshot(project, t -> t.setStatus(STATUS_PROCESSED).setLast(false));
- SnapshotDto toBeDeletedAnalysis = db.components().insertSnapshot(project, t -> t.setStatus(STATUS_UNPROCESSED).setLast(false));
+ db.components().insertSnapshot(project, t -> t.setStatus(STATUS_UNPROCESSED).setLast(false));
SnapshotDto lastAnalysis = db.components().insertSnapshot(project, t -> t.setStatus(STATUS_PROCESSED).setLast(true));
underTest.purge(dbSession, newConfigurationWith30Days(project.uuid()), PurgeListener.EMPTY, new PurgeProfiler());
}
@Test
- public void purge_inactive_short_living_branches() {
+ public void purge_inactive_branches() {
when(system2.now()).thenReturn(new Date().getTime());
RuleDefinitionDto rule = db.rules().insert();
ComponentDto project = db.components().insertMainBranch();
- ComponentDto longBranch = db.components().insertProjectBranch(project);
- ComponentDto recentShortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.SHORT));
+ ComponentDto branch1 = db.components().insertProjectBranch(project);
+ ComponentDto branch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH));
- // short branch with other components and issues, updated 31 days ago
+ // branch with other components and issues, updated 31 days ago
when(system2.now()).thenReturn(DateUtils.addDays(new Date(), -31).getTime());
- ComponentDto shortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.SHORT));
- ComponentDto module = db.components().insertComponent(newModuleDto(shortBranch));
+ ComponentDto branch3 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH));
+ ComponentDto module = db.components().insertComponent(newModuleDto(branch3));
ComponentDto subModule = db.components().insertComponent(newModuleDto(module));
ComponentDto file = db.components().insertComponent(newFileDto(subModule));
- db.issues().insert(rule, shortBranch, file);
- db.issues().insert(rule, shortBranch, subModule);
- db.issues().insert(rule, shortBranch, module);
+ db.issues().insert(rule, branch3, file);
+ db.issues().insert(rule, branch3, subModule);
+ db.issues().insert(rule, branch3, module);
underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.uuid(), project.uuid()), PurgeListener.EMPTY, new PurgeProfiler());
dbSession.commit();
- assertThat(uuidsIn("projects")).containsOnly(project.uuid(), longBranch.uuid(), recentShortBranch.uuid());
+ assertThat(uuidsIn("projects")).containsOnly(project.uuid(), branch1.uuid(), branch2.uuid());
}
@Test
}
@Test
- public void purge_inactive_SLB_when_analyzing_non_main_branch() {
+ public void purge_inactive_branches_when_analyzing_non_main_branch() {
when(system2.now()).thenReturn(new Date().getTime());
RuleDefinitionDto rule = db.rules().insert();
ComponentDto project = db.components().insertMainBranch();
when(system2.now()).thenReturn(DateUtils.addDays(new Date(), -31).getTime());
- // SLB updated 31 days ago
- ComponentDto slb1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.SHORT));
+ // branch updated 31 days ago
+ ComponentDto branch1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH));
- // SLB with other components and issues, updated 31 days ago
- ComponentDto slb2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
- ComponentDto file = db.components().insertComponent(newFileDto(slb2));
- db.issues().insert(rule, slb2, file);
+ // branch with other components and issues, updated 31 days ago
+ ComponentDto branch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
+ ComponentDto file = db.components().insertComponent(newFileDto(branch2));
+ db.issues().insert(rule, branch2, file);
// back to present
when(system2.now()).thenReturn(new Date().getTime());
- // analysing slb1
- underTest.purge(dbSession, newConfigurationWith30Days(system2, slb1.uuid(), slb1.getMainBranchProjectUuid()), PurgeListener.EMPTY, new PurgeProfiler());
+ // analysing branch1
+ underTest.purge(dbSession, newConfigurationWith30Days(system2, branch1.uuid(), branch1.getMainBranchProjectUuid()), PurgeListener.EMPTY, new PurgeProfiler());
dbSession.commit();
- // slb1 wasn't deleted since it was being analyzed!
- assertThat(uuidsIn("projects")).containsOnly(project.uuid(), longBranch.uuid(), slb1.uuid());
+ // branch1 wasn't deleted since it was being analyzed!
+ assertThat(uuidsIn("projects")).containsOnly(project.uuid(), longBranch.uuid(), branch1.uuid());
}
@Test
.setProjectUuid(project1.uuid())
.setBranchUuid(project1.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
- .setValue(analysis1.getUuid())
- );
+ .setValue(analysis1.getUuid()));
ComponentDto project2 = db.components().insertPrivateProject();
SnapshotDto analysis2 = db.components().insertSnapshot(newSnapshot()
.setComponentUuid(project2.uuid())
.setProjectUuid(project.uuid())
.setBranchUuid(project.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
- .setValue(analysisProject.getUuid())
- );
+ .setValue(analysisProject.getUuid()));
ComponentDto branch1 = db.components().insertProjectBranch(project);
SnapshotDto analysisBranch1 = db.components().insertSnapshot(newSnapshot()
.setComponentUuid(branch1.uuid())
.setProjectUuid(project.uuid())
.setBranchUuid(branch2.uuid())
.setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
- .setValue(analysisBranch2.getUuid())
- );
+ .setValue(analysisBranch2.getUuid()));
dbSession.commit();
assertThat(underTest.selectPurgeableAnalyses(project.uuid(), dbSession))
@Test
public void delete_ce_analysis_older_than_180_and_scanner_context_older_than_40_days_of_project_and_branches_when_purging_project() {
LocalDateTime now = LocalDateTime.now();
- ComponentDto project1 = db.components().insertPublicProject();
- ComponentDto branch1 = db.components().insertProjectBranch(project1);
+ ComponentDto project1 = db.components().insertMainBranch();
+ ComponentDto branch1 = db.components().insertProjectBranch(project1, b -> b.setExcludeFromPurge(true));
Consumer<CeQueueDto> belongsToProject1 = t -> t.setMainComponentUuid(project1.uuid()).setComponentUuid(project1.uuid());
Consumer<CeQueueDto> belongsToBranch1 = t -> t.setMainComponentUuid(project1.uuid()).setComponentUuid(branch1.uuid());
.add(3006, "Create NEW_CODE_PERIOD table", CreateNewCodePeriodTable.class)
.add(3007, "Populate NEW_CODE_PERIOD table", PopulateNewCodePeriodTable.class)
.add(3008, "Remove leak period properties", RemoveLeakPeriodProperties.class)
- .add(3009, "Remove GitHub login generation strategy property", RemoveGitHubLoginGenerationStrategyProperty.class)
- ;
+ .add(3009, "Remove GitHub login generation strategy property", RemoveGitHubLoginGenerationStrategyProperty.class);
}
}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BooleanColumnDef.newBooleanColumnDefBuilder;
+
+@SupportsBlueGreen
+public class AddExcludeBranchFromPurgeColumn extends DdlChange {
+ private static final String TABLE = "project_branches";
+ private static final String NEW_COLUMN = "exclude_from_purge";
+
+ public AddExcludeBranchFromPurgeColumn(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ context.execute(new AddColumnsBuilder(getDialect(), TABLE)
+ .addColumn(newBooleanColumnDefBuilder()
+ .setColumnName(NEW_COLUMN)
+ .setIsNullable(false)
+ .setDefaultValue(false)
+ .build())
+ .build());
+ }
+}
.add(3103, "Migrate GitHub ALM settings from PROPERTIES to ALM_SETTINGS tables", MigrateGithubAlmSettings.class)
.add(3104, "Migrate Bitbucket ALM settings from PROPERTIES to ALM_SETTINGS tables", MigrateBitbucketAlmSettings.class)
.add(3105, "Migrate Azure ALM settings from PROPERTIES to ALM_SETTINGS tables", MigrateAzureAlmSettings.class)
- .add(3106, "Delete 'sonar.pullrequest.provider' property", DeleteSonarPullRequestProviderProperty.class);
+ .add(3106, "Delete 'sonar.pullrequest.provider' property", DeleteSonarPullRequestProviderProperty.class)
+ .add(3107, "Migrate default branches to keep global setting", MigrateDefaultBranchesToKeepSetting.class)
+ .add(3108, "Add EXCLUDE_FROM_PURGE column", AddExcludeBranchFromPurgeColumn.class)
+ .add(3109, "Populate EXCLUDE_FROM_PURGE column", PopulateExcludeBranchFromPurgeColumn.class);
}
}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
+@SupportsBlueGreen
+public class MigrateDefaultBranchesToKeepSetting extends DataChange {
+ private static final Logger LOG = Loggers.get(MigrateDefaultBranchesToKeepSetting.class);
+ private static final String DEPRECATED_KEY = "sonar.branch.longLivedBranches.regex";
+ private static final String NEW_KEY = "sonar.dbcleaner.branchesToKeepWhenInactive";
+
+ private final System2 system;
+
+ public MigrateDefaultBranchesToKeepSetting(Database db, System2 system) {
+ super(db);
+ this.system = system;
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ Long now = system.now();
+ try {
+ Long numberOfNewProps = context.prepareSelect("select count(*) from properties props where props.prop_key = '" + NEW_KEY + "'")
+ .get(row -> row.getLong(1));
+ if (numberOfNewProps != null && numberOfNewProps > 0) {
+ // no need for a migration
+ return;
+ }
+
+ Boolean defaultPropertyOverridden = context.prepareSelect("select count(*) from properties props where props.prop_key = '" + DEPRECATED_KEY + "'")
+ .get(row -> row.getLong(1) > 0);
+ if (FALSE.equals(defaultPropertyOverridden)) {
+ migrateDefaultDeprecatedSettings(context, now);
+ } else {
+ migrateOverriddenDeprecatedSettings(context, now);
+ }
+ } catch (Exception ex) {
+ LOG.error("Failed to migrate to new '{}' setting.", NEW_KEY);
+ throw ex;
+ }
+ }
+
+ private static void migrateDefaultDeprecatedSettings(Context context, Long time) throws SQLException {
+ Boolean anyProjectAlreadyExists = context.prepareSelect("select count(*) from projects").get(row -> row.getLong(1) > 0);
+ String newSettingValue = "master,develop,trunk";
+
+ // if old `sonar.branch.longLivedBranches.regex` setting was at default value but there were already projects analyzed we need to add the
+ // old defaults of
+ // that setting to the defaults of the new `sonar.dbcleaner.branchesToKeepWhenInactive` setting for backward compatibility
+ if (TRUE.equals(anyProjectAlreadyExists)) {
+ newSettingValue = "master,develop,trunk,branch-.*,release-.*";
+ }
+
+ context
+ .prepareUpsert("insert into properties (prop_key, is_empty, text_value, created_at) values (?, ?, ?, ?)")
+ .setString(1, NEW_KEY)
+ .setBoolean(2, false)
+ .setString(3, newSettingValue)
+ .setLong(4, time)
+ .execute()
+ .commit();
+ }
+
+ private static void migrateOverriddenDeprecatedSettings(Context context, Long time) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select("select resource_id, text_value from properties props where props.prop_key = '" + DEPRECATED_KEY + "'");
+ massUpdate.rowPluralName("properties");
+ massUpdate.update("insert into properties (resource_id, prop_key, is_empty, text_value, created_at) values (?, ?, ?, ?, ?)");
+ massUpdate.execute((row, update) -> {
+ update.setLong(1, row.getNullableLong(1));
+ update.setString(2, NEW_KEY);
+ update.setBoolean(3, false);
+ update.setString(4, row.getString(2));
+ update.setLong(5, time);
+ return true;
+ });
+ }
+}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+@SupportsBlueGreen
+public class PopulateExcludeBranchFromPurgeColumn extends DataChange {
+ private final System2 system;
+
+ public PopulateExcludeBranchFromPurgeColumn(Database db, System2 system) {
+ super(db);
+ this.system = system;
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ Long now = system.now();
+ context.prepareUpsert("update project_branches set exclude_from_purge = true, updated_at = ? where branch_type = 'LONG'")
+ .setLong(1, now)
+ .execute()
+ .commit();
+ }
+}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AddExcludeBranchFromPurgeColumnTest {
+
+ private static final String TABLE = "project_branches";
+
+ private static final String PROJECT_1_UUID = "1a724f54-c2a4-4d36-b59c-61026f178613";
+ private static final String PROJECT_2_UUID = "cc69ccb8-434a-489b-abd6-6a80371f64ff";
+
+ private static final String MAIN_BRANCH_1 = "8a9789c8-7aee-4aa6-9cb7-407137935ac3";
+ private static final String LONG_BRANCH_1 = "69fdfc19-491c-4ed9-bcac-ef04e0f6cdc6";
+ private static final String SHORT_BRANCH_1 = "f7a6d247-1790-40f8-8591-a0cc39d05ecb";
+ private static final String PR_1 = "d65466bf-271c-4f9f-ac7a-72add44848c0";
+
+ private static final String MAIN_BRANCH_2 = "cdadf128-7bdb-4589-982d-62445d46ae1b";
+ private static final String LONG_BRANCH_2 = "60bf6fa8-3ecc-4ba6-ad8d-991ea7c7f9cb";
+ private static final String SHORT_BRANCH_2 = "ce5632ed-462e-4384-98b6-1773a7bbfe53";
+ private static final String PR_2 = "5897f7d0-d34f-4558-b9c5-a7eb7a5c4b69";
+
+ @Rule
+ public CoreDbTester dbTester = CoreDbTester.createForSchema(AddExcludeBranchFromPurgeColumnTest.class, "schema.sql");
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AddExcludeBranchFromPurgeColumn underTest = new AddExcludeBranchFromPurgeColumn(dbTester.database());
+
+ @Before
+ public void setup() {
+ insertBranch(MAIN_BRANCH_1, PROJECT_1_UUID, "master", "BRANCH", "LONG");
+ insertBranch(LONG_BRANCH_1, PROJECT_1_UUID, "release-1", "BRANCH", "LONG");
+ insertBranch(SHORT_BRANCH_1, PROJECT_1_UUID, "feature/foo", "BRANCH", "SHORT");
+ insertBranch(PR_1, PROJECT_1_UUID, "feature/feature-1", "PULL_REQUEST", "PULL_REQUEST");
+
+ insertBranch(MAIN_BRANCH_2, PROJECT_2_UUID, "trunk", "BRANCH", "LONG");
+ insertBranch(LONG_BRANCH_2, PROJECT_2_UUID, "branch-1", "BRANCH", "LONG");
+ insertBranch(SHORT_BRANCH_2, PROJECT_2_UUID, "feature/bar", "BRANCH", "SHORT");
+ insertBranch(PR_2, PROJECT_2_UUID, "feature/feature-2", "PULL_REQUEST", "PULL_REQUEST");
+ }
+
+ @Test
+ public void execute_migration() throws SQLException {
+ underTest.execute();
+
+ verifyMigrationResult();
+ }
+
+ private void verifyMigrationResult() {
+ assertThat(dbTester.countSql("select count(*) from " + TABLE + " where exclude_from_purge = true")).isEqualTo(0);
+ assertThat(dbTester.countSql("select count(*) from " + TABLE + " where exclude_from_purge = false")).isEqualTo(8);
+ }
+
+ private void insertBranch(String uuid, String projectUuid, String key, String keyType, String branchType) {
+ dbTester.executeInsert(
+ TABLE,
+ "UUID", uuid,
+ "PROJECT_UUID", projectUuid,
+ "KEE", key,
+ "BRANCH_TYPE", branchType,
+ "KEY_TYPE", keyType,
+ "MERGE_BRANCH_UUID", null,
+ "CREATED_AT", System2.INSTANCE.now(),
+ "UPDATED_AT", System2.INSTANCE.now());
+ }
+}
@Test
public void verify_migration_count() {
- verifyMigrationCount(underTest, 7);
+ verifyMigrationCount(underTest, 10);
}
}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MigrateDefaultBranchesToKeepSettingTest {
+
+ private static final String PROPS_TABLE = "properties";
+ private static final String PATTERN_1 = "(branch|release|llb)-.*";
+ private static final String PATTERN_2 = "(branch|llb)-.*";
+ private static final String PATTERN_3 = "llb-.*";
+
+ @Rule
+ public CoreDbTester dbTester = CoreDbTester.createForSchema(MigrateDefaultBranchesToKeepSettingTest.class, "schema.sql");
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private MigrateDefaultBranchesToKeepSetting underTest = new MigrateDefaultBranchesToKeepSetting(dbTester.database(), System2.INSTANCE);
+
+ @Test
+ public void migrate_overridden_old_setting() throws SQLException {
+ setupOverriddenSetting();
+
+ underTest.execute();
+
+ verifyMigrationOfOverriddenSetting();
+ }
+
+ @Test
+ public void migration_of_overridden_setting_is_re_entrant() throws SQLException {
+ setupOverriddenSetting();
+
+ underTest.execute();
+ underTest.execute();
+
+ verifyMigrationOfOverriddenSetting();
+ }
+
+ @Test
+ public void migrate_default_old_setting_on_fresh_install() throws SQLException {
+ setupDefaultSetting();
+
+ underTest.execute();
+
+ verifyMigrationOfDefaultSetting("master,develop,trunk");
+ }
+
+ @Test
+ public void migrate_default_old_setting_on_existing_install() throws SQLException {
+ setupDefaultSetting();
+ insertProject();
+
+ underTest.execute();
+
+ verifyMigrationOfDefaultSetting("master,develop,trunk,branch-.*,release-.*");
+ }
+
+ @Test
+ public void migration_of_default_old_setting_is_re_entrant() throws SQLException {
+ setupDefaultSetting();
+
+ underTest.execute();
+ underTest.execute();
+
+ verifyMigrationOfDefaultSetting("master,develop,trunk");
+ }
+
+ private void setupOverriddenSetting() {
+ insertProperty(1, "some.key", "some.value", 1001L);
+ insertProperty(2, "sonar.branch.longLivedBranches.regex", PATTERN_1, 1001L);
+ insertProperty(3, "some.other.key", "some.other.value", 1001L);
+ insertProperty(4, "sonar.branch.longLivedBranches.regex", PATTERN_2, 1002L);
+ insertProperty(5, "some.other.key", "some.other.value", null);
+ insertProperty(6, "sonar.branch.longLivedBranches.regex", PATTERN_3, null);
+ }
+
+ private void setupDefaultSetting() {
+ insertProperty(1, "some.key", "some.value", 1001L);
+ insertProperty(3, "some.other.key", "some.other.value", 1001L);
+ insertProperty(5, "some.other.key", "some.other.value", null);
+ }
+
+ private void verifyMigrationOfOverriddenSetting() {
+ assertThat(dbTester.countRowsOfTable(PROPS_TABLE)).isEqualTo(9);
+ assertThat(dbTester.countSql("select count(*) from " + PROPS_TABLE + " where prop_key = 'sonar.branch.longLivedBranches.regex'")).isEqualTo(3);
+ assertThat(dbTester.countSql("select count(*) from " + PROPS_TABLE + " where prop_key = 'sonar.dbcleaner.branchesToKeepWhenInactive'")).isEqualTo(3);
+ assertThat(dbTester.select("select resource_id, text_value from " + PROPS_TABLE + " where prop_key = 'sonar.dbcleaner.branchesToKeepWhenInactive'")
+ .stream()
+ .map(e -> new Tuple(e.get("TEXT_VALUE"), e.get("RESOURCE_ID")))
+ .collect(Collectors.toList()))
+ .containsExactlyInAnyOrder(
+ new Tuple(PATTERN_1, 1001L),
+ new Tuple(PATTERN_2, 1002L),
+ new Tuple(PATTERN_3, null));
+ }
+
+ private void verifyMigrationOfDefaultSetting(String expectedValue) {
+ assertThat(dbTester.countRowsOfTable(PROPS_TABLE)).isEqualTo(4);
+ assertThat(dbTester.countSql("select count(*) from " + PROPS_TABLE + " where prop_key = 'sonar.branch.longLivedBranches.regex'")).isEqualTo(0);
+ assertThat(dbTester.countSql("select count(*) from " + PROPS_TABLE + " where prop_key = 'sonar.dbcleaner.branchesToKeepWhenInactive'")).isEqualTo(1);
+ assertThat(dbTester.select("select resource_id, text_value from " + PROPS_TABLE + " where prop_key = 'sonar.dbcleaner.branchesToKeepWhenInactive'")
+ .stream()
+ .map(e -> new Tuple(e.get("TEXT_VALUE"), e.get("RESOURCE_ID")))
+ .collect(Collectors.toList()))
+ .containsExactly(new Tuple(expectedValue, null));
+ }
+
+ private void insertProperty(int id, String key, String value, @Nullable Long resourceId) {
+ dbTester.executeInsert(
+ PROPS_TABLE,
+ "ID", id,
+ "PROP_KEY", key,
+ "RESOURCE_ID", resourceId,
+ "USER_ID", null,
+ "IS_EMPTY", false,
+ "TEXT_VALUE", value,
+ "CLOB_VALUE", null,
+ "CREATED_AT", System2.INSTANCE.now());
+ }
+
+ private void insertProject() {
+ String uuid = Uuids.createFast();
+ dbTester.executeInsert("PROJECTS",
+ "ORGANIZATION_UUID", "default-org",
+ "KEE", uuid + "-key",
+ "UUID", uuid,
+ "PROJECT_UUID", uuid,
+ "main_branch_project_uuid", uuid,
+ "UUID_PATH", ".",
+ "ROOT_UUID", uuid,
+ "PRIVATE", "true",
+ "qualifier", "TRK",
+ "scope", "PRJ");
+ }
+}
--- /dev/null
+/*
+ * 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.v81;
+
+import java.sql.SQLException;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PopulateExcludeBranchFromPurgeColumnTest {
+
+ private static final String TABLE = "PROJECT_BRANCHES";
+
+ private static final String PROJECT_1_UUID = "1a724f54-c2a4-4d36-b59c-61026f178613";
+ private static final String PROJECT_2_UUID = "cc69ccb8-434a-489b-abd6-6a80371f64ff";
+
+ private static final String MAIN_BRANCH_1 = "8a9789c8-7aee-4aa6-9cb7-407137935ac3";
+ private static final String LONG_BRANCH_1 = "69fdfc19-491c-4ed9-bcac-ef04e0f6cdc6";
+ private static final String SHORT_BRANCH_1 = "f7a6d247-1790-40f8-8591-a0cc39d05ecb";
+ private static final String PR_1 = "d65466bf-271c-4f9f-ac7a-72add44848c0";
+
+ private static final String MAIN_BRANCH_2 = "cdadf128-7bdb-4589-982d-62445d46ae1b";
+ private static final String LONG_BRANCH_2 = "60bf6fa8-3ecc-4ba6-ad8d-991ea7c7f9cb";
+ private static final String SHORT_BRANCH_2 = "ce5632ed-462e-4384-98b6-1773a7bbfe53";
+ private static final String PR_2 = "5897f7d0-d34f-4558-b9c5-a7eb7a5c4b69";
+
+ @Rule
+ public CoreDbTester dbTester = CoreDbTester.createForSchema(PopulateExcludeBranchFromPurgeColumnTest.class, "schema.sql");
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private PopulateExcludeBranchFromPurgeColumn underTest = new PopulateExcludeBranchFromPurgeColumn(dbTester.database(), System2.INSTANCE);
+
+ @Before
+ public void setup() {
+ insertBranch(MAIN_BRANCH_1, PROJECT_1_UUID, "master", "BRANCH", "LONG");
+ insertBranch(LONG_BRANCH_1, PROJECT_1_UUID, "release-1", "BRANCH", "LONG");
+ insertBranch(SHORT_BRANCH_1, PROJECT_1_UUID, "feature/foo", "BRANCH", "SHORT");
+ insertBranch(PR_1, PROJECT_1_UUID, "feature/feature-1", "PULL_REQUEST", "PULL_REQUEST");
+
+ insertBranch(MAIN_BRANCH_2, PROJECT_2_UUID, "trunk", "BRANCH", "LONG");
+ insertBranch(LONG_BRANCH_2, PROJECT_2_UUID, "branch-1", "BRANCH", "LONG");
+ insertBranch(SHORT_BRANCH_2, PROJECT_2_UUID, "feature/bar", "BRANCH", "SHORT");
+ insertBranch(PR_2, PROJECT_2_UUID, "feature/feature-2", "PULL_REQUEST", "PULL_REQUEST");
+ }
+
+ @Test
+ public void execute_migration() throws SQLException {
+ underTest.execute();
+
+ verifyMigrationResult();
+ }
+
+ @Test
+ public void migration_is_re_entrant() throws SQLException {
+ underTest.execute();
+ underTest.execute();
+
+ verifyMigrationResult();
+ }
+
+ private void verifyMigrationResult() {
+ assertThat(dbTester.select("select UUID from " + TABLE + " where EXCLUDE_FROM_PURGE = true")
+ .stream()
+ .map(e -> e.get("UUID"))
+ .collect(Collectors.toList())).containsExactlyInAnyOrder(MAIN_BRANCH_1, LONG_BRANCH_1, MAIN_BRANCH_2, LONG_BRANCH_2);
+ assertThat(dbTester.select("select UUID from " + TABLE + " where EXCLUDE_FROM_PURGE = false")
+ .stream()
+ .map(e -> e.get("UUID"))
+ .collect(Collectors.toList())).containsExactlyInAnyOrder(SHORT_BRANCH_1, SHORT_BRANCH_2, PR_1, PR_2);
+ }
+
+ private void insertBranch(String uuid, String projectUuid, String key, String keyType, String branchType) {
+ dbTester.executeInsert(
+ TABLE,
+ "UUID", uuid,
+ "PROJECT_UUID", projectUuid,
+ "KEE", key,
+ "BRANCH_TYPE", branchType,
+ "KEY_TYPE", keyType,
+ "MERGE_BRANCH_UUID", null,
+ "CREATED_AT", System2.INSTANCE.now(),
+ "UPDATED_AT", System2.INSTANCE.now());
+ }
+}
--- /dev/null
+CREATE TABLE PROJECT_BRANCHES(
+ UUID VARCHAR(50) NOT NULL,
+ PROJECT_UUID VARCHAR(50) NOT NULL,
+ KEE VARCHAR(255) NOT NULL,
+ BRANCH_TYPE VARCHAR(12),
+ MERGE_BRANCH_UUID VARCHAR(50),
+ KEY_TYPE VARCHAR(12) NOT NULL,
+ PULL_REQUEST_BINARY BLOB,
+ MANUAL_BASELINE_ANALYSIS_UUID VARCHAR(40),
+ CREATED_AT BIGINT NOT NULL,
+ UPDATED_AT BIGINT NOT NULL,
+ EXCLUDE_FROM_PURGE BOOLEAN
+);
--- /dev/null
+CREATE TABLE PROJECT_BRANCHES(
+ UUID VARCHAR(50) NOT NULL,
+ PROJECT_UUID VARCHAR(50) NOT NULL,
+ KEE VARCHAR(255) NOT NULL,
+ BRANCH_TYPE VARCHAR(12),
+ MERGE_BRANCH_UUID VARCHAR(50),
+ KEY_TYPE VARCHAR(12) NOT NULL,
+ PULL_REQUEST_BINARY BLOB,
+ MANUAL_BASELINE_ANALYSIS_UUID VARCHAR(40),
+ CREATED_AT BIGINT NOT NULL,
+ UPDATED_AT BIGINT NOT NULL
+);
--- /dev/null
+CREATE TABLE "PROPERTIES" (
+ "ID" INTEGER NOT NULL AUTO_INCREMENT (1,1),
+ "PROP_KEY" VARCHAR(512) NOT NULL,
+ "RESOURCE_ID" INTEGER,
+ "USER_ID" INTEGER,
+ "IS_EMPTY" BOOLEAN NOT NULL,
+ "TEXT_VALUE" VARCHAR(4000),
+ "CLOB_VALUE" CLOB,
+ "CREATED_AT" BIGINT
+);
+CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES" ("PROP_KEY");
--- /dev/null
+CREATE TABLE PROJECT_BRANCHES(
+ UUID VARCHAR(50) NOT NULL,
+ PROJECT_UUID VARCHAR(50) NOT NULL,
+ KEE VARCHAR(255) NOT NULL,
+ BRANCH_TYPE VARCHAR(12),
+ MERGE_BRANCH_UUID VARCHAR(50),
+ KEY_TYPE VARCHAR(12) NOT NULL,
+ PULL_REQUEST_BINARY BLOB,
+ MANUAL_BASELINE_ANALYSIS_UUID VARCHAR(40),
+ CREATED_AT BIGINT NOT NULL,
+ UPDATED_AT BIGINT NOT NULL,
+ EXCLUDE_FROM_PURGE BOOLEAN DEFAULT FALSE NOT NULL
+);
ListAction.class,
DeleteAction.class,
RenameAction.class,
+ SetAutomaticDeletionProtectionAction.class,
BranchesWs.class);
}
}
static void addBranchParam(NewAction action) {
action
.createParam(PARAM_BRANCH)
- .setDescription("Name of the branch")
- .setExampleValue("branch1")
+ .setDescription("Branch key")
+ .setExampleValue("feature/my_branch")
.setRequired(true);
}
ofNullable(branchKey).ifPresent(builder::setName);
builder.setIsMain(branch.isMain());
builder.setType(Common.BranchType.valueOf(branch.getBranchType().name()));
+ builder.setExcludedFromPurge(branch.isExcludeFromPurge());
return builder;
}
public static final String ACTION_LIST = "list";
public static final String ACTION_DELETE = "delete";
public static final String ACTION_RENAME = "rename";
+ public static final String ACTION_SET_AUTOMATIC_DELETION_PROTECTION = "set_automatic_deletion_protection";
// parameters
public static final String PARAM_PROJECT = "project";
public static final String PARAM_COMPONENT = "component";
public static final String PARAM_BRANCH = "branch";
public static final String PARAM_NAME = "name";
+ public static final String PARAM_VALUE = "value";
private ProjectBranchesParameters() {
// static utility class
--- /dev/null
+/*
+ * 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.branch.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static org.sonar.server.branch.ws.BranchesWs.addBranchParam;
+import static org.sonar.server.branch.ws.BranchesWs.addProjectParam;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.ACTION_SET_AUTOMATIC_DELETION_PROTECTION;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_BRANCH;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_PROJECT;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_VALUE;
+
+public class SetAutomaticDeletionProtectionAction implements BranchWsAction {
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final ComponentFinder componentFinder;
+
+ public SetAutomaticDeletionProtectionAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.componentFinder = componentFinder;
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction(ACTION_SET_AUTOMATIC_DELETION_PROTECTION)
+ .setSince("8.1")
+ .setDescription("Protect a specific branch from automatic deletion. Protection can't be disabled for the main branch.<br/>"
+ + "Requires 'Administer' permission on the specified project.")
+ .setPost(true)
+ .setHandler(this);
+
+ addProjectParam(action);
+ addBranchParam(action);
+ action.createParam(PARAM_VALUE)
+ .setRequired(true)
+ .setBooleanPossibleValues()
+ .setDescription("Sets whether the branch should be protected from automatic deletion when it hasn't been analyzed for a set period of time.")
+ .setExampleValue("true");
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ userSession.checkLoggedIn();
+ String projectKey = request.mandatoryParam(PARAM_PROJECT);
+ String branchKey = request.mandatoryParam(PARAM_BRANCH);
+ boolean excludeFromPurge = request.mandatoryParamAsBoolean(PARAM_VALUE);
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ ComponentDto project = componentFinder.getRootComponentByUuidOrKey(dbSession, null, projectKey);
+ checkPermission(project);
+
+ BranchDto branch = dbClient.branchDao().selectByBranchKey(dbSession, project.uuid(), branchKey)
+ .orElseThrow(() -> new NotFoundException(format("Branch '%s' not found for project '%s'", branchKey, projectKey)));
+ checkArgument(!branch.isMain() || excludeFromPurge, "Main branch of the project is always excluded from automatic deletion.");
+
+ dbClient.branchDao().updateExcludeFromPurge(dbSession, branch.getUuid(), excludeFromPurge);
+ dbSession.commit();
+ response.noContent();
+ }
+
+ }
+
+ private void checkPermission(ComponentDto project) {
+ userSession.checkComponentPermission(UserRole.ADMIN, project);
+ }
+}
"status": {
"qualityGateStatus": "OK"
},
- "analysisDate": "2017-04-03T13:37:00+0100"
+ "analysisDate": "2017-04-03T13:37:00+0100",
+ "excludedFromPurge": false
},
{
"name": "master",
"status": {
"qualityGateStatus": "ERROR"
},
- "analysisDate": "2017-04-01T01:15:42+0100"
+ "analysisDate": "2017-04-01T01:15:42+0100",
+ "excludedFromPurge": false
}
]
}
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new BranchWsModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 4);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 5);
}
}
--- /dev/null
+/*
+ * 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.branch.ws;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+
+public class SetAutomaticDeletionProtectionActionTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ private ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT);
+ private ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes);
+ private WsActionTester tester = new WsActionTester(new SetAutomaticDeletionProtectionAction(db.getDbClient(), userSession, componentFinder));
+
+ @Test
+ public void test_definition() {
+ WebService.Action definition = tester.getDef();
+ assertThat(definition.key()).isEqualTo("set_automatic_deletion_protection");
+ assertThat(definition.isPost()).isTrue();
+ assertThat(definition.isInternal()).isFalse();
+ assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("project", "branch", "value");
+ assertThat(definition.since()).isEqualTo("8.1");
+ }
+
+ @Test
+ public void fail_if_missing_project_parameter() {
+ userSession.logIn();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("The 'project' parameter is missing");
+
+ tester.newRequest().execute();
+ }
+
+ @Test
+ public void fail_if_missing_branch_parameter() {
+ userSession.logIn();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("The 'branch' parameter is missing");
+
+ tester.newRequest()
+ .setParam("project", "projectName")
+ .execute();
+ }
+
+ @Test
+ public void fail_if_missing_value_parameter() {
+ userSession.logIn();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("The 'value' parameter is missing");
+
+ tester.newRequest()
+ .setParam("project", "projectName")
+ .setParam("branch", "foobar")
+ .execute();
+ }
+
+ @Test
+ public void fail_if_not_logged_in() {
+ expectedException.expect(UnauthorizedException.class);
+ expectedException.expectMessage("Authentication is required");
+
+ tester.newRequest().execute();
+ }
+
+ @Test
+ public void fail_if_no_administer_permission() {
+ userSession.logIn();
+ ComponentDto project = db.components().insertMainBranch();
+
+ expectedException.expect(ForbiddenException.class);
+ expectedException.expectMessage("Insufficient privileges");
+
+ tester.newRequest()
+ .setParam("project", project.getKey())
+ .setParam("branch", "branch1")
+ .setParam("value", "true")
+ .execute();
+ }
+
+ @Test
+ public void fail_when_attempting_to_set_main_branch_as_included_in_purge() {
+ userSession.logIn();
+ ComponentDto project = db.components().insertMainBranch();
+ ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("branch1").setExcludeFromPurge(false));
+ userSession.addProjectPermission(UserRole.ADMIN, project);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Main branch of the project is always excluded from automatic deletion.");
+
+ tester.newRequest()
+ .setParam("project", project.getKey())
+ .setParam("branch", "master")
+ .setParam("value", "false")
+ .execute();
+ }
+
+ @Test
+ public void set_purge_exclusion() {
+ userSession.logIn();
+ ComponentDto project = db.components().insertMainBranch();
+ ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("branch1").setExcludeFromPurge(false));
+ userSession.addProjectPermission(UserRole.ADMIN, project);
+
+ tester.newRequest()
+ .setParam("project", project.getKey())
+ .setParam("branch", "branch1")
+ .setParam("value", "true")
+ .execute();
+
+ assertThat(db.countRowsOfTable("project_branches")).isEqualTo(2);
+ Optional<BranchDto> mainBranch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.uuid());
+ assertThat(mainBranch.get().getKey()).isEqualTo("master");
+ assertThat(mainBranch.get().isExcludeFromPurge()).isFalse();
+
+ Optional<BranchDto> branchDto = db.getDbClient().branchDao().selectByUuid(db.getSession(), branch.uuid());
+ assertThat(branchDto.get().getKey()).isEqualTo("branch1");
+ assertThat(branchDto.get().isExcludeFromPurge()).isTrue();
+ }
+
+ @Test
+ public void fail_on_non_boolean_value_parameter() {
+ userSession.logIn();
+ ComponentDto project = db.components().insertMainBranch();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Value of parameter 'value' (foobar) must be one of: [true, false, yes, no]");
+
+ tester.newRequest()
+ .setParam("project", project.getKey())
+ .setParam("branch", "branch1")
+ .setParam("value", "foobar")
+ .execute();
+ }
+
+ @Test
+ public void fail_if_project_does_not_exist() {
+ userSession.logIn();
+
+ expectedException.expect(NotFoundException.class);
+ expectedException.expectMessage("Project key 'foo' not found");
+
+ tester.newRequest()
+ .setParam("project", "foo")
+ .setParam("branch", "branch1")
+ .setParam("value", "true")
+ .execute();
+ }
+
+ @Test
+ public void fail_if_branch_does_not_exist() {
+ userSession.logIn();
+ ComponentDto project = db.components().insertMainBranch();
+ userSession.addProjectPermission(UserRole.ADMIN, project);
+
+ expectedException.expect(NotFoundException.class);
+ expectedException.expectMessage("Branch 'branch1' not found for project '" + project.getKey() + "'");
+
+ tester.newRequest()
+ .setParam("project", project.getKey())
+ .setParam("branch", "branch1")
+ .setParam("value", "true")
+ .execute();
+ }
+}
String WEEKS_BEFORE_KEEPING_ONLY_ANALYSES_WITH_VERSION = "sonar.dbcleaner.weeksBeforeKeepingOnlyAnalysesWithVersion";
String WEEKS_BEFORE_DELETING_ALL_SNAPSHOTS = "sonar.dbcleaner.weeksBeforeDeletingAllSnapshots";
String DAYS_BEFORE_DELETING_CLOSED_ISSUES = "sonar.dbcleaner.daysBeforeDeletingClosedIssues";
- String DAYS_BEFORE_DELETING_INACTIVE_SHORT_LIVING_BRANCHES = "sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches";
+ String DAYS_BEFORE_DELETING_INACTIVE_BRANCHES = "sonar.dbcleaner.daysBeforeDeletingInactiveBranches";
+ String BRANCHES_TO_KEEP_WHEN_INACTIVE = "sonar.dbcleaner.branchesToKeepWhenInactive";
}
+ "the DbCleaner keeps the most recent one and fully deletes the other ones.")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(1)
.build(),
+ "the DbCleaner keeps the most recent one and fully deletes the other ones")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(2)
.build(),
+ "the DbCleaner keeps the most recent one and fully deletes the other ones.")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(3)
.build(),
-
PropertyDefinition.builder(PurgeConstants.WEEKS_BEFORE_KEEPING_ONLY_ANALYSES_WITH_VERSION)
.defaultValue("104")
.name("Keep only analyses with a version event after")
.description("After this number of weeks, the DbCleaner keeps only analyses with a version event associated.")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(4)
.build(),
.description("After this number of weeks, all analyses are fully deleted.")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(5)
.build(),
.description("Issues that have been closed for more than this number of days will be deleted.")
.type(PropertyType.INTEGER)
.onQualifiers(Qualifiers.PROJECT)
- .category(CoreProperties.CATEGORY_GENERAL)
- .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER)
+ .category(CoreProperties.CATEGORY_HOUSEKEEPING)
+ .subCategory(CoreProperties.SUBCATEGORY_GENERAL)
.index(6)
- .build()
- );
+ .build());
}
}
property.error.notRegexp=Not a valid Java regular expression
property.error.notInOptions=Not a valid option
property.category.scm=SCM
+property.category.housekeeping=Housekeeping
+property.category.housekeeping.general=General
+property.category.housekeeping.branchesAndPullRequests=Branches and Pull Requests
property.sonar.branch.longLivedBranches.regex.description=Regular expression used to detect whether a branch is a long living branch (as opposed to short living branch), based on its name. This applies only during first analysis, the type of a branch cannot be changed later.
/**
* @since 4.0
+ * @deprecated since 8.1. Database cleaning now has a dedicated category {@link CoreProperties#CATEGORY_HOUSEKEEPING}.
*/
+ @Deprecated
String SUBCATEGORY_DATABASE_CLEANER = "databaseCleaner";
/**
*/
String SUBCATEGORY_DUPLICATIONS = "duplications";
+ /**
+ * @since 8.1
+ */
+ String CATEGORY_HOUSEKEEPING = "housekeeping";
+
/**
* @since 6.6
*/
- String SUBCATEGORY_BRANCHES = "Branches";
+ String SUBCATEGORY_BRANCHES_AND_PULL_REQUESTS = "branchesAndPullRequests";
+
+ /**
+ * @since 8.1
+ */
+ String SUBCATEGORY_GENERAL = "general";
/**
* @since 4.0
optional Status status = 5;
optional bool isOrphan = 6;
optional string analysisDate = 7;
+ optional bool excludedFromPurge = 8;
}
message Status {