From 5ab143afc9544634555c67ccfaa8e2e9fdcbefc1 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 8 Mar 2021 14:47:36 +0100 Subject: [PATCH] SONAR-14558 Update SonarQube main branch name during Gitlab project onboarding --- .../sonar/alm/client/gitlab/GitLabBranch.java | 50 ++++++++++++ .../alm/client/gitlab/GitlabHttpClient.java | 23 +++++- .../client/gitlab/GitlabHttpClientTest.java | 57 ++++++++++--- .../ws/gitlab/ImportGitLabProjectAction.java | 23 ++++-- .../gitlab/ImportGitLabProjectActionTest.java | 79 +++++++++++++++++++ 5 files changed, 214 insertions(+), 18 deletions(-) create mode 100644 server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitLabBranch.java diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitLabBranch.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitLabBranch.java new file mode 100644 index 00000000000..d6519aab794 --- /dev/null +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitLabBranch.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.alm.client.gitlab; + +import com.google.gson.annotations.SerializedName; +import javax.annotation.Nullable; + +public class GitLabBranch { + + @SerializedName("name") + private final String name; + + @SerializedName("default") + private final boolean isDefault; + + public GitLabBranch() { + // http://stackoverflow.com/a/18645370/229031 + this(null, false); + } + + public GitLabBranch(@Nullable String name, boolean isDefault) { + this.name = name; + this.isDefault = isDefault; + } + + public String getName() { + return name; + } + + public boolean isDefault() { + return isDefault; + } +} diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java index f621e3d5a1d..a2d6b982f50 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java @@ -25,6 +25,7 @@ import com.google.gson.JsonSyntaxException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.Arrays; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; @@ -57,7 +58,6 @@ public class GitlabHttpClient { .build(); } - public void checkReadPermission(@Nullable String gitlabUrl, @Nullable String personalAccessToken) { checkProjectAccess(gitlabUrl, personalAccessToken, "Could not validate GitLab read permission. Got an unexpected answer."); } @@ -223,6 +223,27 @@ public class GitlabHttpClient { } } + public List getBranches(String gitlabUrl, String pat, Long gitlabProjectId) { + String url = String.format("%s/projects/%s/repository/branches", gitlabUrl, gitlabProjectId); + LOG.debug(String.format("get branches : [%s]", url)); + Request request = new Request.Builder() + .addHeader(PRIVATE_TOKEN, pat) + .get() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + checkResponseIsSuccessful(response); + String body = response.body().string(); + LOG.trace(String.format("loading branches payload result : [%s]", body)); + return Arrays.asList(new GsonBuilder().create().fromJson(body, GitLabBranch[].class)); + } catch (JsonSyntaxException e) { + throw new IllegalArgumentException("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result."); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + public ProjectList searchProjects(String gitlabUrl, String personalAccessToken, @Nullable String projectName, int pageNumber, int pageSize) { String url = String.format("%s/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%s&page=%d&per_page=%d", diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java index a19ee334320..38745899f06 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java @@ -62,7 +62,6 @@ public class GitlabHttpClientTest { .setBody("{\"error\":\"invalid_token\",\"error_description\":\"Token was revoked. You have to re-authorize from the user.\"}"); server.enqueue(response); - String gitlabUrl = this.gitlabUrl; assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Your GitLab token was revoked"); @@ -77,7 +76,6 @@ public class GitlabHttpClientTest { "\"scope\":\"api read_api\"}"); server.enqueue(response); - String gitlabUrl = this.gitlabUrl; assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Your GitLab token has insufficient scope"); @@ -90,7 +88,6 @@ public class GitlabHttpClientTest { .setBody("error in pat"); server.enqueue(response); - String gitlabUrl = this.gitlabUrl; assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid personal access token"); @@ -122,12 +119,55 @@ public class GitlabHttpClientTest { .setBody("non json payload"); server.enqueue(response); - String instanceUrl = gitlabUrl; - assertThatThrownBy(() -> underTest.getProject(instanceUrl, "pat", 12345L)) + assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "pat", 12345L)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result."); } + @Test + public void get_branches(){ + MockResponse response = new MockResponse() + .setResponseCode(200) + .setBody("[{\n" + + " \"name\": \"main\",\n" + + " \"default\": true\n" + + "},{\n" + + " \"name\": \"other\",\n" + + " \"default\": false\n" + + "}]"); + server.enqueue(response); + + assertThat(underTest.getBranches(gitlabUrl, "pat", 12345L)) + .extracting(GitLabBranch::getName, GitLabBranch::isDefault) + .containsExactly( + tuple("main", true), + tuple("other", false) + ); + } + + @Test + public void get_branches_fail_if_non_json_payload() { + MockResponse response = new MockResponse() + .setResponseCode(200) + .setBody("non json payload"); + server.enqueue(response); + + String instanceUrl = gitlabUrl; + assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result."); + } + + @Test + public void get_branches_fail_if_exception() throws IOException { + server.shutdown(); + + String instanceUrl = gitlabUrl; + assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to connect to"); + } + @Test public void search_projects() throws InterruptedException { MockResponse projects = new MockResponse() @@ -236,8 +276,7 @@ public class GitlabHttpClientTest { projects.addHeader("X-Total", "bad-total-number"); server.enqueue(projects); - String gitlabInstanceUrl = gitlabUrl; - assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "pat", "example", 1, 10)) + assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Could not parse pagination number"); } @@ -249,8 +288,7 @@ public class GitlabHttpClientTest { .setBody("[ ]"); server.enqueue(projects); - String gitlabInstanceUrl = gitlabUrl; - assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "pat", "example", 1, 10)) + assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Pagination data from GitLab response is missing"); } @@ -262,7 +300,6 @@ public class GitlabHttpClientTest { .setBody("test"); server.enqueue(projects); - String gitlabUrl = this.gitlabUrl; assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Could not get projects from GitLab instance"); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java index b116ab2e222..ae3629e87cd 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java @@ -21,6 +21,8 @@ package org.sonar.server.almintegration.ws.gitlab; import com.google.common.annotations.VisibleForTesting; import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.alm.client.gitlab.GitLabBranch; import org.sonar.alm.client.gitlab.GitlabHttpClient; import org.sonar.alm.client.gitlab.Project; import org.sonar.api.server.ws.Request; @@ -103,16 +105,23 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction { long gitlabProjectId = request.mandatoryParamAsLong(PARAM_GITLAB_PROJECT_ID); - String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); - Project gitlabProject = gitlabHttpClient.getProject(url, pat, gitlabProjectId); + String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "ALM gitlabUrl cannot be null"); + Project gitlabProject = gitlabHttpClient.getProject(gitlabUrl, pat, gitlabProjectId); - ComponentDto componentDto = createProject(dbSession, gitlabProject); + Optional almMainBranchName = getAlmDefaultBranch(pat, gitlabProjectId, gitlabUrl); + ComponentDto componentDto = createProject(dbSession, gitlabProject, almMainBranchName.orElse(null)); populateMRSetting(dbSession, gitlabProjectId, componentDto, almSettingDto); + componentUpdater.commitAndIndex(dbSession, componentDto); return ImportHelper.toCreateResponse(componentDto); } } + private Optional getAlmDefaultBranch(String pat, long gitlabProjectId, String gitlabUrl) { + Optional almMainBranch = gitlabHttpClient.getBranches(gitlabUrl, pat, gitlabProjectId).stream().filter(GitLabBranch::isDefault).findFirst(); + return almMainBranch.map(GitLabBranch::getName); + } + private void populateMRSetting(DbSession dbSession, Long gitlabProjectId, ComponentDto componentDto, AlmSettingDto almSettingDto) { dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, new ProjectAlmSettingDto() .setProjectUuid(componentDto.projectUuid()) @@ -120,20 +129,20 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction { .setAlmRepo(gitlabProjectId.toString()) .setAlmSlug(null) .setMonorepo(false)); - dbSession.commit(); } - private ComponentDto createProject(DbSession dbSession, Project gitlabProject) { + private ComponentDto createProject(DbSession dbSession, Project gitlabProject, @Nullable String mainBranchName) { boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate(); String sqProjectKey = generateProjectKey(gitlabProject.getPathWithNamespace(), uuidFactory.create()); - return componentUpdater.create(dbSession, newComponentBuilder() + return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder() .setKey(sqProjectKey) .setName(gitlabProject.getName()) .setPrivate(visibility) .setQualifier(PROJECT) .build(), - userSession.getUuid()); + userSession.getUuid(), mainBranchName, s -> { + }); } @VisibleForTesting diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java index 55c0d0e6ea6..9ad81f6beda 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java @@ -21,9 +21,11 @@ package org.sonar.server.almintegration.ws.gitlab; import java.util.Optional; import java.util.stream.IntStream; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.sonar.alm.client.gitlab.GitLabBranch; import org.sonar.alm.client.gitlab.GitlabHttpClient; import org.sonar.alm.client.gitlab.Project; import org.sonar.api.utils.System2; @@ -32,6 +34,7 @@ import org.sonar.core.util.SequenceUuidFactory; import org.sonar.core.util.UuidFactory; import org.sonar.db.DbTester; import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.component.BranchDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.user.UserDto; import org.sonar.server.almintegration.ws.ImportHelper; @@ -45,9 +48,12 @@ import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; import org.sonarqube.ws.Projects; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -93,6 +99,7 @@ public class ImportGitLabProjectActionTest { }); Project project = getGitlabProject(); when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project); + when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(singletonList(new GitLabBranch("master", true))); when(uuidFactory.create()).thenReturn("uuid"); Projects.CreateWsResponse response = ws.newRequest() @@ -111,6 +118,78 @@ public class ImportGitLabProjectActionTest { assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); } + @Test + public void import_project_with_specific_different_default_branch() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + dto.setPersonalAccessToken("PAT"); + }); + Project project = getGitlabProject(); + when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project); + when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(singletonList(new GitLabBranch("main", true))); + when(uuidFactory.create()).thenReturn("uuid"); + + Projects.CreateWsResponse response = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("gitlabProjectId", "12345") + .executeProtobuf(Projects.CreateWsResponse.class); + + verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L); + verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L); + + Projects.CreateWsResponse.Project result = response.getProject(); + assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid"); + assertThat(result.getName()).isEqualTo(project.getName()); + + Optional projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); + assertThat(projectDto).isPresent(); + assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); + + Assertions.assertThat(db.getDbClient().branchDao().selectByProject(db.getSession(), projectDto.get())) + .extracting(BranchDto::getKey, BranchDto::isMain) + .containsExactlyInAnyOrder(tuple("main", true)); + } + + @Test + public void import_project_no_gitlab_default_branch() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + dto.setPersonalAccessToken("PAT"); + }); + Project project = getGitlabProject(); + when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project); + when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(emptyList()); + when(uuidFactory.create()).thenReturn("uuid"); + + Projects.CreateWsResponse response = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("gitlabProjectId", "12345") + .executeProtobuf(Projects.CreateWsResponse.class); + + verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L); + verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L); + + Projects.CreateWsResponse.Project result = response.getProject(); + assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid"); + assertThat(result.getName()).isEqualTo(project.getName()); + + Optional projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); + assertThat(projectDto).isPresent(); + assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); + + Assertions.assertThat(db.getDbClient().branchDao().selectByProject(db.getSession(), projectDto.get())) + .extracting(BranchDto::getKey, BranchDto::isMain) + .containsExactlyInAnyOrder(tuple("master", true)); + } + @Test public void generate_project_key_less_than_250() { String name = "abcdeert"; -- 2.39.5