]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12636 add a global setting that defines default list of branches to keep
authorMichal Duda <michal.duda@sonarsource.com>
Wed, 6 Nov 2019 16:24:57 +0000 (17:24 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 9 Dec 2019 19:46:15 +0000 (20:46 +0100)
39 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeConfiguration.java
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
server/sonar-db-dao/src/schema/schema-sq.ddl
server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDtoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeConfigurationTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v80/DbVersion80.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumn.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSetting.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumn.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v80/MakeExcludeBranchFromPurgeColumnNotNullableTest/schema.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest/schema.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest/schema.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest/schema.sql [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchesWs.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ListAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ProjectBranchesParameters.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/branch/ws/list-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionActionTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/config/PurgeConstants.java
sonar-core/src/main/java/org/sonar/core/config/PurgeProperties.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
sonar-ws/src/main/protobuf/ws-projectbranches.proto

index 5192cbc9ffa20f8231d4d2314b3e6147fb13fc9d..c3b3a2924f4ee560a443500aa45ad384fc2f400d 100644 (file)
@@ -63,6 +63,11 @@ public class BranchDao implements Dao {
     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);
   }
index 08dee30c11c816fef6a6b99fdc7449d568ccb7c6..dd5cb0d599d253321ce0339c1fdde1873963c2b8 100644 (file)
@@ -88,6 +88,8 @@ public class BranchDto {
   @Nullable
   private byte[] pullRequestBinary;
 
+  private boolean excludeFromPurge;
+
   public String getUuid() {
     return uuid;
   }
@@ -171,6 +173,14 @@ public class BranchDto {
     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 {
@@ -212,14 +222,14 @@ public class BranchDto {
 
   @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 +
+      '}';
   }
 }
index 10e501b5f5d50f9179d8e1955973035740f0b33c..4c3317859d5083874e0adc1b4fe49f8e10008881 100644 (file)
@@ -34,6 +34,9 @@ public interface BranchMapper {
 
   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);
@@ -44,5 +47,5 @@ public interface BranchMapper {
 
   long countNonMainBranches();
 
-  long countByTypeAndCreationDate(@Param("branchType")String branchType, @Param("sinceDate") long sinceDate);
+  long countByTypeAndCreationDate(@Param("branchType") String branchType, @Param("sinceDate") long sinceDate);
 }
index 461aa2b315377d883ee0281c6fc0e7ffecc6fb57..c707615368fa9b0d7b32ffa006113f201bef2c5d 100644 (file)
@@ -38,24 +38,24 @@ public class PurgeConfiguration {
   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);
   }
 
   /**
@@ -87,8 +87,8 @@ public class PurgeConfiguration {
     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
index 86ed071b5019fff4d711a696db9d8b1431f0efb6..46913a74abe65e0407163b4fa768002ea172e1ec 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.db.component.ComponentTreeQuery;
 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;
 
@@ -75,14 +76,15 @@ public class PurgeDao implements Dao {
   }
 
   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)) {
index 0e8107fb77a222ff0ebffd49eeeed9acbca63ab6..7b6e3b0465e20dfd09dd3bd61554084349e85c9d 100644 (file)
@@ -94,7 +94,7 @@ public interface PurgeMapper {
 
   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);
index 7f54831c8e029f566e51619a85889aa2799fcbc1..d65e0a48a69397602a40005e02e89d3aa8695daa 100644 (file)
@@ -9,7 +9,8 @@
     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">
@@ -22,7 +23,8 @@
       merge_branch_uuid,
       pull_request_binary,
       created_at,
-      updated_at
+      updated_at,
+      exclude_from_purge
     ) values (
       #{dto.uuid, jdbcType=VARCHAR},
       #{dto.projectUuid, jdbcType=VARCHAR},
@@ -32,7 +34,8 @@
       #{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
index 1151abc69e58a966fca82a732838825a944c7c7f..add001d29750700302c591edfe98fd874657d7a0 100644 (file)
       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 &lt; #{toDate}
   </select>
 
index 6c4514c452a28bf0cc6f60f06d8135021a1b6179..4939a434a2741f51c4b7d75b5bd58f9c6c88d49e 100644 (file)
@@ -591,7 +591,8 @@ CREATE TABLE "PROJECT_BRANCHES"(
     "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");
index 505c55cb536e83758c9aa2db28208f038f143bfd..1e844b6df59acdcc316ec58d9e8b13278e608923 100644 (file)
@@ -101,6 +101,22 @@ public class BranchDaoTest {
     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[][] {
index 2f39a2fb03d2ac45347010aa0b958cef30ea74bc..1be094379913fc0d4a24fa5253fbacdf800f65aa 100644 (file)
@@ -56,9 +56,10 @@ public class BranchDtoTest {
     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
index 4d83aa022e7eaeca177de9d6b0adc995c206669d..5e983925eaf352261ca5ac79159ea950a9680b3c 100644 (file)
@@ -58,15 +58,15 @@ public class PurgeConfigurationTest {
   @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
index 722f488517b591a537c593a05bcd94ca01a16e08..149ae2e229a2ad69dd76e745ac47eeaa5075e8f6 100644 (file)
@@ -127,7 +127,7 @@ public class PurgeDaoTest {
   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());
@@ -137,27 +137,27 @@ public class PurgeDaoTest {
   }
 
   @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
@@ -185,7 +185,7 @@ public class PurgeDaoTest {
   }
 
   @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();
@@ -193,22 +193,22 @@ public class PurgeDaoTest {
 
     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
@@ -483,8 +483,7 @@ public class PurgeDaoTest {
         .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())
@@ -510,8 +509,7 @@ public class PurgeDaoTest {
         .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())
@@ -527,8 +525,7 @@ public class PurgeDaoTest {
         .setProjectUuid(project.uuid())
         .setBranchUuid(branch2.uuid())
         .setType(NewCodePeriodType.SPECIFIC_ANALYSIS)
-        .setValue(analysisBranch2.getUuid())
-    );
+        .setValue(analysisBranch2.getUuid()));
     dbSession.commit();
 
     assertThat(underTest.selectPurgeableAnalyses(project.uuid(), dbSession))
@@ -1178,8 +1175,8 @@ public class PurgeDaoTest {
   @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());
 
index 3cc49559ff09c89d1469cb105e61b5a2ec47d3de..7086ea9591e7e3bd6e41b6aafe2a3fd1688aab78 100644 (file)
@@ -35,7 +35,6 @@ public class DbVersion80 implements DbVersion {
       .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);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumn.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumn.java
new file mode 100644 (file)
index 0000000..34297af
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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());
+  }
+}
index b8ca648b3d0899a9cbb58d055f31a57cdae3f615..e51407ffd2fa0bf3c2f1f52d20f8a4db89d2320e 100644 (file)
@@ -32,6 +32,9 @@ public class DbVersion81 implements DbVersion {
       .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);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSetting.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSetting.java
new file mode 100644 (file)
index 0000000..0fc106d
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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;
+    });
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumn.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumn.java
new file mode 100644 (file)
index 0000000..6b5a505
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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();
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest.java
new file mode 100644 (file)
index 0000000..9ff62d9
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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());
+  }
+}
index 64b052cd26cb7b9684e37245550c0c2ede37a881..45f8522bf48deffce67db3ca7b06c5d77def1a66 100644 (file)
@@ -37,7 +37,7 @@ public class DbVersion81Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 7);
+    verifyMigrationCount(underTest, 10);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest.java
new file mode 100644 (file)
index 0000000..1995e21
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * 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");
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest.java
new file mode 100644 (file)
index 0000000..b8464c6
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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());
+  }
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v80/MakeExcludeBranchFromPurgeColumnNotNullableTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v80/MakeExcludeBranchFromPurgeColumnNotNullableTest/schema.sql
new file mode 100644 (file)
index 0000000..8e8a6fa
--- /dev/null
@@ -0,0 +1,13 @@
+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
+);
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/AddExcludeBranchFromPurgeColumnTest/schema.sql
new file mode 100644 (file)
index 0000000..32de015
--- /dev/null
@@ -0,0 +1,12 @@
+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
+);
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateDefaultBranchesToKeepSettingTest/schema.sql
new file mode 100644 (file)
index 0000000..367029e
--- /dev/null
@@ -0,0 +1,11 @@
+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");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/PopulateExcludeBranchFromPurgeColumnTest/schema.sql
new file mode 100644 (file)
index 0000000..fb58dc6
--- /dev/null
@@ -0,0 +1,13 @@
+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
+);
index 47369fc861c8b6537422d68d7456cfa4db898e88..272501e31f061e0ad073e3b57cded9a09747c043 100644 (file)
@@ -28,6 +28,7 @@ public class BranchWsModule extends Module {
       ListAction.class,
       DeleteAction.class,
       RenameAction.class,
+      SetAutomaticDeletionProtectionAction.class,
       BranchesWs.class);
   }
 }
index 3e64024baa5f69b2a4289fbc6d5cda4dd5724a9f..4d9a3bfe9db70155f7c5b82ae2a2212201c29900 100644 (file)
@@ -54,8 +54,8 @@ public class BranchesWs implements WebService {
   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);
   }
 
index e3bee34d4ddb6e45dbb6a809bb35966c90e12133..60f0df556af15f692e43862769dee8b886d4f6ca 100644 (file)
@@ -141,6 +141,7 @@ public class ListAction implements BranchWsAction {
     ofNullable(branchKey).ifPresent(builder::setName);
     builder.setIsMain(branch.isMain());
     builder.setType(Common.BranchType.valueOf(branch.getBranchType().name()));
+    builder.setExcludedFromPurge(branch.isExcludeFromPurge());
     return builder;
   }
 
index dda583e6271a65ceebd72e3dfca88761017a5ee9..d8b555d49446b12c979c54c8d4b7d04819a11f8f 100644 (file)
@@ -27,12 +27,14 @@ public class ProjectBranchesParameters {
   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
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionAction.java
new file mode 100644 (file)
index 0000000..5ad07f3
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * 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);
+  }
+}
index f9daa0a2b6db7ba8319899c1ee3a96a1583c9faa..a9a0c47d8afb3bac110e7d0f6263f5491cf71942 100644 (file)
@@ -7,7 +7,8 @@
       "status": {
         "qualityGateStatus": "OK"
       },
-      "analysisDate": "2017-04-03T13:37:00+0100"
+      "analysisDate": "2017-04-03T13:37:00+0100",
+      "excludedFromPurge": false
     },
     {
       "name": "master",
@@ -16,7 +17,8 @@
       "status": {
         "qualityGateStatus": "ERROR"
       },
-      "analysisDate": "2017-04-01T01:15:42+0100"
+      "analysisDate": "2017-04-01T01:15:42+0100",
+      "excludedFromPurge": false
     }
   ]
 }
index cf5884296e50144826ad4a7d8bffa7ea8dcefdff..28b59682b65bb3e107c83024806a3b7fde95feb3 100644 (file)
@@ -30,6 +30,6 @@ public class BranchWsModuleTest {
   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);
   }
 }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/SetAutomaticDeletionProtectionActionTest.java
new file mode 100644 (file)
index 0000000..bd50095
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * 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();
+  }
+}
index 7e83f4deb697ff3e3e1865abd3afb303feb4921c..160cef6ed9faf3ceb696418e30dcb8b2479d8644 100644 (file)
@@ -27,5 +27,6 @@ public interface PurgeConstants {
   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";
 }
index 0c6637e50149ae52a9718edff607a5a05428adde..cfe4476e189a2f41ec53c97d0ed8962c15ed301f 100644 (file)
@@ -41,8 +41,8 @@ public final class PurgeProperties {
           + "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(),
 
@@ -53,8 +53,8 @@ public final class PurgeProperties {
           + "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(),
 
@@ -65,20 +65,19 @@ public final class PurgeProperties {
           + "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(),
 
@@ -88,8 +87,8 @@ public final class PurgeProperties {
         .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(),
 
@@ -99,10 +98,9 @@ public final class PurgeProperties {
         .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());
   }
 }
index 6ec2b5138648ceec09972ec13bce5e0e7e5c5a37..b6c744be79e337253d25e368f6f69d54bf446edc 100644 (file)
@@ -1034,6 +1034,9 @@ property.error.notFloat=Not a floating point number
 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.
 
 
index 6f693c89334aa689f5016ef538ab35c22e60e2bf..49e92f7e7be669c7dad61f2c5d57a32fa8d38dbb 100644 (file)
@@ -41,7 +41,9 @@ public interface CoreProperties {
 
   /**
    * @since 4.0
+   * @deprecated since 8.1. Database cleaning now has a dedicated category {@link CoreProperties#CATEGORY_HOUSEKEEPING}.
    */
+  @Deprecated
   String SUBCATEGORY_DATABASE_CLEANER = "databaseCleaner";
 
   /**
@@ -54,10 +56,20 @@ public interface CoreProperties {
    */
   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
index 75ae338e9fc5ca6a283eae13c56149b487b432ba..e48d6eed666d5e49aa26384f7bec30a780667288 100644 (file)
@@ -44,6 +44,7 @@ message Branch {
   optional Status status = 5;
   optional bool isOrphan = 6;
   optional string analysisDate = 7;
+  optional bool excludedFromPurge = 8;
 }
 
 message Status {