]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19850 Add POST endpoint to set main branch
authorBenjamin Campomenosi <benjamin.campomenosi@sonarsource.com>
Thu, 6 Jul 2023 14:06:48 +0000 (16:06 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:06 +0000 (20:03 +0000)
13 files changed:
server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java
server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java
server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java
server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/branch/ws/SetMainBranchActionIT.java [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/ProjectBranchesParameters.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetMainBranchAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java

index 8caffc16280ae361d339ce07dfd67885f88b3af4..8cd260e3f1844f31634dd0277761c455457fa1dd 100644 (file)
@@ -485,8 +485,7 @@ public class BranchDaoIT {
   }
 
   @Test
-  public void
-  selectByPullRequestKey() {
+  public void selectByPullRequestKey() {
     BranchDto mainBranch = new BranchDto();
     mainBranch.setProjectUuid("U1");
     mainBranch.setUuid("U1");
@@ -634,8 +633,7 @@ public class BranchDaoIT {
       .containsExactlyInAnyOrder(
         tuple(projectData1.projectUuid(), 3L, 3L),
         tuple(projectData2.projectUuid(), 1L, 1L),
-        tuple(projectData3.projectUuid(), 2L, 0L)
-      );
+        tuple(projectData3.projectUuid(), 2L, 0L));
   }
 
   @Test
@@ -655,7 +653,7 @@ public class BranchDaoIT {
 
     assertThat(underTest.selectProjectUuidsWithIssuesNeedSync(db.getSession(),
       Sets.newHashSet(project1Dto.getUuid(), project2Dto.getUuid(), project3Dto.getUuid(), project4Dto.getUuid())))
-      .containsOnly(project1Dto.getUuid());
+        .containsOnly(project1Dto.getUuid());
   }
 
   @Test
@@ -767,6 +765,23 @@ public class BranchDaoIT {
     assertThat(project2.get().isNeedIssueSync()).isFalse();
   }
 
+  @Test
+  public void updateIsMain() {
+    ProjectData projectData = db.components().insertPrivateProject();
+    ProjectDto projectDto = projectData.getProjectDto();
+    BranchDto mainBranch = projectData.getMainBranchDto();
+    BranchDto nonMainBranch = db.components().insertProjectBranch(projectDto).setBranchType(BRANCH).setIsMain(false);
+
+    underTest.updateIsMain(dbSession, mainBranch.getUuid(), false);
+    underTest.updateIsMain(dbSession, nonMainBranch.getUuid(), true);
+
+    Optional<BranchDto> oldMainBranch = underTest.selectByUuid(dbSession, mainBranch.getUuid());
+    assertThat(oldMainBranch).isPresent().get().extracting(BranchDto::isMain).isEqualTo(false);
+    Optional<BranchDto> newMainBranch = underTest.selectByUuid(dbSession, nonMainBranch.getUuid());
+    assertThat(newMainBranch).isPresent().get().extracting(BranchDto::isMain).isEqualTo(true);
+
+  }
+
   @Test
   public void doAnyOfComponentsNeedIssueSync() {
     assertThat(underTest.doAnyOfComponentsNeedIssueSync(dbSession, emptyList())).isFalse();
index c65d73d43f5db217073c09c5b6d74c583615e5d3..86407a9c3ec9e5c27d16681ebf95b88f10b48ddc 100644 (file)
@@ -184,6 +184,10 @@ public class BranchDao implements Dao {
     long now = system2.now();
     return mapper(dbSession).updateNeedIssueSync(branchUuid, needIssueSync, now);
   }
+  public long updateIsMain(DbSession dbSession, String branchUuid, boolean isMain) {
+    long now = system2.now();
+    return mapper(dbSession).updateIsMain(branchUuid, isMain, now);
+  }
 
   public boolean doAnyOfComponentsNeedIssueSync(DbSession session, List<String> components) {
     if (!components.isEmpty()) {
index 435bcfebb44ba55cdcddd079933048c2874161bc..55224320e9c8afa37f2eae3e3b247e99d6f88c77 100644 (file)
@@ -68,6 +68,8 @@ public interface BranchMapper {
 
   long updateNeedIssueSync(@Param("uuid") String uuid, @Param("needIssueSync")boolean needIssueSync,@Param("now") long now);
 
+  long updateIsMain(@Param("uuid") String uuid, @Param("isMain") boolean isMain, @Param("now") long now);
+
   short doAnyOfComponentsNeedIssueSync(@Param("componentKeys") List<String> components);
 
   Optional<BranchDto> selectMainBranchByProjectUuid(String projectUuid);
@@ -75,4 +77,5 @@ public interface BranchMapper {
   List<BranchDto> selectMainBranchesByProjectUuids(@Param("projectUuids") Collection<String> projectUuids);
 
   List<BranchMeasuresDto> selectBranchMeasuresWithCaycMetric(long yesterday);
+
 }
index 8f05c259c8d8045709613f8986cc44833910ef1a..b2b3993d2bfce6dc2e22806aad3785d4110ae23b 100644 (file)
       uuid = #{uuid, jdbcType=VARCHAR}
   </update>
 
+    <update id="updateIsMain">
+    update project_branches
+    set
+      is_main = #{isMain, jdbcType=BOOLEAN},
+      updated_at = #{now, jdbcType=BIGINT}
+    where
+      uuid = #{uuid, jdbcType=VARCHAR}
+  </update>
+
   <sql id="doAnyOfComponentsNeedIssueSyncSql">
     select
     case when exists
index f16ea78b849919b709c9dfde454b1108e49e99cb..4f89a015ee77d535863861cb9718547fce97df9e 100644 (file)
@@ -30,9 +30,9 @@ public interface ProjectLifeCycleListener {
   void onProjectsDeleted(Set<DeletedProject> projects);
 
   /**
-   * This method is called after the specified projects have been deleted.
+   * This method is called after the specified projects have branches deleted or main branch changed.
    */
-  void onProjectBranchesDeleted(Set<Project> projects);
+  void onProjectBranchesChanged(Set<Project> projects);
 
   /**
    * This method is called after the specified projects' keys have been modified.
index 9b0ba01ccc2a88abbcf7cd04c1bdc2b6ba5e9b65..0a8c2238cb020b06eb5bbb864c986dd784042174 100644 (file)
@@ -33,8 +33,8 @@ public interface ProjectLifeCycleListeners {
   void onProjectsDeleted(Set<DeletedProject> projects);
 
   /**
-   * This method is called after the specified project branches have been deleted and will call method
-   * {@link ProjectLifeCycleListener#onProjectBranchesDeleted(Set)} of all known
+   * This method is called after the specified project have any king of change (branch deleted, change of main branch, ...)
+   *  This method will call method {@link ProjectLifeCycleListener#onProjectBranchesChanged(Set)} of all known
    * {@link ProjectLifeCycleListener} implementations.
    * <p>
    * This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
index c80d6110ce5fa97b09906a31d02d583e473476cf..a7e96c110e162b2d7bb91dd4d18a0a7930fa4ce0 100644 (file)
@@ -68,7 +68,7 @@ public class ProjectLifeCycleListenersImpl implements ProjectLifeCycleListeners
     }
 
     Arrays.stream(listeners)
-      .forEach(safelyCallListener(listener -> listener.onProjectBranchesDeleted(projects)));
+      .forEach(safelyCallListener(listener -> listener.onProjectBranchesChanged(projects)));
   }
 
   @Override
index 5c789db2d6463dc1ad9a1111992c1569a269a3cb..c73ef83e00e48196cd944ce2f6d9964bfcac93f8 100644 (file)
@@ -160,9 +160,9 @@ public class ProjectLifeCycleListenersImplTest {
 
     underTestWithListeners.onProjectBranchesDeleted(projects);
 
-    inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+    inOrder.verify(listener1).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener2).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener3).onProjectBranchesChanged(same(projects));
     inOrder.verifyNoMoreInteractions();
   }
 
@@ -172,13 +172,13 @@ public class ProjectLifeCycleListenersImplTest {
     InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
     doThrow(new RuntimeException("Faking listener2 throwing an exception"))
       .when(listener2)
-      .onProjectBranchesDeleted(any());
+      .onProjectBranchesChanged(any());
 
     underTestWithListeners.onProjectBranchesDeleted(projects);
 
-    inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+    inOrder.verify(listener1).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener2).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener3).onProjectBranchesChanged(same(projects));
     inOrder.verifyNoMoreInteractions();
   }
 
@@ -188,13 +188,13 @@ public class ProjectLifeCycleListenersImplTest {
     InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
     doThrow(new Error("Faking listener2 throwing an Error"))
       .when(listener2)
-      .onProjectBranchesDeleted(any());
+      .onProjectBranchesChanged(any());
 
     underTestWithListeners.onProjectBranchesDeleted(projects);
 
-    inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
-    inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+    inOrder.verify(listener1).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener2).onProjectBranchesChanged(same(projects));
+    inOrder.verify(listener3).onProjectBranchesChanged(same(projects));
     inOrder.verifyNoMoreInteractions();
   }
 
@@ -215,7 +215,6 @@ public class ProjectLifeCycleListenersImplTest {
     };
   }
 
-
   @Test
   public void onProjectsRekeyed_throws_NPE_if_set_is_null() {
     assertThatThrownBy(() -> underTestWithListeners.onProjectsRekeyed(null))
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/branch/ws/SetMainBranchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/branch/ws/SetMainBranchActionIT.java
new file mode 100644 (file)
index 0000000..6bf4680
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.event.Level;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.testfixtures.log.LogTester;
+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.ComponentTesting;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.project.Project;
+import org.sonar.server.project.ProjectLifeCycleListeners;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.ACTION_SET_MAIN_BRANCH;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_BRANCH;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_PROJECT;
+
+public class SetMainBranchActionIT {
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public LogTester logTester = new LogTester().setLevel(Level.INFO);
+  ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class);
+  private WsActionTester tester = new WsActionTester(new SetMainBranchAction(db.getDbClient(), userSession, projectLifeCycleListeners));
+
+  @Test
+  public void testDefinition() {
+    WebService.Action definition = tester.getDef();
+    assertThat(definition.key()).isEqualTo(ACTION_SET_MAIN_BRANCH);
+    assertThat(definition.isPost()).isTrue();
+    assertThat(definition.isInternal()).isFalse();
+    assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("project", "branch");
+    assertThat(definition.since()).isEqualTo("10.2");
+  }
+
+  @Test
+  public void fail_whenProjectParameterIsMissing_shouldThrowIllegalArgumentException() {
+    userSession.logIn();
+
+    assertThatThrownBy(tester.newRequest()::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("The 'project' parameter is missing");
+  }
+
+  @Test
+  public void fail_whenBranchParameterIsMissing_shouldIllegalArgumentException() {
+    userSession.logIn();
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, "projectKey");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("The 'branch' parameter is missing");
+  }
+
+  @Test
+  public void fail_whenNotLoggedIn_shouldThrowUnauthorizedException() {
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, "project")
+      .setParam(PARAM_BRANCH, "anotherBranch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(UnauthorizedException.class)
+      .hasMessageContaining("Authentication is required");
+  }
+
+  @Test
+  public void fail_whenNoAdministerPermission_shouldThrowForbiddenException() {
+    userSession.logIn();
+    ProjectDto projectDto = db.components().insertPublicProject().getProjectDto();
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, projectDto.getKey())
+      .setParam(PARAM_BRANCH, "anotherBranch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessageContaining("Insufficient privileges");
+  }
+
+  @Test
+  public void fail_whenProjectIsNotFound_shouldThrowNotFoundException() {
+    userSession.logIn();
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, "noExistingProjectKey")
+      .setParam(PARAM_BRANCH, "aBranch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessageContaining("Project 'noExistingProjectKey' not found.");
+  }
+
+  @Test
+  public void fail_whenKeyPassedIsApplicationKey_shouldThrowIllegalArgumentException() {
+    userSession.logIn();
+    ProjectData application = db.components().insertPublicApplication();
+    userSession.addProjectPermission(UserRole.ADMIN, application.getProjectDto());
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, application.projectKey())
+      .setParam(PARAM_BRANCH, "aBranch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessageContaining("Project '%s' not found.".formatted(application.projectKey()));
+  }
+
+  @Test
+  public void fail_whenNewMainBranchIsNotFound_shouldThrowNotFoundException() {
+    userSession.logIn();
+
+    ProjectData projectData = db.components().insertPublicProject();
+    userSession.addProjectPermission(UserRole.ADMIN, projectData.getProjectDto());
+
+    String nonExistingBranch = "aNonExistingBranch";
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, projectData.projectKey())
+      .setParam(PARAM_BRANCH, nonExistingBranch);
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessageContaining("Branch '%s' not found for project '%s'.".formatted(nonExistingBranch, projectData.projectKey()));
+  }
+
+  @Test
+  public void fail_whenProjectHasNoMainBranch_shouldThrowIllegalStateException() {
+    userSession.logIn();
+    ProjectDto project = insertProjectWithoutMainBranch();
+    userSession.addProjectPermission(UserRole.ADMIN, project);
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, project.getKey())
+      .setParam(PARAM_BRANCH, "anotherBranch");
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessageContaining("No main branch for existing project '%s'".formatted(project.getKey()));
+  }
+
+  private ProjectDto insertProjectWithoutMainBranch() {
+    ProjectDto project = ComponentTesting.newProjectDto();
+    db.getDbClient().projectDao().insert(db.getSession(), project);
+    db.commit();
+    return project;
+  }
+
+  @Test
+  public void log_whenOldBranchAndNewBranchAreSame_shouldThrowServerException() {
+    userSession.logIn();
+
+    ProjectData projectData = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.ADMIN, projectData.getProjectDto());
+
+    TestRequest request = tester.newRequest()
+      .setParam(PARAM_PROJECT, projectData.projectKey())
+      .setParam(PARAM_BRANCH, projectData.getMainBranchDto().getKey());
+
+    request.execute();
+
+    assertThat(logTester.logs(Level.INFO))
+      .containsOnly("Branch '%s' is already the main branch.".formatted(projectData.getMainBranchDto().getKey()));
+  }
+
+  @Test
+  public void setNewMainBranch_shouldConfigureNewBranchAsMainBranchAndKeepThePreviousExcludeFromPurge() {
+    userSession.logIn();
+
+    ProjectData projectData = db.components().insertPrivateProject();
+    BranchDto newMainBranch = db.components().insertProjectBranch(projectData.getProjectDto(), branchDto -> branchDto.setKey("newMain"));
+    userSession.addProjectPermission(UserRole.ADMIN, projectData.getProjectDto());
+
+    tester.newRequest()
+      .setParam(PARAM_PROJECT, projectData.projectKey())
+      .setParam(PARAM_BRANCH, newMainBranch.getKey()).execute();
+
+    checkCallToProjectLifeCycleListenersOnProjectBranchesChanges(projectData.getProjectDto());
+    checkNewMainBranch(projectData.projectUuid(), newMainBranch.getUuid());
+    checkPreviousMainBranch(projectData);
+    assertThat(logTester.logs(Level.INFO))
+      .containsOnly("The new main branch of project '%s' is '%s' (Previous one : '%s')"
+        .formatted(projectData.projectKey(), newMainBranch.getKey(), projectData.getMainBranchDto().getKey()));
+  }
+
+  private void checkCallToProjectLifeCycleListenersOnProjectBranchesChanges(ProjectDto projectDto) {
+    Project project = Project.from(projectDto);
+    verify(projectLifeCycleListeners).onProjectBranchesDeleted(Set.of(project));
+  }
+
+  private void checkNewMainBranch(String projectUuid, String newBranchUuid) {
+    Optional<BranchDto> branchDto = db.getDbClient().branchDao().selectMainBranchByProjectUuid(db.getSession(), projectUuid);
+    assertThat(branchDto).isPresent();
+    assertThat(branchDto.get().getUuid()).isEqualTo(newBranchUuid);
+    assertThat(branchDto.get().isExcludeFromPurge()).isTrue();
+  }
+
+  private void checkPreviousMainBranch(ProjectData projectData) {
+    Optional<BranchDto> branchDto1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), projectData.getMainBranchDto().getUuid());
+    assertThat(branchDto1).isPresent();
+    BranchDto oldBranchAfterSetting = branchDto1.get();
+    assertThat(oldBranchAfterSetting.isMain()).isFalse();
+    assertThat(oldBranchAfterSetting.isExcludeFromPurge()).isTrue();
+  }
+
+}
index 338c92ce1ba4cdccbda29d2d1dcf136f1c35e746..c70b6b707088383c90aba03db85e54ad2021afdd 100644 (file)
@@ -29,6 +29,7 @@ public class BranchWsModule extends Module {
       DeleteAction.class,
       RenameAction.class,
       SetAutomaticDeletionProtectionAction.class,
+      SetMainBranchAction.class,
       BranchesWs.class);
   }
 }
index d7fd75fb5b9e7631107c589c95f889236339b47d..7835a06712faddc534ae6608f028f90733d15ae6 100644 (file)
@@ -27,6 +27,7 @@ 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_MAIN_BRANCH = "set_main";
   public static final String ACTION_SET_AUTOMATIC_DELETION_PROTECTION = "set_automatic_deletion_protection";
 
   // parameters
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetMainBranchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetMainBranchAction.java
new file mode 100644 (file)
index 0000000..8f5f2cb
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.project.ProjectDto;
+import org.sonar.server.project.Project;
+import org.sonar.server.project.ProjectLifeCycleListeners;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Collections.singleton;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.ACTION_SET_MAIN_BRANCH;
+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.exceptions.NotFoundException.checkFoundWithOptional;
+
+public class SetMainBranchAction implements BranchWsAction {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(SetMainBranchAction.class);
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final ProjectLifeCycleListeners projectLifeCycleListeners;
+
+  public SetMainBranchAction(DbClient dbClient, UserSession userSession, ProjectLifeCycleListeners projectLifeCycleListeners) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.projectLifeCycleListeners = projectLifeCycleListeners;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction(ACTION_SET_MAIN_BRANCH)
+      .setSince("10.2")
+      .setDescription("Allow to set a new main branch.<br/>. Caution, only applicable on projects.<br>" +
+        "Requires 'Administer' rights on the specified project or application.")
+      .setPost(true)
+      .setHandler(this);
+
+    action.createParam(PARAM_PROJECT)
+      .setDescription("Project key")
+      .setExampleValue("my_project")
+      .setRequired(true);
+    action.createParam(PARAM_BRANCH)
+      .setDescription("Branch key")
+      .setExampleValue("new_master")
+      .setRequired(true);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    userSession.checkLoggedIn();
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+    String newMainBranchKey = request.mandatoryParam(PARAM_BRANCH);
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ProjectDto projectDto = checkFoundWithOptional(dbClient.projectDao().selectProjectByKey(dbSession, projectKey),
+        "Project '%s' not found.", projectKey);
+      checkPermission(projectDto);
+
+      BranchDto oldMainBranch = dbClient.branchDao().selectMainBranchByProjectUuid(dbSession, projectDto.getUuid())
+        .orElseThrow(() -> new IllegalStateException("No main branch for existing project '%s'".formatted(projectDto.getKey())));
+      BranchDto newMainBranch = checkFoundWithOptional(dbClient.branchDao().selectByBranchKey(dbSession, projectDto.getUuid(), newMainBranchKey),
+        "Branch '%s' not found for project '%s'.", newMainBranchKey, projectDto.getKey());
+
+      if (checkAndLogIfNewBranchIsAlreadyMainBranch(oldMainBranch, newMainBranch)) {
+        response.noContent();
+        return;
+      }
+      configureProjectWithNewMainBranch(dbSession, projectDto.getKey(), oldMainBranch, newMainBranch);
+      refreshApplicationsAndPortfoliosComputedByProject(projectDto);
+      // todo : refresh elasticSearchIndexes
+
+      dbSession.commit();
+      response.noContent();
+    }
+  }
+
+  private void configureProjectWithNewMainBranch(DbSession dbSession, String projectKey, BranchDto oldMainBranch, BranchDto newMainBranch) {
+    updatePreviousMainBranch(dbSession, oldMainBranch);
+    updateNewMainBranch(dbSession, newMainBranch);
+
+    LOGGER.info("The new main branch of project '{}' is '{}' (Previous one : '{}')",
+      projectKey, newMainBranch.getKey(), oldMainBranch.getKey());
+  }
+
+  private static boolean checkAndLogIfNewBranchIsAlreadyMainBranch(BranchDto oldMainBranch, BranchDto newMainBranch) {
+    if (Objects.equals(oldMainBranch.getKey(), newMainBranch.getKey())) {
+      LOGGER.info("Branch '{}' is already the main branch.", newMainBranch.getKey());
+      return true;
+    }
+    return false;
+  }
+
+  private void refreshApplicationsAndPortfoliosComputedByProject(ProjectDto projectDto) {
+    projectLifeCycleListeners.onProjectBranchesDeleted(singleton(Project.from(projectDto)));
+  }
+
+  private void updateNewMainBranch(DbSession dbSession, BranchDto newMainBranch) {
+    if (!newMainBranch.isExcludeFromPurge()) {
+      dbClient.branchDao().updateExcludeFromPurge(dbSession, newMainBranch.getUuid(), true);
+    }
+    dbClient.branchDao().updateIsMain(dbSession, newMainBranch.getUuid(), true);
+  }
+
+  private void updatePreviousMainBranch(DbSession dbSession, BranchDto oldMainBranch) {
+    dbClient.branchDao().updateIsMain(dbSession, oldMainBranch.getUuid(), false);
+  }
+
+  private void checkPermission(ProjectDto project) {
+    userSession.checkEntityPermission(UserRole.ADMIN, project);
+  }
+}
index aa2866fc4fa34ff74b339a668447379df9524478..406646ca1bc9df65f204bbffc5f2064db97eec20 100644 (file)
@@ -29,6 +29,6 @@ public class BranchWsModuleTest {
   public void verify_count_of_added_components() {
     ListContainer container = new ListContainer();
     new BranchWsModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(5);
+    assertThat(container.getAddedObjects()).hasSize(6);
   }
 }