From: Benjamin Campomenosi Date: Thu, 6 Jul 2023 14:06:48 +0000 (+0200) Subject: SONAR-19850 Add POST endpoint to set main branch X-Git-Tag: 10.2.0.77647~338 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7867b6b333315e353456558add64e7d9a4392183;p=sonarqube.git SONAR-19850 Add POST endpoint to set main branch --- diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java index 8caffc16280..8cd260e3f18 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java @@ -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 oldMainBranch = underTest.selectByUuid(dbSession, mainBranch.getUuid()); + assertThat(oldMainBranch).isPresent().get().extracting(BranchDto::isMain).isEqualTo(false); + Optional 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(); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java index c65d73d43f5..86407a9c3ec 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java @@ -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 components) { if (!components.isEmpty()) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java index 435bcfebb44..55224320e9c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java @@ -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 components); Optional selectMainBranchByProjectUuid(String projectUuid); @@ -75,4 +77,5 @@ public interface BranchMapper { List selectMainBranchesByProjectUuids(@Param("projectUuids") Collection projectUuids); List selectBranchMeasuresWithCaycMetric(long yesterday); + } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml index 8f05c259c8d..b2b3993d2bf 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml @@ -269,6 +269,15 @@ uuid = #{uuid, jdbcType=VARCHAR} + + update project_branches + set + is_main = #{isMain, jdbcType=BOOLEAN}, + updated_at = #{now, jdbcType=BIGINT} + where + uuid = #{uuid, jdbcType=VARCHAR} + + select case when exists diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java index f16ea78b849..4f89a015ee7 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java @@ -30,9 +30,9 @@ public interface ProjectLifeCycleListener { void onProjectsDeleted(Set 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 projects); + void onProjectBranchesChanged(Set projects); /** * This method is called after the specified projects' keys have been modified. diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java index 9b0ba01ccc2..0a8c2238cb0 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java @@ -33,8 +33,8 @@ public interface ProjectLifeCycleListeners { void onProjectsDeleted(Set 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. *

* This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java index c80d6110ce5..a7e96c110e1 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java @@ -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 diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java index 5c789db2d64..c73ef83e00e 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java @@ -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 index 00000000000..6bf46806a6c --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/branch/ws/SetMainBranchActionIT.java @@ -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 = 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 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(); + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchWsModule.java index 338c92ce1ba..c70b6b70708 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/BranchWsModule.java @@ -29,6 +29,7 @@ public class BranchWsModule extends Module { DeleteAction.class, RenameAction.class, SetAutomaticDeletionProtectionAction.class, + SetMainBranchAction.class, BranchesWs.class); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ProjectBranchesParameters.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ProjectBranchesParameters.java index d7fd75fb5b9..7835a06712f 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ProjectBranchesParameters.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/ProjectBranchesParameters.java @@ -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 index 00000000000..8f5f2cb33dc --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/branch/ws/SetMainBranchAction.java @@ -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.
. Caution, only applicable on projects.
" + + "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); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java index aa2866fc4fa..406646ca1bc 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/branch/ws/BranchWsModuleTest.java @@ -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); } }