From fdb382a064b4526f7c34cbcda658306e7c2c723c Mon Sep 17 00:00:00 2001 From: Michael Clarke Date: Sun, 2 Feb 2020 21:53:45 +0000 Subject: [PATCH 01/26] Add support for Sonarqube 8.1.0 Sonarqube 8.1 removed the concept of short and long lived branches, instead simplifying the setup to either `BRANCH` or `PULL_REQUEST`. This release of Sonarqube also moved the management of Pull Request decoration into a standard User Interface calling a set of 'ALM' services that provide the management of decorators, and the binding of each project to these decorators. However the implementation of these services is not included in the Community Edition of Sonarqube, although the UI is made visible based on the presence of the branch management components provided by this plugin. This change therefore introduces the services required to support UI components for the management of Pull Request decoration, as well as updating the handling of the configuration and loading of branches to support the removal of the SHORT/LONG branch constructs. As these changes require the reference of classes that were not present in older version of Sonarqube (namely `AlmSettingsDao` and `ProjectAlmSettingsDao`), the compatibility interfaces for these versions have been removed, as well as any methods and classes that existed purely to allow these versions to be supported by this plugin. Given the standard UI provides a restricted set of fields for creating and binding each Pull Request decorator, some of the patterns that were previously used by this plugin for configuring the decoration of Pull Requests do not fit into this UI, and were awkward to find/manage when moved to another screen in the UI. This results in the configuration for disabling the addition and removing of comments on Merge Requests being removed from the plugin, with the Gitlab decorator removing the deleting of old comments since this was leading to discussion boxes being shown in Gitlab with no content, and the BitBucket decorator removing since it can't work out which user posted comments based purely on the configuration provided by Sonarqube. To ensure the UI works for all configuration options, the services for configuring and binding Azure DevOps configuration have been included in this change-set, although no decorator currently exists for this ALM, so any project attempting to use this configuration will get a warning appear in the CE logs when attempting to use this decorator. Similarly, as the UI does not provide any options for configuring the URL for the Gitlab API, or the Slug/name of the project on the binding, a `Sensor` has been added into the scanner to detect these properties from injection by the Gitlab CI Runner, as well as injection directly from scanner arguments of `com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.url` and `com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.repositorySlug` for the repository URL and repository-name configuration entries. --- build.gradle | 4 +- .../plugin/CommunityBranchPlugin.java | 166 ++--- .../plugin/SonarqubeCompatibility.java | 52 -- .../plugin/ce/BranchCompatibility.java | 32 - .../sonarqube/plugin/ce/CommunityBranch.java | 19 +- .../ce/CommunityBranchLoaderDelegate.java | 17 +- .../ce/pullrequest/AnalysisDetails.java | 65 +- .../PostProjectAnalysisTaskCompatibility.java | 53 -- .../PullRequestBuildStatusDecorator.java | 17 +- .../PullRequestPostAnalysisTask.java | 66 +- .../bitbucket/response/activity/Activity.java | 51 -- .../response/activity/ActivityPage.java | 72 -- .../bitbucket/response/activity/Comment.java | 58 -- .../bitbucket/response/activity/User.java | 44 -- .../BitbucketServerPullRequestDecorator.java | 176 +---- .../pullrequest/github/CheckRunProvider.java | 7 +- .../github/GithubPullRequestDecorator.java | 15 +- .../github/v4/GraphqlCheckRunProvider.java | 46 +- .../GitlabServerPullRequestDecorator.java | 187 ++--- .../ce/pullrequest/gitlab/response/Note.java | 49 -- .../ce/pullrequest/gitlab/response/User.java | 35 - ...ranchConfigurationLoaderCompatibility.java | 49 -- .../BranchParamsValidatorCompatibility.java | 44 -- .../scanner/CommunityBranchConfiguration.java | 14 +- .../CommunityBranchConfigurationLoader.java | 67 +- .../CommunityBranchParamsValidator.java | 22 +- .../CommunityProjectBranchesLoader.java | 6 +- .../CommunityProjectPullRequestsLoader.java | 6 +- .../ScannerPullRequestPropertySensor.java | 60 ++ .../scanner/ScannerWsClientWrapper.java | 61 -- .../CommunityBranchSupportDelegate.java | 9 +- .../plugin/server/CommunityComponentKey.java | 9 +- .../pullrequest/ws/AlmSettingsWs.java} | 36 +- .../ws/AlmTypeMapper.java} | 30 +- .../ws/SetBindingAlmSettingsWsAction.java | 46 ++ .../ws/action/AlmSettingsWsAction.java | 48 ++ .../ws/action/CountBindingAction.java | 69 ++ .../pullrequest/ws/action/DeleteAction.java | 64 ++ .../ws/action/DeleteBindingAction.java | 67 ++ .../ws/action/GetBindingAction.java | 88 +++ .../pullrequest/ws/action/ListAction.java | 83 +++ .../ws/action/ListDefinitionsAction.java | 115 +++ .../ws/action/azure/CreateAzureAction.java | 71 ++ .../action/azure/SetAzureBindingAction.java | 71 ++ .../ws/action/azure/UpdateAzureAction.java | 77 +++ .../bitbucket/CreateBitBucketAction.java | 75 ++ .../bitbucket/SetBitbucketBindingAction.java | 77 +++ .../bitbucket/UpdateBitbucketAction.java | 81 +++ .../ws/action/github/CreateGithubAction.java | 79 +++ .../action/github/SetGithubBindingAction.java | 74 ++ .../ws/action/github/UpdateGitHubAction.java | 85 +++ .../ws/action/gitlab/CreateGitlabAction.java | 71 ++ .../action/gitlab/SetGitlabBindingAction.java | 71 ++ .../ws/action/gitlab/UpdateGitlabAction.java | 77 +++ .../plugin/CommunityBranchPluginTest.java | 6 +- .../ce/CommunityBranchLoaderDelegateTest.java | 84 +-- .../plugin/ce/CommunityBranchTest.java | 8 +- .../ce/pullrequest/AnalysisDetailsTest.java | 66 +- .../PullRequestPostAnalysisTaskTest.java | 263 ++++++- ...tbucketServerPullRequestDecoratorTest.java | 193 ++---- .../GithubPullRequestDecoratorTest.java | 20 +- .../v4/GraphqlCheckRunProviderTest.java | 164 +---- .../GitlabServerPullRequestDecoratorTest.java | 161 +++-- ...ommunityBranchConfigurationLoaderTest.java | 652 +----------------- .../CommunityBranchParamsValidatorTest.java | 58 +- .../CommunityProjectBranchesLoaderTest.java | 4 +- .../scanner/ScannerWsClientWrapperTest.java | 117 ---- .../CommunityBranchSupportDelegateTest.java | 56 +- 68 files changed, 2409 insertions(+), 2576 deletions(-) delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/SonarqubeCompatibility.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/BranchCompatibility.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostProjectAnalysisTaskCompatibility.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Activity.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/ActivityPage.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Comment.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/User.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationLoaderCompatibility.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchParamsValidatorCompatibility.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapper.java rename src/main/java/com/github/mc1arke/sonarqube/plugin/{ce/pullrequest/gitlab/response/Discussion.java => server/pullrequest/ws/AlmSettingsWs.java} (52%) rename src/main/java/com/github/mc1arke/sonarqube/plugin/server/{ComponentKeyCompatibility.java => pullrequest/ws/AlmTypeMapper.java} (50%) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/SetBindingAlmSettingsWsAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/AlmSettingsWsAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListDefinitionsAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/CreateAzureAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/SetAzureBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/UpdateAzureAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/CreateBitBucketAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/SetBitbucketBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/UpdateBitbucketAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/CreateGithubAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/SetGithubBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/UpdateGitHubAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/CreateGitlabAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/SetGitlabBindingAction.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/UpdateGitlabAction.java delete mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapperTest.java diff --git a/build.gradle b/build.gradle index b32434f3a..709c0bd89 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -40,7 +40,7 @@ repositories { } } -def sonarqubeVersion = '7.8' +def sonarqubeVersion = '8.1.0.31237' def sonarqubeLibDir = "${projectDir}/sonarqube-lib" def sonarLibraries = "${sonarqubeLibDir}/sonarqube-${sonarqubeVersion}/lib" diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java index adcf2f739..22a40d550 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,21 +20,37 @@ import com.github.mc1arke.sonarqube.plugin.ce.CommunityBranchEditionProvider; import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.server.BitbucketServerPullRequestDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectBranchesLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectPullRequestsLoader; +import com.github.mc1arke.sonarqube.plugin.scanner.ScannerPullRequestPropertySensor; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchFeatureExtension; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchSupportDelegate; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.AlmSettingsWs; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.CountBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.DeleteAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.DeleteBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.GetBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.ListAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.ListDefinitionsAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure.CreateAzureAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure.SetAzureBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure.UpdateAzureAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket.CreateBitBucketAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket.SetBitbucketBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket.UpdateBitbucketAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github.CreateGithubAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github.SetGithubBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github.UpdateGitHubAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab.CreateGitlabAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab.SetGitlabBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab.UpdateGitlabAction; import org.sonar.api.CoreProperties; import org.sonar.api.Plugin; import org.sonar.api.PropertyType; import org.sonar.api.SonarQubeSide; import org.sonar.api.config.PropertyDefinition; -import org.sonar.api.resources.Qualifiers; import org.sonar.core.config.PurgeConstants; import org.sonar.core.extension.CoreExtension; @@ -43,11 +59,6 @@ */ public class CommunityBranchPlugin implements Plugin, CoreExtension { - private static final String PULL_REQUEST_CATEGORY_LABEL = "Pull Request"; - private static final String GITHUB_INTEGRATION_SUBCATEGORY_LABEL = "Integration With Github"; - private static final String GENERAL = "General"; - private static final String BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL = "Integration With Bitbucket"; - private static final String GITLAB_INTEGRATION_SUBCATEGORY_LABEL = "Integration With Gitlab"; @Override public String getName() { @@ -61,131 +72,46 @@ public void load(CoreExtension.Context context) { } else if (SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class, - /* org.sonar.db.purge.PurgeConfiguration uses the value for the this property if it's configured, so it only - needs to be specified here, but doesn't need any additional classes to perform the relevant purge/cleanup - */ + AlmSettingsWs.class, CountBindingAction.class, DeleteAction.class, + DeleteBindingAction.class, ListAction.class, ListDefinitionsAction.class, + GetBindingAction.class, + + CreateGithubAction.class, SetGithubBindingAction.class, UpdateGitHubAction.class, + + CreateAzureAction.class, SetAzureBindingAction.class, UpdateAzureAction.class, + + CreateBitBucketAction.class, SetBitbucketBindingAction.class, + UpdateBitbucketAction.class, + + CreateGitlabAction.class, SetGitlabBindingAction.class, UpdateGitlabAction.class, + + /* org.sonar.db.purge.PurgeConfiguration uses the value for the this property if it's configured, so it only + needs to be specified here, but doesn't need any additional classes to perform the relevant purge/cleanup + */ PropertyDefinition - .builder(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_SHORT_LIVING_BRANCHES) + .builder(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_BRANCHES_AND_PRS) .name("Number of days before purging inactive short living branches") .description( "Short living branches are permanently deleted when there are no analysis for the configured number of days.") - .category(CoreProperties.CATEGORY_GENERAL) - .subCategory(CoreProperties.SUBCATEGORY_DATABASE_CLEANER).defaultValue("30") - .type(PropertyType.INTEGER).build(), - - //the name and description shown on the UI are automatically loaded from core.properties so don't need to be specified here - PropertyDefinition.builder(CoreProperties.LONG_LIVED_BRANCHES_REGEX) - .onQualifiers(Qualifiers.PROJECT).category(CoreProperties.CATEGORY_GENERAL) - .subCategory(CoreProperties.SUBCATEGORY_BRANCHES) - .defaultValue(CommunityBranchConfigurationLoader.DEFAULT_BRANCH_REGEX).build() + .category(CoreProperties.CATEGORY_HOUSEKEEPING) + .subCategory(CoreProperties.SUBCATEGORY_GENERAL).defaultValue("30") + .type(PropertyType.INTEGER).build() ); - } - if (SonarQubeSide.COMPUTE_ENGINE == context.getRuntime().getSonarQubeSide() || - SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { - context.addExtensions( - PropertyDefinition.builder("sonar.pullrequest.provider").category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory("General").onQualifiers(Qualifiers.PROJECT).name("Provider") - .type(PropertyType.SINGLE_SELECT_LIST).options("Github", "BitbucketServer", "GitlabServer").build(), - - PropertyDefinition.builder("sonar.alm.github.app.privateKey.secured") - .category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GITHUB_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.APP).name("App Private Key").type(PropertyType.TEXT).build(), - - PropertyDefinition.builder("sonar.alm.github.app.name").category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory(GITHUB_INTEGRATION_SUBCATEGORY_LABEL).onQualifiers(Qualifiers.APP) - .name("App Name").defaultValue("SonarQube Community Pull Request Analysis") - .type(PropertyType.STRING).build(), - - PropertyDefinition.builder("sonar.alm.github.app.id").category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory(GITHUB_INTEGRATION_SUBCATEGORY_LABEL).onQualifiers(Qualifiers.APP) - .name("App ID").type(PropertyType.STRING).build(), - - PropertyDefinition.builder("sonar.pullrequest.github.repository") - .category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GITHUB_INTEGRATION_SUBCATEGORY_LABEL) - .onlyOnQualifiers(Qualifiers.PROJECT).name("Repository identifier") - .description("Example: SonarSource/sonarqube").type(PropertyType.STRING).build(), - - PropertyDefinition.builder("sonar.pullrequest.github.endpoint") - .category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GITHUB_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.APP).name("The API URL for a GitHub instance").description( - "The API url for a GitHub instance. https://api.github.com/ for github.com, https://github.company.com/api/ when using GitHub Enterprise") - .type(PropertyType.STRING).defaultValue("https://api.github.com").build(), - - PropertyDefinition.builder(PullRequestBuildStatusDecorator.PULL_REQUEST_COMMENT_SUMMARY_ENABLED).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GENERAL) - .onQualifiers(Qualifiers.PROJECT).name("Enable summary comment").description("This enables the summary comment (if implemented).") - .type(PropertyType.BOOLEAN).defaultValue("true").build(), - - PropertyDefinition.builder(PullRequestBuildStatusDecorator.PULL_REQUEST_FILE_COMMENT_ENABLED).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GENERAL) - .onQualifiers(Qualifiers.PROJECT).name("Enable file comment").description("This enables commenting (if implemented).").type(PropertyType.BOOLEAN) - .defaultValue("true").build(), - - PropertyDefinition.builder(PullRequestBuildStatusDecorator.PULL_REQUEST_DELETE_COMMENTS_ENABLED).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(GENERAL) - .onQualifiers(Qualifiers.PROJECT).name("Enable deleting comments").description("This cleans up the comments from previous runs (if implemented).") - .type(PropertyType.BOOLEAN).defaultValue("false").build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_URL).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.PROJECT).name("URL for Bitbucket (Server or Cloud) instance").description("Example: http://bitbucket.local").type(PropertyType.STRING).build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_TOKEN).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.PROJECT).name("The token for the user to comment to the PR on Bitbucket (Server or Cloud) instance") - .description("Token used for authentication and commenting to your Bitbucket instance").type(PropertyType.STRING).build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_COMMENT_USER_SLUG) - .category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL).onQualifiers(Qualifiers.PROJECT).name("Comment User Slug") - .description("User slug for the comment user. Needed only for comment deletion.").type(PropertyType.STRING).build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG) - .category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL).onlyOnQualifiers(Qualifiers.PROJECT).name("Repository Slug").description( - "Repository Slug see for example https://docs.atlassian.com/bitbucket-server/rest/latest/bitbucket-rest.html") - .type(PropertyType.STRING).build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_USER_SLUG).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL) - .onlyOnQualifiers(Qualifiers.PROJECT).name("User Slug").description("This is used for '/users' repos. Only set one User Slug or ProjectKey!") - .type(PropertyType.STRING).index(2).build(), - - PropertyDefinition.builder(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_PROJECT_KEY).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(BITBUCKET_INTEGRATION_SUBCATEGORY_LABEL) - .onlyOnQualifiers(Qualifiers.PROJECT).name("ProjectKey").description("This is used for '/projects' repos. Only set one User Slug or ProjectKey!") - .type(PropertyType.STRING).index(1).build(), - - PropertyDefinition.builder(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL) - .category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory(GITLAB_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.PROJECT) - .name("URL for Gitlab (Server or Cloud) instance") - .description("Example: https://ci-server.local/gitlab") - .type(PropertyType.STRING) - .defaultValue("https://gitlab.com") - .build(), - - PropertyDefinition.builder(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_TOKEN) - .category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory(GITLAB_INTEGRATION_SUBCATEGORY_LABEL) - .onQualifiers(Qualifiers.PROJECT) - .name("The token for the user to comment to the PR on Gitlab (Server or Cloud) instance") - .description("Token used for authentication and commenting to your Gitlab instance") - .type(PropertyType.STRING) - .build(), - - PropertyDefinition.builder(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG) - .category(PULL_REQUEST_CATEGORY_LABEL) - .subCategory(GITLAB_INTEGRATION_SUBCATEGORY_LABEL) - .onlyOnQualifiers(Qualifiers.PROJECT) - .name("Repository Slug for the Gitlab (Server or Cloud) instance") - .description("The repository slug can be either in the form of user/repo or it can be the Project ID") - .type(PropertyType.STRING) - .build() - ); } + } @Override public void define(Plugin.Context context) { + + if (SonarQubeSide.SCANNER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityProjectBranchesLoader.class, CommunityProjectPullRequestsLoader.class, - CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class); + CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class, + ScannerPullRequestPropertySensor.class); } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/SonarqubeCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/SonarqubeCompatibility.java deleted file mode 100644 index 1c13fb670..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/SonarqubeCompatibility.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.mc1arke.sonarqube.plugin; - -/** - * Provides a marker to indicate a class is implementing a feature to remain compatible with a specific version of Sonarqube. - * Classes should not normally implement this interface directly, but should implement an interface that extends from one - * of the sub-interfaces included in this interface. - * - * When creating the compatibility interfaces for individual features, mark methods as {@link Deprecated} if they're only - * there to provide compatibility with historic builds of SonarQube, otherwise put a comment on the method to make it clear - * it's a temporary marker to allow {@link Override} annotations and allow suporession of build warnings when building - * against older SonarQube versions. - * - * @author Michael Clarke - */ -public interface SonarqubeCompatibility { - - /** - * A marker for all features needed from SonarQube v7 that are no longer present in later v7 releases, or in v8, or - * features that have been specifically written to allow support for running a plugin against newer version when - * built against v7. - */ - interface Major7 extends SonarqubeCompatibility { - - /** - * A marker for all features needed from SonarQube v7.8 that are no longer present in v7.9. - */ - interface Minor8 extends Major7 { - - } - - /** - * A marker for all features needed from SonarQube v7.9 that are no longer present in v8.0 - */ - interface Minor9 extends Major7 { - - } - - } - - /** - * A marker for all features needed from SonarQube v8 that are no longer present in later v8 releases. - */ - interface Major8 extends SonarqubeCompatibility { - - /** - * A marker for all features needed from SonarQube v8.0 that are no longer present in v8.1. - */ - interface Minor0 extends Major8 { - - } - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/BranchCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/BranchCompatibility.java deleted file mode 100644 index 679d67465..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/BranchCompatibility.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.ce; - -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; - -public interface BranchCompatibility extends SonarqubeCompatibility { - - interface BranchCompatibilityMajor7 extends BranchCompatibility, SonarqubeCompatibility.Major7 { - - interface BranchCompatibilityMinor9 extends BranchCompatibilityMajor7, SonarqubeCompatibility.Major7.Minor9 { - - boolean isLegacyFeature(); - } - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranch.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranch.java index dbe0e34f5..dae22eaf6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranch.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranch.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -27,22 +27,22 @@ /** * @author Michael Clarke */ -public class CommunityBranch implements Branch, BranchCompatibility.BranchCompatibilityMajor7.BranchCompatibilityMinor9 { +public class CommunityBranch implements Branch { private final String name; private final BranchType branchType; private final boolean main; - private final String mergeBranchUuid; + private final String referenceBranchUuid; private final String pullRequestKey; private final String targetBranchName; - public CommunityBranch(String name, BranchType branchType, boolean main, String mergeBranchUuid, + public CommunityBranch(String name, BranchType branchType, boolean main, String referenceBranchUuid, String pullRequestKey, String targetBranchName) { super(); this.name = name; this.branchType = branchType; this.main = main; - this.mergeBranchUuid = mergeBranchUuid; + this.referenceBranchUuid = referenceBranchUuid; this.pullRequestKey = pullRequestKey; this.targetBranchName = targetBranchName; } @@ -63,13 +63,8 @@ public boolean isMain() { } @Override - public boolean isLegacyFeature() { - return false; - } - - @Override - public String getMergeBranchUuid() { - return mergeBranchUuid; + public String getReferenceBranchUuid() { + return referenceBranchUuid; } @Override diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java index a1c972299..a37d5462a 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,6 +29,7 @@ import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.server.project.Project; +import javax.annotation.Nonnull; import java.util.Optional; /** @@ -45,7 +46,7 @@ public CommunityBranchLoaderDelegate(DbClient dbClient, MutableAnalysisMetadataH } @Override - public void load(ScannerReport.Metadata metadata) { + public void load(@Nonnull ScannerReport.Metadata metadata) { Branch branch = load(metadata, metadataHolder.getProject(), dbClient); metadataHolder.setBranch(branch); @@ -67,7 +68,7 @@ private static Branch load(ScannerReport.Metadata metadata, Project project, DbC throw new IllegalStateException("Could not find main branch"); } } else { - String targetBranch = StringUtils.trimToNull(metadata.getMergeBranchName()); + String targetBranch = StringUtils.trimToNull(metadata.getReferenceBranchName()); ScannerReport.Metadata.BranchType branchType = metadata.getBranchType(); if (null == targetBranchName) { targetBranchName = targetBranch; @@ -75,9 +76,8 @@ private static Branch load(ScannerReport.Metadata metadata, Project project, DbC if (ScannerReport.Metadata.BranchType.PULL_REQUEST == branchType) { return createPullRequest(metadata, dbClient, branchName, projectUuid, targetBranch, targetBranchName); - } else if (ScannerReport.Metadata.BranchType.LONG == branchType || - ScannerReport.Metadata.BranchType.SHORT == branchType) { - return createBranch(dbClient, branchName, projectUuid, targetBranch, branchType, targetBranchName); + } else if (ScannerReport.Metadata.BranchType.BRANCH == branchType) { + return createBranch(dbClient, branchName, projectUuid, targetBranch, targetBranchName); } else { throw new IllegalStateException(String.format("Invalid branch type '%s'", branchType.name())); } @@ -100,7 +100,7 @@ private static Branch createPullRequest(ScannerReport.Metadata metadata, DbClien } private static Branch createBranch(DbClient dbClient, String branchName, String projectUuid, String targetBranch, - ScannerReport.Metadata.BranchType branchType, String targetBranchName) { + String targetBranchName) { String targetUuid; if (null == targetBranch) { targetUuid = projectUuid; @@ -113,8 +113,7 @@ private static Branch createBranch(DbClient dbClient, String branchName, String String.format("Could not find target branch '%s' in project", targetBranch)); } } - return new CommunityBranch(branchName, ScannerReport.Metadata.BranchType.LONG == branchType ? BranchType.LONG : - BranchType.SHORT, + return new CommunityBranch(branchName, BranchType.BRANCH, findBranchByKey(projectUuid, branchName, dbClient).map(BranchDto::isMain) .orElse(false), targetUuid, null, targetBranchName); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java index cdfbfa486..2919f60b3 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,8 +19,6 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; -import org.apache.commons.lang.StringUtils; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Heading; @@ -30,10 +28,12 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Node; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; +import org.apache.commons.lang.StringUtils; import org.sonar.api.ce.posttask.Analysis; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.ce.posttask.QualityGate.EvaluationStatus; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.measures.CoreMetrics; @@ -47,8 +47,11 @@ import org.sonar.core.issue.DefaultIssue; import org.sonar.server.measure.Rating; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; @@ -83,10 +86,11 @@ public class AnalysisDetails { private final Analysis analysis; private final Project project; private final Configuration configuration; + private final ScannerContext scannerContext; AnalysisDetails(BranchDetails branchDetails, PostAnalysisIssueVisitor postAnalysisIssueVisitor, QualityGate qualityGate, MeasuresHolder measuresHolder, Analysis analysis, Project project, - Configuration configuration, String publicRootURL) { + Configuration configuration, String publicRootURL, ScannerContext scannerContext) { super(); this.publicRootURL = publicRootURL; this.branchDetails = branchDetails; @@ -96,6 +100,7 @@ public class AnalysisDetails { this.analysis = analysis; this.project = project; this.configuration = configuration; + this.scannerContext = scannerContext; } public String getBranchName() { @@ -110,6 +115,10 @@ public QualityGate.Status getQualityGateStatus() { return qualityGate.getStatus(); } + public Optional getScannerProperty(String propertyName) { + return Optional.ofNullable(scannerContext.getProperties().get(propertyName)); + } + public String createAnalysisSummary(FormatterFactory formatterFactory) { BigDecimal newCoverage = @@ -119,7 +128,7 @@ public String createAnalysisSummary(FormatterFactory formatterFactory) { .map(BigDecimal::new) .orElse(null); - double coverage = findMeasure(CoreMetrics.COVERAGE_KEY).map(MeasureWrapper::getDoubleValue).orElse(0D); + double coverage = findMeasure(CoreMetrics.COVERAGE_KEY).map(Measure::getDoubleValue).orElse(0D); BigDecimal newDuplications = findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY) .filter(condition -> condition.getStatus() != EvaluationStatus.NO_VALUE) @@ -128,7 +137,7 @@ public String createAnalysisSummary(FormatterFactory formatterFactory) { .orElse(null); double duplications = - findMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY).map(MeasureWrapper::getDoubleValue).orElse(0D); + findMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY).map(Measure::getDoubleValue).orElse(0D); NumberFormat decimalFormat = new DecimalFormat("#0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @@ -184,8 +193,9 @@ public String createAnalysisSummary(FormatterFactory formatterFactory) { .map(i -> i + "% Duplicated Code") .orElse("No duplication information") + " (" + decimalFormat.format(duplications) + - "% Estimated after merge)"))), - new Link(publicRootURL + "/dashboard?id=" + URLEncoder.encode(project.getKey()) + "&pullRequest=" + branchDetails.getBranchName(), new Text("View in SonarQube"))); + "% Estimated after merge)"))), new Link( + publicRootURL + "/dashboard?id=" + encode(project.getKey(), StandardCharsets.UTF_8) + "&pullRequest=" + + branchDetails.getBranchName(), new Text("View in SonarQube"))); return formatterFactory.documentFormatter().format(document, formatterFactory); } @@ -206,9 +216,10 @@ public String createAnalysisIssueSummary(PostAnalysisIssueVisitor.ComponentIssue new Paragraph(new Text(String.format("**Type:** %s ", issue.type().name())), new Image(issue.type().name(), String.format("%s/checks/IssueType/%s.svg?sanitize=true", baseImageUrl, issue.type().name().toLowerCase()))), new Paragraph(new Text(String.format("**Severity:** %s ", issue.severity())), new Image(issue.severity(), String.format("%s/checks/Severity/%s.svg?sanitize=true", baseImageUrl, issue.severity().toLowerCase()))), new Paragraph(new Text(String.format("**Message:** %s", issue.getMessage()))), - effortNode, - resolutionNode, - new Link(publicRootURL + "/project/issues?id=" + URLEncoder.encode(project.getKey()) + "&pullRequest=" + branchDetails.getBranchName() + "&issues=" + issue.key() + "&open=" + issue.key(), new Text("View in SonarQube")) + effortNode, resolutionNode, new Link( + publicRootURL + "/project/issues?id=" + encode(project.getKey(), StandardCharsets.UTF_8) + + "&pullRequest=" + branchDetails.getBranchName() + "&issues=" + issue.key() + "&open=" + issue.key(), + new Text("View in SonarQube")) ); return formatterFactory.documentFormatter().format(document, formatterFactory); } @@ -276,11 +287,10 @@ private List findFailedConditions() { .collect(Collectors.toList()); } - private Optional findMeasure(String metricKey) { + private Optional findMeasure(String metricKey) { return measuresHolder.getMeasureRepository().getRawMeasure(measuresHolder.getTreeRootHolder().getRoot(), measuresHolder.getMetricRepository() - .getByKey(metricKey)) - .map(MeasureWrapper::new); + .getByKey(metricKey)); } public Optional findQualityGateCondition(String metricKey) { @@ -322,6 +332,14 @@ private static String format(QualityGate.Condition condition) { } } + private static String encode(String original, Charset charset) { + try { + return URLEncoder.encode(original, charset.name()); + } catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Could not encode input", ex); + } + } + public static class BranchDetails { private final String branchName; @@ -388,23 +406,4 @@ private String getImageName() { } } - private static class MeasureWrapper implements SonarqubeCompatibility.Major7.Minor9 { - - private final Measure measure; - - MeasureWrapper(Measure measure) { - super(); - this.measure = measure; - } - - Double getDoubleValue() { - try { - return (Double) Measure.class.getDeclaredMethod("getDoubleValue").invoke(measure); - } catch (ReflectiveOperationException ex) { - throw new IllegalStateException("Could not invoke getDoubleValue", ex); - } - } - - } - } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostProjectAnalysisTaskCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostProjectAnalysisTaskCompatibility.java deleted file mode 100644 index 54398f4ac..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostProjectAnalysisTaskCompatibility.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest; - -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; -import org.sonar.api.ce.posttask.PostProjectAnalysisTask; - -public interface PostProjectAnalysisTaskCompatibility extends SonarqubeCompatibility { - - interface PostProjectAnalysisTaskCompatibilityMajor8 - extends PostProjectAnalysisTaskCompatibility, SonarqubeCompatibility.Major8 { - - interface PostProjectAnalysisTaskCompatibilityMinor0 - extends PostProjectAnalysisTaskCompatibilityMajor8, SonarqubeCompatibility.Major8.Minor0 { - - String getDescription(); - - /** - * Setup and perform any post analysis task (e.g. pull request decoration) - * - * @param projectAnalysis the details of the task that was performed (quality gate, branches etc) - *

- * See {@link PostProjectAnalysisTask#finished(PostProjectAnalysisTask.ProjectAnalysis)} for implementation - * details - * @deprecated We can't introduce the new finished(Context context) method until we compile against - * SonarQube 8, since the Context class was only introduced at that point. Once our base is SonarQube 8 or - * above, we'll replace the implementation of this method with the non-deprecated equivalent. Marking this - * as Deprecated in the meantime so it's clear work will be needed here. - */ - @Deprecated - void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis); - - } - - } - -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java index 3b11e1d46..ba4fb9771 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,15 +18,16 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -public interface PullRequestBuildStatusDecorator { - - String PULL_REQUEST_COMMENT_SUMMARY_ENABLED = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.comment.summary.enabled"; - - String PULL_REQUEST_FILE_COMMENT_ENABLED = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.file.comment.enabled"; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; - String PULL_REQUEST_DELETE_COMMENTS_ENABLED = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.delete.comments.enabled"; +public interface PullRequestBuildStatusDecorator { String name(); - void decorateQualityGateStatus(AnalysisDetails analysisDetails); + void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto); + + ALM alm(); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index 6bd076d18..b559adf50 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -30,12 +30,16 @@ import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; import org.sonar.ce.task.projectanalysis.metric.MetricRepository; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.util.List; import java.util.Optional; -public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask, - PostProjectAnalysisTaskCompatibility.PostProjectAnalysisTaskCompatibilityMajor8.PostProjectAnalysisTaskCompatibilityMinor0 { +public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask { private static final Logger LOGGER = Loggers.get(PullRequestPostAnalysisTask.class); @@ -46,13 +50,14 @@ public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask, private final MetricRepository metricRepository; private final MeasureRepository measureRepository; private final TreeRootHolder treeRootHolder; + private final DbClient dbClient; public PullRequestPostAnalysisTask(Server server, ConfigurationRepository configurationRepository, List pullRequestDecorators, PostAnalysisIssueVisitor postAnalysisIssueVisitor, MetricRepository metricRepository, MeasureRepository measureRepository, - TreeRootHolder treeRootHolder) { + TreeRootHolder treeRootHolder, DbClient dbClient) { super(); this.server = server; this.configurationRepository = configurationRepository; @@ -61,6 +66,7 @@ public PullRequestPostAnalysisTask(Server server, this.metricRepository = metricRepository; this.measureRepository = measureRepository; this.treeRootHolder = treeRootHolder; + this.dbClient = dbClient; } @Override @@ -68,9 +74,9 @@ public String getDescription() { return "Pull Request Decoration"; } - @Deprecated @Override - public void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) { + public void finished(Context context) { + ProjectAnalysis projectAnalysis = context.getProjectAnalysis(); LOGGER.debug("found " + pullRequestDecorators.size() + " pull request decorators"); Optional optionalPullRequest = projectAnalysis.getBranch().filter(branch -> Branch.Type.PULL_REQUEST == branch.getType()); @@ -87,8 +93,32 @@ public void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) { Configuration configuration = configurationRepository.getConfiguration(); + ProjectAlmSettingDto projectAlmSettingDto; + Optional optionalAlmSettingDto; + try (DbSession dbSession = dbClient.openSession(false)) { + + Optional optionalProjectAlmSettingDto = + dbClient.projectAlmSettingDao().selectByProject(dbSession, projectAnalysis.getProject().getUuid()); + + if (!optionalProjectAlmSettingDto.isPresent()) { + LOGGER.debug("No ALM has been set on the current project"); + return; + } + + projectAlmSettingDto = optionalProjectAlmSettingDto.get(); + String almSettingUuid = projectAlmSettingDto.getAlmSettingUuid(); + optionalAlmSettingDto = dbClient.almSettingDao().selectByUuid(dbSession, almSettingUuid); + + } + + if (!optionalAlmSettingDto.isPresent()) { + LOGGER.warn("The ALM configured for this project could not be found"); + return; + } + + AlmSettingDto almSettingDto = optionalAlmSettingDto.get(); Optional optionalPullRequestDecorator = - findCurrentPullRequestStatusDecorator(configuration, pullRequestDecorators); + findCurrentPullRequestStatusDecorator(almSettingDto, pullRequestDecorators); if (!optionalPullRequestDecorator.isPresent()) { LOGGER.info("No decorator found for this Pull Request"); @@ -123,32 +153,26 @@ public void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) { postAnalysisIssueVisitor, qualityGate, new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder), analysis, - projectAnalysis.getProject(), configuration, server.getPublicRootUrl()); + projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), + projectAnalysis.getScannerContext()); PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get(); LOGGER.info("using pull request decorator" + pullRequestDecorator.name()); - pullRequestDecorator.decorateQualityGateStatus(analysisDetails); + pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } - private static Optional findCurrentPullRequestStatusDecorator( - Configuration configuration, List pullRequestDecorators) { - - Optional optionalImplementationName = configuration.get("sonar.pullrequest.provider"); - if (!optionalImplementationName.isPresent()) { - LOGGER.debug("'sonar.pullrequest.provider' property not set"); - return Optional.empty(); - } - - String implementationName = optionalImplementationName.get(); + private static Optional findCurrentPullRequestStatusDecorator( + AlmSettingDto almSetting, List pullRequestDecorators) { + ALM alm = almSetting.getAlm(); for (PullRequestBuildStatusDecorator pullRequestDecorator : pullRequestDecorators) { - if (pullRequestDecorator.name().equals(implementationName)) { + if (alm == pullRequestDecorator.alm()) { return Optional.of(pullRequestDecorator); } } - LOGGER.warn("No decorator could be found matching " + implementationName); + LOGGER.warn("No decorator could be found matching " + alm); return Optional.empty(); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Activity.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Activity.java deleted file mode 100644 index 05d586036..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Activity.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2019 Oliver Jedinger - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Activity implements Serializable { - private final int id; - - private final User user; - - private final Comment comment; - - @JsonCreator - public Activity(@JsonProperty("id") final int id, @JsonProperty("user") final User user, @JsonProperty("comment") final Comment comment) { - this.id = id; - this.user = user; - this.comment = comment; - } - - public int getId() { - return id; - } - - public User getUser() { - return user; - } - - public Comment getComment() { - return comment; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/ActivityPage.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/ActivityPage.java deleted file mode 100644 index 63eff791d..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/ActivityPage.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 Oliver Jedinger - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class ActivityPage implements Serializable { - private final int size; - - private final int limit; - - private final boolean isLastPage; - - private final int start; - - private final int nextPageStart; - - private final Activity[] values; - - @JsonCreator - public ActivityPage(@JsonProperty("size") final int size, @JsonProperty("limit") final int limit, @JsonProperty("isLastPage") final boolean isLastPage, @JsonProperty("start") final int start, @JsonProperty("nextPageStart") final int nextPageStart, @JsonProperty("values") final Activity[] values) { - this.size = size; - this.limit = limit; - this.isLastPage = isLastPage; - this.start = start; - this.nextPageStart = nextPageStart; - this.values = values; - } - - public int getSize() { - return size; - } - - public int getLimit() { - return limit; - } - - public boolean isLastPage() { - return isLastPage; - } - - public int getStart() { - return start; - } - - public int getNextPageStart() { - return nextPageStart; - } - - public Activity[] getValues() { - return values; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Comment.java deleted file mode 100644 index 97ebdbed4..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/Comment.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2019 Oliver Jedinger - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Comment implements Serializable { - private final int id; - - private final int version; - - private final String text; - - private final User author; - - @JsonCreator - public Comment(@JsonProperty("id") final int id, @JsonProperty("version") final int version, @JsonProperty("text") final String text, @JsonProperty("author") final User author) { - this.id = id; - this.version = version; - this.text = text; - this.author = author; - } - - public int getId() { - return id; - } - - public int getVersion() { - return version; - } - - public String getText() { - return text; - } - - public User getAuthor() { - return author; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/User.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/User.java deleted file mode 100644 index e0ef00055..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/User.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2019 Oliver Jedinger - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class User implements Serializable { - private final String name; - - private final String slug; - - @JsonCreator - public User(@JsonProperty("name") final String name, @JsonProperty("slug") final String slug) { - this.name = name; - this.slug = slug; - } - - public String getName() { - return name; - } - - public String getSlug() { - return slug; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java index 1d5f3a4b6..87852d68a 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Oliver Jedinger + * Copyright (C) 2020 Oliver Jedinger, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,12 +23,9 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity.Activity; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity.ActivityPage; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.Anchor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.FileComment; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.SummaryComment; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity.Comment; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.Diff; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.DiffLine; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.DiffPage; @@ -39,7 +36,6 @@ import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; @@ -47,16 +43,16 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; -import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,91 +61,44 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStatusDecorator { - public static final String PULL_REQUEST_BITBUCKET_URL = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.url"; - - public static final String PULL_REQUEST_BITBUCKET_TOKEN = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.token"; - - public static final String PULL_REQUEST_BITBUCKET_PROJECT_KEY = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.projectKey"; - - public static final String PULL_REQUEST_BITBUCKET_USER_SLUG = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.userSlug"; - - public static final String PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.repositorySlug"; - - public static final String PULL_REQUEST_BITBUCKET_COMMENT_USER_SLUG = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.bitbucket.comment.userSlug"; - private static final Logger LOGGER = Loggers.get(BitbucketServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList()); private static final String REST_API = "/rest/api/1.0/"; - private static final String USER_PR_API = "users/%s/repos/%s/pull-requests/%s/"; private static final String PROJECT_PR_API = "projects/%s/repos/%s/pull-requests/%s/"; private static final String COMMENTS_API = "comments"; private static final String DIFF_API = "diff"; - private static final String ACTIVITIES = "activities?limit=%s"; private static final String FULL_PR_COMMENT_API = "%s" + REST_API + PROJECT_PR_API + COMMENTS_API; - private static final String FULL_PR_COMMENT_USER_API = "%s" + REST_API + USER_PR_API + COMMENTS_API; - - private static final String FULL_PR_ACTIVITIES_API = "%s" + REST_API + PROJECT_PR_API + ACTIVITIES; - private static final String FULL_PR_ACTIVITIES_USER_API = "%s" + REST_API + USER_PR_API + ACTIVITIES; - private static final String FULL_PR_DIFF_API = "%s" + REST_API + PROJECT_PR_API + DIFF_API; - private static final String FULL_PR_DIFF_USER_API = "%s" + REST_API + USER_PR_API + DIFF_API; - - - private final ConfigurationRepository configurationRepository; - - public BitbucketServerPullRequestDecorator(ConfigurationRepository configurationRepository) { - super(); - this.configurationRepository = configurationRepository; - } @Override - public void decorateQualityGateStatus(AnalysisDetails analysisDetails) { + public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto) { LOGGER.info("starting to analyze with " + analysisDetails.toString()); try { - Configuration configuration = configurationRepository.getConfiguration(); - final String hostURL = getMandatoryProperty(PULL_REQUEST_BITBUCKET_URL, configuration); - final String apiToken = getMandatoryProperty(PULL_REQUEST_BITBUCKET_TOKEN, configuration); - final String repositorySlug = getMandatoryProperty(PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG, configuration); + final String hostURL = almSettingDto.getUrl(); + final String apiToken = almSettingDto.getPersonalAccessToken(); + final String repositorySlug = projectAlmSettingDto.getAlmSlug(); final String pullRequestId = analysisDetails.getBranchName(); - final String userSlug = configuration.get(PULL_REQUEST_BITBUCKET_USER_SLUG).orElse(StringUtils.EMPTY); - final String projectKey = configuration.get(PULL_REQUEST_BITBUCKET_PROJECT_KEY).orElse(StringUtils.EMPTY); - final String commentUserSlug = configuration.get(PULL_REQUEST_BITBUCKET_COMMENT_USER_SLUG).orElse(StringUtils.EMPTY); + final String projectKey = projectAlmSettingDto.getAlmRepo(); - final boolean summaryCommentEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_COMMENT_SUMMARY_ENABLED, configuration)); - final boolean fileCommentEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_FILE_COMMENT_ENABLED, configuration)); - final boolean deleteCommentsEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_DELETE_COMMENTS_ENABLED, configuration)); + String commentUrl = String.format(FULL_PR_COMMENT_API, hostURL, projectKey, repositorySlug, pullRequestId); + String diffUrl = String.format(FULL_PR_DIFF_API, hostURL, projectKey, repositorySlug, pullRequestId); - final String commentUrl; - final String activityUrl; - final String diffUrl; - if (StringUtils.isNotBlank(userSlug)) { - commentUrl = String.format(FULL_PR_COMMENT_USER_API, hostURL, userSlug, repositorySlug, pullRequestId); - diffUrl = String.format(FULL_PR_DIFF_USER_API, hostURL, userSlug, repositorySlug, pullRequestId); - activityUrl = String.format(FULL_PR_ACTIVITIES_USER_API, hostURL, userSlug, repositorySlug, pullRequestId, 250); - } else if (StringUtils.isNotBlank(projectKey)) { - commentUrl = String.format(FULL_PR_COMMENT_API, hostURL, projectKey, repositorySlug, pullRequestId); - diffUrl = String.format(FULL_PR_DIFF_API, hostURL, projectKey, repositorySlug, pullRequestId); - activityUrl = String.format(FULL_PR_ACTIVITIES_API, hostURL, projectKey, repositorySlug, pullRequestId, 250); - } else { - throw new IllegalStateException(String.format("Property userSlug (%s) for /user repo or projectKey (%s) for /projects repo needs to be set.", PULL_REQUEST_BITBUCKET_USER_SLUG, PULL_REQUEST_BITBUCKET_PROJECT_KEY)); - } LOGGER.debug(String.format("Comment URL is: %s ", commentUrl)); - LOGGER.debug(String.format("Activity URL is: %s ", activityUrl)); LOGGER.debug(String.format("Diff URL is: %s ", diffUrl)); Map headers = new HashMap<>(); headers.put("Authorization", String.format("Bearer %s", apiToken)); headers.put("Accept", "application/json"); - deleteComments(activityUrl, commentUrl, commentUserSlug, headers, deleteCommentsEnabled); String analysisSummary = analysisDetails.createAnalysisSummary(new MarkdownFormatterFactory()); StringEntity summaryCommentEntity = new StringEntity(new ObjectMapper().writeValueAsString(new SummaryComment(analysisSummary)), ContentType.APPLICATION_JSON); - postComment(commentUrl, headers, summaryCommentEntity, summaryCommentEnabled); + postComment(commentUrl, headers, summaryCommentEntity); DiffPage diffPage = getPage(diffUrl, headers, DiffPage.class); List componentIssues = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status())).collect(Collectors.toList()); @@ -166,7 +115,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails) { StringEntity fileCommentEntity = new StringEntity( new ObjectMapper().writeValueAsString(new FileComment(analysisIssueSummary, new Anchor(issueLine, issueType, issuePath, fileType))), ContentType.APPLICATION_JSON ); - postComment(commentUrl, headers, fileCommentEntity, fileCommentEnabled); + postComment(commentUrl, headers, fileCommentEntity); } } catch (IOException ex) { throw new IllegalStateException("Could not decorate Pull Request on Bitbucket Server", ex); @@ -206,66 +155,6 @@ private String getExtractIssueType(int issueLine, String issueType, List h return issueType; } - protected boolean deleteComments(String activityUrl, String commentUrl, String userSlug, Map headers, boolean deleteCommentsEnabled) { - if (!deleteCommentsEnabled) { - return false; - } - if (StringUtils.isEmpty(userSlug)) { - LOGGER.info("No comments deleted cause property comment.userSlug is not set."); - return false; - } - boolean commentsRemoved = false; - final ActivityPage activityPage = getPage(activityUrl, headers, ActivityPage.class); - if (activityPage != null) { - final List commentsToDelete = getCommentsToDelete(userSlug, activityPage); - LOGGER.debug(String.format("Deleting %s comments", commentsToDelete)); - for (Comment comment : commentsToDelete) { - try { - boolean commentDeleted = deleteComment(commentUrl, headers, comment); - if (commentDeleted) { - commentsRemoved = true; - } - } catch (IOException ex) { - LOGGER.error("Could not delete comment from Bitbucket Server", ex); - } - } - - } - return commentsRemoved; - } - - private boolean deleteComment(String commentUrl, Map headers, Comment comment) throws IOException { - boolean commentDeleted = false; - String deleteCommentUrl = commentUrl + "/%s?version=%s"; - HttpDelete httpDelete = new HttpDelete(String.format(deleteCommentUrl, comment.getId(), comment.getVersion())); - LOGGER.debug("delete " + comment.getId() + " " + comment.getVersion()); - for (Map.Entry entry : headers.entrySet()) { - httpDelete.addHeader(entry.getKey(), entry.getValue()); - } - try (CloseableHttpClient closeableHttpClient = HttpClients.createDefault()) { - HttpResponse deleteResponse = closeableHttpClient.execute(httpDelete); - if (null == deleteResponse) { - LOGGER.error("HttpResponse for deleting comment was null"); - } else if (deleteResponse.getStatusLine().getStatusCode() != 204) { - LOGGER.error(IOUtils.toString(deleteResponse.getEntity().getContent(), StandardCharsets.UTF_8.name())); - LOGGER.error("An error was returned in the response from the Bitbucket API. See the previous log messages for details"); - } else { - LOGGER.debug(String.format("Comment %s version %s deleted", comment.getId(), comment.getVersion())); - commentDeleted = true; - } - } - return commentDeleted; - } - - protected List getCommentsToDelete(String userSlug, ActivityPage activityPage) { - return Arrays.stream(activityPage.getValues()) - .filter(a -> a.getComment() != null) - .filter(a -> a.getComment().getAuthor() != null) - .filter(a -> userSlug.equals(a.getComment().getAuthor().getSlug())) - .map(Activity::getComment) - .collect(Collectors.toList()); - } - protected T getPage(String diffUrl, Map headers, Class type) { T page = null; try (CloseableHttpClient closeableHttpClient = HttpClients.createDefault()) { @@ -296,7 +185,8 @@ protected T getPage(String diffUrl, Map headers, Class ty return type.cast(page); } - protected boolean postComment(String commentUrl, Map headers, StringEntity requestEntity, boolean sendRequest) throws IOException { + protected boolean postComment(String commentUrl, Map headers, StringEntity requestEntity) + throws IOException { boolean commentPosted = false; HttpPost httpPost = new HttpPost(commentUrl); for (Map.Entry entry : headers.entrySet()) { @@ -304,31 +194,29 @@ protected boolean postComment(String commentUrl, Map headers, St } httpPost.setEntity(requestEntity); LOGGER.debug(EntityUtils.toString(requestEntity)); - if (sendRequest) { - try (CloseableHttpClient closeableHttpClient = HttpClients.createDefault()) { - HttpResponse httpResponse = closeableHttpClient.execute(httpPost); - if (null == httpResponse) { - LOGGER.error("HttpResponse for posting comment was null"); - } else if (httpResponse.getStatusLine().getStatusCode() != 201) { - HttpEntity entity = httpResponse.getEntity(); - LOGGER.error(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())); - } else { - HttpEntity entity = httpResponse.getEntity(); - LOGGER.debug(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())); - commentPosted = true; - } + try (CloseableHttpClient closeableHttpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = closeableHttpClient.execute(httpPost); + if (null == httpResponse) { + LOGGER.error("HttpResponse for posting comment was null"); + } else if (httpResponse.getStatusLine().getStatusCode() != 201) { + HttpEntity entity = httpResponse.getEntity(); + LOGGER.error(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())); + } else { + HttpEntity entity = httpResponse.getEntity(); + LOGGER.debug(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())); + commentPosted = true; } } return commentPosted; } - private static String getMandatoryProperty(String propertyName, Configuration configuration) { - return configuration.get(propertyName).orElseThrow(() -> new IllegalStateException( - String.format("%s must be specified in the project configuration", propertyName))); - } - @Override public String name() { return "BitbucketServer"; } + + @Override + public ALM alm() { + return ALM.BITBUCKET; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java index 01ba8b2e6..572f9c4ec 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,10 +19,13 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.security.GeneralSecurityException; public interface CheckRunProvider { - void createCheckRun(AnalysisDetails analysisDetails) throws IOException, GeneralSecurityException; + void createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto) throws IOException, GeneralSecurityException; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java index cddaf058b..086559b65 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,9 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; public class GithubPullRequestDecorator implements PullRequestBuildStatusDecorator { @@ -30,15 +33,21 @@ public GithubPullRequestDecorator(CheckRunProvider checkRunProvider) { } @Override - public void decorateQualityGateStatus(AnalysisDetails analysisDetails) { + public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto) { try { - checkRunProvider.createCheckRun(analysisDetails); + checkRunProvider.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); } catch (Exception ex) { throw new IllegalStateException("Could not decorate Pull Request on Github", ex); } } + @Override + public ALM alm() { + return ALM.GITHUB; + } + @Override public String name() { return "Github"; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java index 875227fa9..6918c3ba5 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -34,15 +34,13 @@ import io.aexp.nodes.graphql.InputObject; import io.aexp.nodes.graphql.internal.Error; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.config.Configuration; -import org.sonar.api.config.PropertyDefinition; -import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.platform.Server; import org.sonar.api.rule.Severity; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.net.URLEncoder; @@ -69,40 +67,30 @@ public class GraphqlCheckRunProvider implements CheckRunProvider { private final Clock clock; private final GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider; private final Server server; - private final ConfigurationRepository configurationRepository; - private final PropertyDefinitions propertyDefinitions; public GraphqlCheckRunProvider(Clock clock, GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider, - Server server, ConfigurationRepository configurationRepository, - PropertyDefinitions propertyDefinitions) { - this(new DefaultGraphqlProvider(), clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); + Server server) { + this(new DefaultGraphqlProvider(), clock, githubApplicationAuthenticationProvider, server); } GraphqlCheckRunProvider(GraphqlProvider graphqlProvider, Clock clock, GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider, - Server server, ConfigurationRepository configurationRepository, - PropertyDefinitions propertyDefinitions) { + Server server) { super(); this.graphqlProvider = graphqlProvider; this.clock = clock; this.githubApplicationAuthenticationProvider = githubApplicationAuthenticationProvider; this.server = server; - this.configurationRepository = configurationRepository; - this.propertyDefinitions = propertyDefinitions; } @Override - public void createCheckRun(AnalysisDetails analysisDetails) throws IOException, GeneralSecurityException { - Configuration configuration = configurationRepository.getConfiguration(); - String apiUrl = getMandatoryProperty("sonar.pullrequest.github.endpoint", configuration, propertyDefinitions); - String apiPrivateKey = - getMandatoryProperty("sonar.alm.github.app.privateKey.secured", configuration, propertyDefinitions); - String projectPath = - getMandatoryProperty("sonar.pullrequest.github.repository", configuration, propertyDefinitions); - String appId = getMandatoryProperty("sonar.alm.github.app.id", configuration, propertyDefinitions); - String appName = getMandatoryProperty("sonar.alm.github.app.name", configuration, propertyDefinitions); + public void createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto) throws IOException, GeneralSecurityException { + String apiUrl = almSettingDto.getUrl(); + String apiPrivateKey = almSettingDto.getPrivateKey(); + String projectPath = projectAlmSettingDto.getAlmSlug(); + String appId = almSettingDto.getAppId(); RepositoryAuthenticationToken repositoryAuthenticationToken = githubApplicationAuthenticationProvider.getInstallationToken(apiUrl, appId, apiPrivateKey, projectPath); @@ -138,7 +126,7 @@ public void createCheckRun(AnalysisDetails analysisDetails) throws IOException, InputObject repositoryInputObject = graphqlProvider.createInputObject().put("repositoryId", repositoryAuthenticationToken.getRepositoryId()) - .put("name", appName + " Results").put("headSha", analysisDetails.getCommitSha()) + .put("name", "Sonarqube Results").put("headSha", analysisDetails.getCommitSha()) .put("status", RequestableCheckStatusState.COMPLETED).put("conclusion", QualityGate.Status.OK == analysisDetails .getQualityGateStatus() ? @@ -196,12 +184,4 @@ private static CheckAnnotationLevel mapToGithubAnnotationLevel(String sonarqubeS } } - - private static String getMandatoryProperty(String propertyName, Configuration configuration, - PropertyDefinitions propertyDefinitions) { - return configuration.get(propertyName).orElseGet( - () -> Optional.ofNullable(propertyDefinitions.get(propertyName)).map(PropertyDefinition::defaultValue) - .map(v -> "".equals(v) ? null : v).orElseThrow(() -> new IllegalStateException( - String.format("%s must be specified in the project configuration", propertyName)))); - } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 3eedc6e41..983b4ee06 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Markus Heberling + * Copyright (C) 2020 Markus Heberling, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,20 +18,6 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,10 +25,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Commit; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Discussion; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.MergeRequest; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Note; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.User; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; import org.apache.commons.io.IOUtils; import org.apache.http.Header; @@ -50,106 +33,96 @@ import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.platform.Server; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public class GitlabServerPullRequestDecorator implements PullRequestBuildStatusDecorator { + public static final String PULLREQUEST_GITLAB_URL = + "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.url"; + public static final String PULLREQUEST_GITLAB_REPOSITORY_SLUG = + "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.repositorySlug"; + private static final Logger LOGGER = Loggers.get(GitlabServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList()); - public static final String PULLREQUEST_GITLAB_URL = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.url"; - public static final String PULLREQUEST_GITLAB_TOKEN = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.token"; - public static final String PULLREQUEST_GITLAB_REPOSITORY_SLUG = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.repositorySlug"; - - private final ConfigurationRepository configurationRepository; private final Server server; private final ScmInfoRepository scmInfoRepository; - public GitlabServerPullRequestDecorator(Server server, ConfigurationRepository configurationRepository, ScmInfoRepository scmInfoRepository) { + public GitlabServerPullRequestDecorator(Server server, ScmInfoRepository scmInfoRepository) { super(); - this.configurationRepository = configurationRepository; this.server = server; this.scmInfoRepository = scmInfoRepository; } @Override - public void decorateQualityGateStatus(AnalysisDetails analysis) { + public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto) { LOGGER.info("starting to analyze with " + analysis.toString()); String revision = analysis.getCommitSha(); try { - Configuration configuration = configurationRepository.getConfiguration(); - final String hostURL = getMandatoryProperty(PULLREQUEST_GITLAB_URL, configuration); - final String apiToken = getMandatoryProperty(PULLREQUEST_GITLAB_TOKEN, configuration); - final String repositorySlug = getMandatoryProperty(PULLREQUEST_GITLAB_REPOSITORY_SLUG, configuration); + final String hostURL = analysis.getScannerProperty(PULLREQUEST_GITLAB_URL).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate Gitlab merge request. '%s' has not been set in scanner properties", + PULLREQUEST_GITLAB_URL))); + final String apiToken = almSettingDto.getPersonalAccessToken(); + final String repositorySlug = analysis.getScannerProperty(PULLREQUEST_GITLAB_REPOSITORY_SLUG).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate Gitlab merge request. '%s' has not been set in scanner properties", + PULLREQUEST_GITLAB_REPOSITORY_SLUG))); final String pullRequestId = analysis.getBranchName(); - final boolean summaryCommentEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_COMMENT_SUMMARY_ENABLED, configuration)); - final boolean fileCommentEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_FILE_COMMENT_ENABLED, configuration)); - final boolean deleteCommentsEnabled = Boolean.parseBoolean(getMandatoryProperty(PULL_REQUEST_DELETE_COMMENTS_ENABLED, configuration)); - final String restURL = String.format("%s/api/v4", hostURL); - final String userURL = restURL + "/user"; final String projectURL = restURL + String.format("/projects/%s", URLEncoder.encode(repositorySlug, StandardCharsets.UTF_8.name())); final String statusUrl = projectURL + String.format("/statuses/%s", revision); final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); final String prCommitsURL = mergeRequestURl + "/commits"; final String mergeRequestDiscussionURL = mergeRequestURl + "/discussions"; - LOGGER.info(String.format("Status url is: %s ", statusUrl)); LOGGER.info(String.format("PR commits url is: %s ", prCommitsURL)); LOGGER.info(String.format("MR discussion url is: %s ", mergeRequestDiscussionURL)); - LOGGER.info(String.format("User url is: %s ", userURL)); Map headers = new HashMap<>(); headers.put("PRIVATE-TOKEN", apiToken); headers.put("Accept", "application/json"); - User user = getSingle(userURL, headers, User.class); - LOGGER.info(String.format("Using user: %s ", user.getUsername())); - - List commits = getPagedList(prCommitsURL, headers, deleteCommentsEnabled, new TypeReference>() { + List commits = getPagedList(prCommitsURL, headers, new TypeReference>() { }).stream().map(Commit::getId).collect(Collectors.toList()); MergeRequest mergeRequest = getSingle(mergeRequestURl, headers, MergeRequest.class); - List discussions = getPagedList(mergeRequestDiscussionURL, headers, deleteCommentsEnabled, new TypeReference>() { - }); - - LOGGER.info(String.format("Discussions in MR: %s ", discussions - .stream() - .map(Discussion::getId) - .collect(Collectors.joining(", ")))); - - for (Discussion discussion : discussions) { - for (Note note : discussion.getNotes()) { - if (!note.isSystem() && note.getAuthor() != null && note.getAuthor().getUsername().equals(user.getUsername())) { - //delete only our own comments - deleteCommitDiscussionNote(mergeRequestDiscussionURL + String.format("/%s/notes/%s", - discussion.getId(), - note.getId()), - headers, deleteCommentsEnabled); - } - } - } - QualityGate.Condition newCoverageCondition = analysis.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) .orElseThrow(() -> new IllegalStateException("Could not find New Coverage Condition in analysis")); String coverageValue = newCoverageCondition.getStatus().equals(QualityGate.EvaluationStatus.NO_VALUE) ? "0" : newCoverageCondition.getValue(); @@ -160,9 +133,9 @@ public void decorateQualityGateStatus(AnalysisDetails analysis) { String summaryComment = analysis.createAnalysisSummary(new MarkdownFormatterFactory()); List summaryContentParams = Collections.singletonList(new BasicNameValuePair("body", summaryComment)); - postStatus(statusUrl, headers, analysis, coverageValue, true); + postStatus(statusUrl, headers, analysis, coverageValue); - postCommitComment(mergeRequestDiscussionURL, headers, summaryContentParams, summaryCommentEnabled); + postCommitComment(mergeRequestDiscussionURL, headers, summaryContentParams); for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { String path = analysis.getSCMPathForIssue(issue).orElse(null); @@ -185,7 +158,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysis) { new BasicNameValuePair("position[new_path]", path), new BasicNameValuePair("position[new_line]", String.valueOf(issue.getIssue().getLine())), new BasicNameValuePair("position[position_type]", "text")); - postCommitComment(mergeRequestDiscussionURL, headers, fileContentParams, fileCommentEnabled); + postCommitComment(mergeRequestDiscussionURL, headers, fileContentParams); } else { LOGGER.info(String.format("Skipping %s:%d since the commit does not belong to the MR", path, issue.getIssue().getLine())); } @@ -197,34 +170,43 @@ public void decorateQualityGateStatus(AnalysisDetails analysis) { } + @Override + public ALM alm() { + return ALM.GITLAB; + } + private X getSingle(String userURL, Map headers, Class type) throws IOException { HttpGet httpGet = new HttpGet(userURL); for (Map.Entry entry : headers.entrySet()) { httpGet.addHeader(entry.getKey(), entry.getValue()); } - HttpResponse httpResponse = HttpClients.createDefault().execute(httpGet); - if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { - LOGGER.error(httpResponse.toString()); - LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); - throw new IllegalStateException("An error was returned in the response from the Gitlab API. See the previous log messages for details"); - } else if (null != httpResponse) { - LOGGER.debug(httpResponse.toString()); - HttpEntity entity = httpResponse.getEntity(); - X user = new ObjectMapper() + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpGet); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error(httpResponse.toString()); + LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException( + "An error was returned in the response from the Gitlab API. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug(httpResponse.toString()); + HttpEntity entity = httpResponse.getEntity(); + X user = new ObjectMapper() .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .readValue(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8), type); - LOGGER.info(type + " received"); + LOGGER.info(type + " received"); - return user; - } else { - throw new IOException("No response reveived"); + return user; + } else { + throw new IOException("No response reveived"); + } } } - private List getPagedList(String commitDiscussionURL, Map headers, boolean sendRequest, TypeReference> typeRef) throws IOException { + private List getPagedList(String commitDiscussionURL, Map headers, + TypeReference> typeRef) throws IOException { HttpGet httpGet = new HttpGet(commitDiscussionURL); for (Map.Entry entry : headers.entrySet()) { httpGet.addHeader(entry.getKey(), entry.getValue()); @@ -232,8 +214,8 @@ private List getPagedList(String commitDiscussionURL, Map List discussions = new ArrayList<>(); - if (sendRequest) { - HttpResponse httpResponse = HttpClients.createDefault().execute(httpGet); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpGet); if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { LOGGER.error(httpResponse.toString()); LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); @@ -251,29 +233,15 @@ private List getPagedList(String commitDiscussionURL, Map Optional nextURL = getNextUrl(httpResponse); if (nextURL.isPresent()) { LOGGER.info("Getting next page"); - discussions.addAll(getPagedList(nextURL.get(), headers, sendRequest, typeRef)); + discussions.addAll(getPagedList(nextURL.get(), headers, typeRef)); } } } return discussions; } - private void deleteCommitDiscussionNote(String commitDiscussionNoteURL, Map headers, boolean sendRequest) throws IOException { - //https://docs.gitlab.com/ee/api/discussions.html#delete-a-commit-thread-note - HttpDelete httpDelete = new HttpDelete(commitDiscussionNoteURL); - for (Map.Entry entry : headers.entrySet()) { - httpDelete.addHeader(entry.getKey(), entry.getValue()); - } - - if (sendRequest) { - LOGGER.info("Deleting {} with headers {}", commitDiscussionNoteURL, headers); - - HttpResponse httpResponse = HttpClients.createDefault().execute(httpDelete); - validateGitlabResponse(httpResponse, 204, "Commit discussions note deleted"); - } - } - - private void postCommitComment(String commitCommentUrl, Map headers, List params, boolean sendRequest) throws IOException { + private void postCommitComment(String commitCommentUrl, Map headers, List params) + throws IOException { //https://docs.gitlab.com/ee/api/commits.html#post-comment-to-commit HttpPost httpPost = new HttpPost(commitCommentUrl); for (Map.Entry entry : headers.entrySet()) { @@ -281,15 +249,16 @@ private void postCommitComment(String commitCommentUrl, Map head } httpPost.setEntity(new UrlEncodedFormEntity(params)); - if (sendRequest) { - LOGGER.info("Posting {} with headers {} to {}", params, headers, commitCommentUrl); + LOGGER.info("Posting {} with headers {} to {}", params, headers, commitCommentUrl); - HttpResponse httpResponse = HttpClients.createDefault().execute(httpPost); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPost); validateGitlabResponse(httpResponse, 201, "Comment posted"); } } - private void postStatus(String statusPostUrl, Map headers, AnalysisDetails analysis, String coverage, boolean sendRequest) throws IOException{ + private void postStatus(String statusPostUrl, Map headers, AnalysisDetails analysis, + String coverage) throws IOException { //See https://docs.gitlab.com/ee/api/commits.html#post-the-build-status-to-a-commit statusPostUrl += "?name=SonarQube"; String status = (analysis.getQualityGateStatus() == QualityGate.Status.OK ? "success" : "failed"); @@ -307,8 +276,9 @@ private void postStatus(String statusPostUrl, Map headers, Analy for (Map.Entry entry : headers.entrySet()) { httpPost.addHeader(entry.getKey(), entry.getValue()); } - if (sendRequest) { - HttpResponse httpResponse = HttpClients.createDefault().execute(httpPost); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPost); if (null != httpResponse && httpResponse.toString().contains("Cannot transition status")) { // Workaround for https://gitlab.com/gitlab-org/gitlab-ce/issues/25807 LOGGER.debug("Transition status is already {}", status); @@ -329,11 +299,6 @@ private void validateGitlabResponse(HttpResponse httpResponse, int expectedStatu } } - private static String getMandatoryProperty(String propertyName, Configuration configuration) { - return configuration.get(propertyName).orElseThrow(() -> new IllegalStateException( - String.format("%s must be specified in the project configuration", propertyName))); - } - private static Optional getNextUrl(HttpResponse httpResponse) { Header linkHeader = httpResponse.getFirstHeader("Link"); if (linkHeader != null) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java deleted file mode 100644 index edb22c9f9..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2019 Markus Heberling - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Note { - private final long id; - - private final boolean system; - - private final User author; - - @JsonCreator - public Note(@JsonProperty("id") long id, @JsonProperty("system") boolean system, @JsonProperty("author") User author) { - this.id = id; - this.system = system; - this.author = author; - } - - public long getId() { - return id; - } - - public boolean isSystem() { - return system; - } - - public User getAuthor() { - return author; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java deleted file mode 100644 index c96d0ebf9..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2019 Markus Heberling - * - * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class User { - private final String username; - - @JsonCreator - public User(@JsonProperty("username") String username) { - this.username = username; - } - - public String getUsername() { - return username; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationLoaderCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationLoaderCompatibility.java deleted file mode 100644 index 451be0659..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationLoaderCompatibility.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.scanner; - -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; -import org.sonar.scanner.scan.branch.BranchConfiguration; -import org.sonar.scanner.scan.branch.ProjectBranches; -import org.sonar.scanner.scan.branch.ProjectPullRequests; - -import java.util.Map; -import java.util.function.Supplier; - -public interface BranchConfigurationLoaderCompatibility extends SonarqubeCompatibility { - - interface BranchConfigurationLoaderCompatibilityMajor7 extends SonarqubeCompatibility.Major7, BranchConfigurationLoaderCompatibility { - - interface BranchConfigurationLoaderCompatibilityMinor8 extends BranchConfigurationLoaderCompatibilityMajor7, SonarqubeCompatibility.Major7.Minor8 { - - BranchConfiguration load(Map localSettings, Supplier> supplier, - ProjectBranches projectBranches, ProjectPullRequests projectPullRequests); - - } - - interface BranchConfigurationLoaderCompatibilityMinor9 extends BranchParamsValidatorCompatibility.BranchParamsValidatorCompatibilityMajor7, SonarqubeCompatibility.Major7.Minor9 { - - BranchConfiguration load(Map localSettings, ProjectBranches projectBranches, - ProjectPullRequests pullRequests); - } - - } - - -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchParamsValidatorCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchParamsValidatorCompatibility.java deleted file mode 100644 index 453700f23..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchParamsValidatorCompatibility.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.scanner; - -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; - -import java.util.List; - -public interface BranchParamsValidatorCompatibility extends SonarqubeCompatibility { - - interface BranchParamsValidatorCompatibilityMajor7 extends BranchParamsValidatorCompatibility, SonarqubeCompatibility.Major7 { - - interface BranchParamsValidatorCompatibilityMinor9 extends BranchParamsValidatorCompatibilityMajor7, SonarqubeCompatibility.Major7.Minor9 { - - void validate(List validationMessages, String deprecatedBranchName); - - } - - } - - interface BranchParamsValidatorCompatibilityMajor8 extends SonarqubeCompatibility.Major8, BranchParamsValidatorCompatibility { - - interface BranchParamsValidatorCompatibilityMinor0 extends BranchParamsValidatorCompatibilityMajor8, SonarqubeCompatibility.Major8.Minor0 { - - void validate(List validationMessages); - } - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfiguration.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfiguration.java index 337f8a0b1..1d890292e 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfiguration.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,16 +28,16 @@ public class CommunityBranchConfiguration implements BranchConfiguration { private final String branchName; private final BranchType branchType; - private final String longLivingSonarReferenceBranch; + private final String referenceBranchName; private final String targetScmBranch; private final String pullRequestKey; - /*package*/ CommunityBranchConfiguration(String branchName, BranchType branchType, - String longLivingSonarReferenceBranch, String targetScmBranch, + /*package*/ CommunityBranchConfiguration(String branchName, BranchType branchType, String referenceBranchName, + String targetScmBranch, String pullRequestKey) { this.branchName = branchName; this.branchType = branchType; - this.longLivingSonarReferenceBranch = longLivingSonarReferenceBranch; + this.referenceBranchName = referenceBranchName; this.targetScmBranch = targetScmBranch; this.pullRequestKey = pullRequestKey; } @@ -53,8 +53,8 @@ public String branchName() { } @Override - public String longLivingSonarReferenceBranch() { - return longLivingSonarReferenceBranch; + public String referenceBranchName() { + return referenceBranchName; } @Override diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java index d43d168be..63aa23b25 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,6 @@ package com.github.mc1arke.sonarqube.plugin.scanner; import org.apache.commons.lang.StringUtils; -import org.sonar.api.CoreProperties; import org.sonar.api.utils.MessageException; import org.sonar.core.config.ScannerProperties; import org.sonar.scanner.scan.branch.BranchConfiguration; @@ -34,15 +33,11 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; /** * @author Michael Clarke */ -public class CommunityBranchConfigurationLoader implements BranchConfigurationLoader, BranchConfigurationLoaderCompatibility.BranchConfigurationLoaderCompatibilityMajor7.BranchConfigurationLoaderCompatibilityMinor8, BranchConfigurationLoaderCompatibility.BranchConfigurationLoaderCompatibilityMajor7.BranchConfigurationLoaderCompatibilityMinor9 { - - public static final String DEFAULT_BRANCH_REGEX = "(branch|release).*"; - +public class CommunityBranchConfigurationLoader implements BranchConfigurationLoader { private static final Set BRANCH_ANALYSIS_PARAMETERS = new HashSet<>(Arrays.asList(ScannerProperties.BRANCH_NAME, ScannerProperties.BRANCH_TARGET)); @@ -51,34 +46,6 @@ public class CommunityBranchConfigurationLoader implements BranchConfigurationLo Arrays.asList(ScannerProperties.PULL_REQUEST_BRANCH, ScannerProperties.PULL_REQUEST_KEY, ScannerProperties.PULL_REQUEST_BASE)); - @Override - public BranchConfiguration load(Map localSettings, Supplier> supplier, - ProjectBranches projectBranches, ProjectPullRequests projectPullRequests) { - if (projectBranches.isEmpty()) { - if (isTargetingDefaultBranch(localSettings)) { - return new DefaultBranchConfiguration(); - } else { - // it would be nice to identify the 'primary' branch directly, but different projects work differently: using any of master, develop, main etc as primary - // A project/global configuration entry could be used to drive this in the future, but the current documented SonarQube parameters need followed for now - throw MessageException - .of("No branches currently exist in this project. Please scan the main branch without passing any branch parameters."); - } - } - if (BRANCH_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { - return createBranchConfiguration(localSettings.get(ScannerProperties.BRANCH_NAME), - localSettings.get(ScannerProperties.BRANCH_TARGET), - supplier.get().get(CoreProperties.LONG_LIVED_BRANCHES_REGEX), - projectBranches); - } else if (PULL_REQUEST_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { - return createPullRequestConfiguration(localSettings.get(ScannerProperties.PULL_REQUEST_KEY), - localSettings.get(ScannerProperties.PULL_REQUEST_BRANCH), - localSettings.get(ScannerProperties.PULL_REQUEST_BASE), - projectBranches); - } - - return new DefaultBranchConfiguration(); - } - @Override public BranchConfiguration load(Map localSettings, ProjectBranches projectBranches, ProjectPullRequests pullRequests) { @@ -95,7 +62,6 @@ public BranchConfiguration load(Map localSettings, ProjectBranch if (BRANCH_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { return createBranchConfiguration(localSettings.get(ScannerProperties.BRANCH_NAME), localSettings.get(ScannerProperties.BRANCH_TARGET), - localSettings.get(CoreProperties.LONG_LIVED_BRANCHES_REGEX), projectBranches); } else if (PULL_REQUEST_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { return createPullRequestConfiguration(localSettings.get(ScannerProperties.PULL_REQUEST_KEY), @@ -115,7 +81,6 @@ private static boolean isTargetingDefaultBranch(Map localSetting } private static BranchConfiguration createBranchConfiguration(String branchName, String branchTarget, - String longLivedBranchesRegex, ProjectBranches branches) { if (null == branchTarget || branchTarget.isEmpty()) { branchTarget = branches.defaultBranchName(); @@ -124,28 +89,12 @@ private static BranchConfiguration createBranchConfiguration(String branchName, BranchInfo existingBranch = branches.get(branchName); if (null == existingBranch) { - final BranchType branchType = computeBranchType(longLivedBranchesRegex, branchName); final BranchInfo targetBranch = findTargetBranch(branchTarget, branches); - return new CommunityBranchConfiguration(branchName, branchType, targetBranch.name(), branchTarget, null); - } - - if (BranchType.LONG == existingBranch.type()) { - return new CommunityBranchConfiguration(branchName, existingBranch.type(), branchName, null, null); - } else { - return new CommunityBranchConfiguration(branchName, existingBranch.type(), branchTarget, branchTarget, + return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, targetBranch.name(), branchTarget, null); } - } - private static BranchType computeBranchType(String longLivedBranchesRegex, String branchName) { - if (null == longLivedBranchesRegex) { - longLivedBranchesRegex = DEFAULT_BRANCH_REGEX; - } - if (branchName.matches(longLivedBranchesRegex)) { - return BranchType.LONG; - } else { - return BranchType.SHORT; - } + return new CommunityBranchConfiguration(branchName, existingBranch.type(), branchName, null, null); } private static BranchConfiguration createPullRequestConfiguration(String pullRequestKey, String pullRequestBranch, @@ -168,13 +117,7 @@ private static BranchInfo findTargetBranch(String targetBranch, ProjectBranches String.format("Target branch '%s' does not exist", targetBranch))); } - if (BranchType.LONG == target.type()) { - return target; - } else { - throw MessageException.of("Could not target requested branch", new IllegalStateException( - String.format("Expected branch type of %s but got %s", BranchType.LONG.name(), - target.type().name()))); - } + return target; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidator.java index 84e881f74..6d175b291 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,8 +18,6 @@ */ package com.github.mc1arke.sonarqube.plugin.scanner; -import org.sonar.core.config.ScannerProperties; -import org.sonar.scanner.bootstrap.GlobalConfiguration; import org.sonar.scanner.scan.branch.BranchParamsValidator; import java.util.List; @@ -27,24 +25,8 @@ /** * @author Michael Clarke */ -public class CommunityBranchParamsValidator implements BranchParamsValidator, BranchParamsValidatorCompatibility.BranchParamsValidatorCompatibilityMajor7.BranchParamsValidatorCompatibilityMinor9, BranchParamsValidatorCompatibility.BranchParamsValidatorCompatibilityMajor8.BranchParamsValidatorCompatibilityMinor0 { +public class CommunityBranchParamsValidator implements BranchParamsValidator { - private final GlobalConfiguration globalConfiguration; - - public CommunityBranchParamsValidator(GlobalConfiguration globalConfiguration) { - super(); - this.globalConfiguration = globalConfiguration; - } - - @Override - public void validate(List validationMessages, String deprecatedBranchName) { - if (null != deprecatedBranchName && (globalConfiguration.hasKey(ScannerProperties.BRANCH_NAME) || - globalConfiguration.hasKey(ScannerProperties.BRANCH_TARGET))) { - validationMessages.add(String.format( - "The legacy 'sonar.branch' parameter cannot be used at the same time as '%s' or '%s'", - ScannerProperties.BRANCH_NAME, ScannerProperties.BRANCH_TARGET)); - } - } @Override public void validate(List validationMessages) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoader.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoader.java index c5579fb63..b2f51526e 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoader.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -48,12 +48,12 @@ public class CommunityProjectBranchesLoader implements ProjectBranchesLoader { String.format("/%s/%s?%s=", ProjectBranchesParameters.CONTROLLER, ProjectBranchesParameters.ACTION_LIST, ProjectBranchesParameters.PARAM_PROJECT); - private final ScannerWsClientWrapper scannerWsClient; + private final ScannerWsClient scannerWsClient; private final Gson gson; public CommunityProjectBranchesLoader(ScannerWsClient scannerWsClient) { super(); - this.scannerWsClient = new ScannerWsClientWrapper(scannerWsClient); + this.scannerWsClient = scannerWsClient; this.gson = GsonHelper.create(); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectPullRequestsLoader.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectPullRequestsLoader.java index 1fd876e5f..bc79505b9 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectPullRequestsLoader.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectPullRequestsLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -54,12 +54,12 @@ public class CommunityProjectPullRequestsLoader implements ProjectPullRequestsLo private static final Logger LOGGER = Loggers.get(CommunityProjectPullRequestsLoader.class); private static final String PROJECT_PULL_REQUESTS_URL = "/api/project_pull_requests/list?project="; - private final ScannerWsClientWrapper scannerWsClient; + private final ScannerWsClient scannerWsClient; private final Gson gson; public CommunityProjectPullRequestsLoader(ScannerWsClient scannerWsClient) { super(); - this.scannerWsClient = new ScannerWsClientWrapper(scannerWsClient); + this.scannerWsClient = scannerWsClient; this.gson = new GsonBuilder().registerTypeAdapter(PullRequestInfo.class, createPullRequestInfoJsonDeserialiser()) .create(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java new file mode 100644 index 000000000..660c5a61d --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.scanner; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabServerPullRequestDecorator; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.utils.System2; + +import java.util.Optional; + +public class ScannerPullRequestPropertySensor implements Sensor { + + private final System2 system2; + + public ScannerPullRequestPropertySensor(System2 system2) { + super(); + this.system2 = system2; + } + + @Override + public void describe(SensorDescriptor sensorDescriptor) { + sensorDescriptor.name(getClass().getName()); + } + + @Override + public void execute(SensorContext sensorContext) { + if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { + Optional.ofNullable(system2.envVariable("CI_API_V4_URL")).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL, v)); + Optional.ofNullable(system2.envVariable("CI_PROJECT_PATH")).ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, v)); + } else { + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)) + .ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, + v)); + } + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapper.java deleted file mode 100644 index 7fd35ec9d..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.scanner; - -import org.sonar.scanner.bootstrap.ScannerWsClient; -import org.sonarqube.ws.client.WsRequest; -import org.sonarqube.ws.client.WsResponse; - -import java.lang.reflect.InvocationTargetException; - -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; - -/** - * Provides a way of invoking {@link ScannerWsClient} between SonarQube versions where it changed from being a class - * to being an interface. - */ -/*package*/ class ScannerWsClientWrapper implements SonarqubeCompatibility.Major7.Minor9 { - - private final Object wsClient; - - /*package*/ ScannerWsClientWrapper(Object wsClient) { - this.wsClient = wsClient; - } - - WsResponse call(WsRequest request) { - try { - return (WsResponse) wsClient.getClass().getMethod("call", WsRequest.class).invoke(wsClient, request); - } catch (ReflectiveOperationException ex) { - handleIfInvocationException(ex); - throw new IllegalStateException("Could not execute ScannerWsClient", ex); - } - } - - private static void handleIfInvocationException(ReflectiveOperationException ex) { - if (!(ex instanceof InvocationTargetException)) { - return; - } - Throwable cause = ex.getCause(); - if (cause instanceof Error) { - throw (Error) cause; - } else if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegate.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegate.java index 76647ff54..997ca50c6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegate.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -72,15 +72,12 @@ public CommunityComponentKey createComponentKey(String projectKey, Map getDeprecatedBranchName() { - return Optional.ofNullable(deprecatedBranchName); - } - @Override public Optional getBranch() { return Optional.ofNullable(branch); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmSettingsWs.java similarity index 52% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmSettingsWs.java index 17b487f1e..e4b93497c 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmSettingsWs.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Markus Heberling + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,29 +16,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; +package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.WebService; -public class Discussion { - private final String id; +import java.util.List; - private final List notes; +public class AlmSettingsWs implements WebService { + private final List actions; - @JsonCreator - public Discussion(@JsonProperty("id") String id, @JsonProperty("notes") List notes) { - this.id = id; - this.notes = notes; + public AlmSettingsWs(List actions) { + super(); + this.actions = actions; } - public String getId() { - return id; - } + @Override + public void define(Context context) { + NewController controller = context.createController("api/alm_settings"); + + for (AlmSettingsWsAction action : actions) { + action.define(controller); + } - public List getNotes() { - return notes; + controller.done(); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/ComponentKeyCompatibility.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmTypeMapper.java similarity index 50% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/server/ComponentKeyCompatibility.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmTypeMapper.java index 36efee88e..a390aaaf0 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/ComponentKeyCompatibility.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/AlmTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,19 +16,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.server; +package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws; -import com.github.mc1arke.sonarqube.plugin.SonarqubeCompatibility; +import org.sonar.db.alm.setting.ALM; +import org.sonarqube.ws.AlmSettings; -import java.util.Optional; +public final class AlmTypeMapper { -public interface ComponentKeyCompatibility extends SonarqubeCompatibility { - - interface ComponentKeyCompatibilityMajor7 extends ComponentKeyCompatibility, SonarqubeCompatibility.Major7 { - - interface ComponentKeyCompatibilityMinor9 extends ComponentKeyCompatibilityMajor7, SonarqubeCompatibility.Major7.Minor9 { + private AlmTypeMapper() { + super(); + } - Optional getDeprecatedBranchName(); + public static AlmSettings.Alm toAlmWs(ALM alm) { + switch (alm) { + case AZURE_DEVOPS: + return AlmSettings.Alm.azure; + case BITBUCKET: + return AlmSettings.Alm.bitbucket; + case GITHUB: + return AlmSettings.Alm.github; + case GITLAB: + return AlmSettings.Alm.gitlab; + default: + throw new IllegalStateException(String.format("Unknown ALM '%s'", alm.name())); } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/SetBindingAlmSettingsWsAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/SetBindingAlmSettingsWsAction.java new file mode 100644 index 000000000..bde6121a6 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/SetBindingAlmSettingsWsAction.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +import static org.sonar.api.web.UserRole.ADMIN; + +public abstract class SetBindingAlmSettingsWsAction extends AlmSettingsWsAction { + + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public SetBindingAlmSettingsWsAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super(dbClient); + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + protected ComponentDto getProject(DbSession dbSession, String projectKey) { + ComponentDto project = componentFinder.getByKey(dbSession, projectKey); + userSession.checkComponentPermission(ADMIN, project); + return project; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/AlmSettingsWsAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/AlmSettingsWsAction.java new file mode 100644 index 000000000..6e11c9a63 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/AlmSettingsWsAction.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.ws.WsAction; + +import static java.lang.String.format; + +public abstract class AlmSettingsWsAction implements WsAction { + + private final DbClient dbClient; + + protected AlmSettingsWsAction(DbClient dbClient) { + super(); + this.dbClient = dbClient; + } + + protected AlmSettingDto getAlmSetting(DbSession dbSession, String almSetting) { + return dbClient.almSettingDao().selectByKey(dbSession, almSetting) + .orElseThrow(() -> new NotFoundException(format("ALM setting '%s' could not be found", almSetting))); + } + + protected void checkAlmSettingDoesNotAlreadyExist(DbSession dbSession, String almSetting) { + dbClient.almSettingDao().selectByKey(dbSession, almSetting).ifPresent(a -> { + throw new IllegalArgumentException(format("ALM setting '%s' already exists", a.getKey())); + }); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java new file mode 100644 index 000000000..09bd1b394 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.AlmSettings.CountBindingWsResponse; + +import static org.sonar.server.ws.WsUtils.writeProtobuf; + + +public class CountBindingAction extends AlmSettingsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + + private final DbClient dbClient; + private final UserSession userSession; + + public CountBindingAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("count_binding").setHandler(this); + + action.createParam(PARAM_ALM_SETTING).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSetting = getAlmSetting(dbSession, almSettingKey); + int projectsBound = dbClient.projectAlmSettingDao().countByAlmSetting(dbSession, almSetting); + CountBindingWsResponse.Builder builder = + CountBindingWsResponse.newBuilder().setKey(almSetting.getKey()).setProjects(projectsBound); + + writeProtobuf(builder.build(), request, response); + } + + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteAction.java new file mode 100644 index 000000000..4c44d9ead --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteAction.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +public class DeleteAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + + private final DbClient dbClient; + private final UserSession userSession; + + public DeleteAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("delete").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = getAlmSetting(dbSession, key); + dbClient.projectAlmSettingDao().deleteByAlmSetting(dbSession, almSettingDto); + dbClient.almSettingDao().delete(dbSession, almSettingDto); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteBindingAction.java new file mode 100644 index 000000000..f58dc2efa --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/DeleteBindingAction.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +import static org.sonar.api.web.UserRole.ADMIN; + +public class DeleteBindingAction extends AlmSettingsWsAction { + + private static final String PARAM_PROJECT = "project"; + + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + public DeleteBindingAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("delete_binding").setPost(true).setHandler(this); + + action.createParam(PARAM_PROJECT).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = componentFinder.getByKey(dbSession, projectKey); + userSession.checkComponentPermission(ADMIN, project); + dbClient.projectAlmSettingDao().deleteByProject(dbSession, project); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java new file mode 100644 index 000000000..67466481e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.AlmTypeMapper; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +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 org.sonarqube.ws.AlmSettings.GetBindingWsResponse; + +import static java.lang.String.format; +import static java.util.Optional.ofNullable; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class GetBindingAction extends AlmSettingsWsAction { + + private static final String PARAM_PROJECT = "project"; + + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + public GetBindingAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("get_binding").setHandler(this); + + action.createParam(PARAM_PROJECT).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + + String projectKey = request.mandatoryParam(PARAM_PROJECT); + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = componentFinder.getByKey(dbSession, projectKey); + userSession.checkComponentPermission(ADMIN, project); + ProjectAlmSettingDto projectAlmSetting = dbClient.projectAlmSettingDao().selectByProject(dbSession, project) + .orElseThrow(() -> new NotFoundException( + format("Project '%s' is not bound to any ALM", project.getKey()))); + AlmSettingDto almSetting = + dbClient.almSettingDao().selectByUuid(dbSession, projectAlmSetting.getAlmSettingUuid()).orElseThrow( + () -> new IllegalStateException( + format("ALM setting '%s' cannot be found", projectAlmSetting.getAlmSettingUuid()))); + + GetBindingWsResponse.Builder builder = + GetBindingWsResponse.newBuilder().setAlm(AlmTypeMapper.toAlmWs(almSetting.getAlm())) + .setKey(almSetting.getKey()); + ofNullable(projectAlmSetting.getAlmRepo()).ifPresent(builder::setRepository); + ofNullable(almSetting.getUrl()).ifPresent(builder::setUrl); + ofNullable(projectAlmSetting.getAlmSlug()).ifPresent(builder::setSlug); + writeProtobuf(builder.build(), request, response); + } + + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListAction.java new file mode 100644 index 000000000..063b5352d --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListAction.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.AlmTypeMapper; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.AlmSettings.AlmSetting; +import org.sonarqube.ws.AlmSettings.ListWsResponse; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Optional.ofNullable; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class ListAction extends AlmSettingsWsAction { + + private static final String PARAM_PROJECT = "project"; + + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + public ListAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("list").setHandler(this); + + action.createParam(PARAM_PROJECT).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = componentFinder.getByKey(dbSession, projectKey); + userSession.checkComponentPermission(ADMIN, project); + List settings = dbClient.almSettingDao().selectAll(dbSession); + List wsAlmSettings = settings.stream().map(almSetting -> { + AlmSetting.Builder almSettingBuilder = AlmSetting.newBuilder().setKey(almSetting.getKey()) + .setAlm(AlmTypeMapper.toAlmWs(almSetting.getAlm())); + ofNullable(almSetting.getUrl()).ifPresent(almSettingBuilder::setUrl); + return almSettingBuilder.build(); + }).collect(Collectors.toList()); + ListWsResponse.Builder builder = ListWsResponse.newBuilder().addAllAlmSettings(wsAlmSettings); + + writeProtobuf(builder.build(), request, response); + } + } + + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListDefinitionsAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListDefinitionsAction.java new file mode 100644 index 000000000..3ba66707c --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/ListDefinitionsAction.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.AlmSettings; +import org.sonarqube.ws.AlmSettings.AlmSettingGithub; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.AlmSettings.AlmSettingAzure; +import static org.sonarqube.ws.AlmSettings.AlmSettingBitbucket; +import static org.sonarqube.ws.AlmSettings.ListDefinitionsWsResponse; + +public class ListDefinitionsAction extends AlmSettingsWsAction { + + private final DbClient dbClient; + private final UserSession userSession; + + public ListDefinitionsAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + context.createAction("list_definitions").setHandler(this); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + try (DbSession dbSession = dbClient.openSession(false)) { + List settings = dbClient.almSettingDao().selectAll(dbSession); + Map> settingsByAlm = + settings.stream().collect(Collectors.groupingBy(AlmSettingDto::getAlm)); + List githubSettings = + settingsByAlm.getOrDefault(ALM.GITHUB, emptyList()).stream().map(ListDefinitionsAction::toGitHub) + .collect(Collectors.toList()); + List azureSettings = settingsByAlm.getOrDefault(ALM.AZURE_DEVOPS, emptyList()).stream() + .map(ListDefinitionsAction::toAzure).collect(Collectors.toList()); + List bitbucketSettings = + settingsByAlm.getOrDefault(ALM.BITBUCKET, emptyList()).stream() + .map(ListDefinitionsAction::toBitbucket).collect(Collectors.toList()); + List gitlabSettings = + settingsByAlm.getOrDefault(ALM.GITLAB, emptyList()).stream().map(ListDefinitionsAction::toGitlab) + .collect(Collectors.toList()); + ListDefinitionsWsResponse.Builder builder = + ListDefinitionsWsResponse.newBuilder().addAllGithub(githubSettings).addAllAzure(azureSettings) + .addAllBitbucket(bitbucketSettings).addAllGitlab(gitlabSettings); + + writeProtobuf(builder.build(), request, response); + } + + } + + private static AlmSettingGithub toGitHub(AlmSettingDto settingDto) { + return AlmSettingGithub.newBuilder().setKey(settingDto.getKey()) + .setUrl(requireNonNull(settingDto.getUrl(), "URL cannot be null for GitHub ALM setting")) + .setAppId(requireNonNull(settingDto.getAppId(), "App ID cannot be null for GitHub ALM setting")) + .setPrivateKey( + requireNonNull(settingDto.getPrivateKey(), "Private Key cannot be null for GitHub ALM setting")) + .build(); + } + + private static AlmSettingAzure toAzure(AlmSettingDto settingDto) { + return AlmSettingAzure.newBuilder().setKey(settingDto.getKey()).setPersonalAccessToken( + requireNonNull(settingDto.getPersonalAccessToken(), + "Personal Access Token cannot be null for Azure ALM setting")).build(); + } + + private static AlmSettingBitbucket toBitbucket(AlmSettingDto settingDto) { + return AlmSettingBitbucket.newBuilder().setKey(settingDto.getKey()) + .setUrl(requireNonNull(settingDto.getUrl(), "URL cannot be null for Bitbucket ALM setting")) + .setPersonalAccessToken(requireNonNull(settingDto.getPersonalAccessToken(), + "Personal Access Token cannot be null for Bitbucket ALM setting")) + .build(); + } + + private static AlmSettings.AlmSettingGitlab toGitlab(AlmSettingDto settingDto) { + return AlmSettings.AlmSettingGitlab.newBuilder().setKey(settingDto.getKey()).setPersonalAccessToken( + requireNonNull(settingDto.getPersonalAccessToken(), + "Personal Access Token cannot be null for Gitlab ALM setting")).build(); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/CreateAzureAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/CreateAzureAction.java new file mode 100644 index 000000000..c87bdd205 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/CreateAzureAction.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.sonar.db.alm.setting.ALM.AZURE_DEVOPS; + +public class CreateAzureAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private UserSession userSession; + + public CreateAzureAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("create_azure").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String token = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, key); + dbClient.almSettingDao().insert(dbSession, new AlmSettingDto().setAlm(AZURE_DEVOPS).setKey(key) + .setPersonalAccessToken(token)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/SetAzureBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/SetAzureBindingAction.java new file mode 100644 index 000000000..5d33e5db1 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/SetAzureBindingAction.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.SetBindingAlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +public class SetAzureBindingAction extends SetBindingAlmSettingsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PROJECT = "project"; + + private final DbClient dbClient; + + public SetAzureBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super(dbClient, componentFinder, userSession); + this.dbClient = dbClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_azure_binding").setPost(true).setHandler(this); + + action.createParam(PARAM_ALM_SETTING).setRequired(true); + action.createParam(PARAM_PROJECT).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + String almSetting = request.mandatoryParam(PARAM_ALM_SETTING); + String projectKey = request.mandatoryParam(PARAM_PROJECT); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = getProject(dbSession, projectKey); + AlmSettingDto almSettingDto = getAlmSetting(dbSession, almSetting); + dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, + new ProjectAlmSettingDto().setProjectUuid(project.uuid()) + .setAlmSettingUuid(almSettingDto.getUuid()) + .setAlmRepo(null).setAlmSlug(null)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/UpdateAzureAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/UpdateAzureAction.java new file mode 100644 index 000000000..cee303294 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/azure/UpdateAzureAction.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.azure; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +public class UpdateAzureAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_NEW_KEY = "newKey"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private final UserSession userSession; + + public UpdateAzureAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("update_azure").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_NEW_KEY).setMaximumLength(200); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String newKey = request.param(PARAM_NEW_KEY); + String token = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = getAlmSetting(dbSession, key); + if (isNotBlank(newKey) && !newKey.equals(key)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, newKey); + } + dbClient.almSettingDao().update(dbSession, almSettingDto.setKey(isNotBlank(newKey) ? newKey : key) + .setPersonalAccessToken(token)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/CreateBitBucketAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/CreateBitBucketAction.java new file mode 100644 index 000000000..9d95a96fb --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/CreateBitBucketAction.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.sonar.db.alm.setting.ALM.BITBUCKET; + +public class CreateBitBucketAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_URL = "url"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private UserSession userSession; + + public CreateBitBucketAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("create_bitbucket").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_URL).setRequired(true).setMaximumLength(2000); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String url = request.mandatoryParam(PARAM_URL); + String token = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, key); + dbClient.almSettingDao().insert(dbSession, new AlmSettingDto().setAlm(BITBUCKET).setKey(key).setUrl(url) + .setPersonalAccessToken(token)); + dbSession.commit(); + } + + response.noContent(); + } + + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/SetBitbucketBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/SetBitbucketBindingAction.java new file mode 100644 index 000000000..7b02f46c2 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/SetBitbucketBindingAction.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.SetBindingAlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +public class SetBitbucketBindingAction extends SetBindingAlmSettingsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PROJECT = "project"; + private static final String PARAM_REPOSITORY = "repository"; + private static final String PARAM_SLUG = "slug"; + + private final DbClient dbClient; + + public SetBitbucketBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super(dbClient, componentFinder, userSession); + this.dbClient = dbClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_bitbucket_binding").setPost(true).setHandler(this); + + action.createParam(PARAM_ALM_SETTING).setRequired(true); + action.createParam(PARAM_PROJECT).setRequired(true); + action.createParam(PARAM_REPOSITORY).setRequired(true); + action.createParam(PARAM_SLUG).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + String almSetting = request.mandatoryParam(PARAM_ALM_SETTING); + String projectKey = request.mandatoryParam(PARAM_PROJECT); + String repository = request.mandatoryParam(PARAM_REPOSITORY); + String slug = request.mandatoryParam(PARAM_SLUG); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = getProject(dbSession, projectKey); + AlmSettingDto almSettingDto = getAlmSetting(dbSession, almSetting); + dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, + new ProjectAlmSettingDto().setProjectUuid(project.uuid()) + .setAlmSettingUuid(almSettingDto.getUuid()) + .setAlmRepo(repository).setAlmSlug(slug)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/UpdateBitbucketAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/UpdateBitbucketAction.java new file mode 100644 index 000000000..ec19d1c10 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/bitbucket/UpdateBitbucketAction.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.bitbucket; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +public class UpdateBitbucketAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_NEW_KEY = "newKey"; + private static final String PARAM_URL = "url"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private UserSession userSession; + + public UpdateBitbucketAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("update_bitbucket").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_NEW_KEY).setMaximumLength(200); + action.createParam(PARAM_URL).setRequired(true).setMaximumLength(2000); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String newKey = request.param(PARAM_NEW_KEY); + String url = request.mandatoryParam(PARAM_URL); + String token = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = getAlmSetting(dbSession, key); + if (isNotBlank(newKey) && !newKey.equals(key)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, newKey); + } + dbClient.almSettingDao().update(dbSession, + almSettingDto.setKey(isNotBlank(newKey) ? newKey : key).setUrl(url) + .setPersonalAccessToken(token)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/CreateGithubAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/CreateGithubAction.java new file mode 100644 index 000000000..511cc6593 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/CreateGithubAction.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.sonar.db.alm.setting.ALM.GITHUB; + +public class CreateGithubAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_URL = "url"; + private static final String PARAM_APP_ID = "appId"; + private static final String PARAM_PRIVATE_KEY = "privateKey"; + + private final DbClient dbClient; + private final UserSession userSession; + + public CreateGithubAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("create_github").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_URL).setRequired(true).setMaximumLength(2000); + action.createParam(PARAM_APP_ID).setRequired(true).setMaximumLength(80); + action.createParam(PARAM_PRIVATE_KEY).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String url = request.mandatoryParam(PARAM_URL); + String appId = request.mandatoryParam(PARAM_APP_ID); + String privateKey = request.mandatoryParam(PARAM_PRIVATE_KEY); + + try (DbSession dbSession = dbClient.openSession(false)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, key); + dbClient.almSettingDao().insert(dbSession, + new AlmSettingDto().setAlm(GITHUB).setKey(key).setUrl(url).setAppId(appId) + .setPrivateKey(privateKey)); + dbSession.commit(); + } + + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/SetGithubBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/SetGithubBindingAction.java new file mode 100644 index 000000000..5809bc5d7 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/SetGithubBindingAction.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.SetBindingAlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +public class SetGithubBindingAction extends SetBindingAlmSettingsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PROJECT = "project"; + private static final String PARAM_REPOSITORY = "repository"; + + private final DbClient dbClient; + + public SetGithubBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super(dbClient, componentFinder, userSession); + this.dbClient = dbClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_github_binding").setPost(true).setHandler(this); + + action.createParam(PARAM_ALM_SETTING).setRequired(true); + action.createParam(PARAM_PROJECT).setRequired(true); + action.createParam(PARAM_REPOSITORY).setRequired(true).setMaximumLength(256); + } + + @Override + public void handle(Request request, Response response) { + String almSetting = request.mandatoryParam(PARAM_ALM_SETTING); + String projectKey = request.mandatoryParam(PARAM_PROJECT); + String repository = request.mandatoryParam(PARAM_REPOSITORY); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = getProject(dbSession, projectKey); + AlmSettingDto almSettingDto = getAlmSetting(dbSession, almSetting); + dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, + new ProjectAlmSettingDto().setProjectUuid(project.uuid()) + .setAlmSettingUuid(almSettingDto.getUuid()) + .setAlmRepo(repository).setAlmSlug(null)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/UpdateGitHubAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/UpdateGitHubAction.java new file mode 100644 index 000000000..f5f627ef4 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/github/UpdateGitHubAction.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.github; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +public class UpdateGitHubAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_NEW_KEY = "newKey"; + private static final String PARAM_URL = "url"; + private static final String PARAM_APP_ID = "appId"; + private static final String PARAM_PRIVATE_KEY = "privateKey"; + + private final DbClient dbClient; + private final UserSession userSession; + + public UpdateGitHubAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("update_github").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_NEW_KEY).setMaximumLength(200); + action.createParam(PARAM_URL).setRequired(true).setMaximumLength(2000); + action.createParam(PARAM_APP_ID).setRequired(true).setMaximumLength(80); + action.createParam(PARAM_PRIVATE_KEY).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String newKey = request.param(PARAM_NEW_KEY); + String url = request.mandatoryParam(PARAM_URL); + String appId = request.mandatoryParam(PARAM_APP_ID); + String privateKey = request.mandatoryParam(PARAM_PRIVATE_KEY); + + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = getAlmSetting(dbSession, key); + if (isNotBlank(newKey) && !newKey.equals(key)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, newKey); + } + dbClient.almSettingDao().update(dbSession, + almSettingDto.setKey(isNotBlank(newKey) ? newKey : key).setUrl(url) + .setAppId(appId).setPrivateKey(privateKey)); + dbSession.commit(); + } + + response.noContent(); + } + + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/CreateGitlabAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/CreateGitlabAction.java new file mode 100644 index 000000000..685d2233a --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/CreateGitlabAction.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.sonar.db.alm.setting.ALM.GITLAB; + +public class CreateGitlabAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private final UserSession userSession; + + public CreateGitlabAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("create_gitlab").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String accessToken = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, key); + dbClient.almSettingDao().insert(dbSession, new AlmSettingDto().setAlm(GITLAB).setKey(key) + .setPersonalAccessToken(accessToken)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/SetGitlabBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/SetGitlabBindingAction.java new file mode 100644 index 000000000..79851f28b --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/SetGitlabBindingAction.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.SetBindingAlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +public class SetGitlabBindingAction extends SetBindingAlmSettingsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PROJECT = "project"; + + private final DbClient dbClient; + + public SetGitlabBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + super(dbClient, componentFinder, userSession); + this.dbClient = dbClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set_gitlab_binding").setPost(true).setHandler(this); + + action.createParam(PARAM_ALM_SETTING).setRequired(true); + action.createParam(PARAM_PROJECT).setRequired(true); + } + + @Override + public void handle(Request request, Response response) { + String almSetting = request.mandatoryParam(PARAM_ALM_SETTING); + String projectKey = request.mandatoryParam(PARAM_PROJECT); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = getProject(dbSession, projectKey); + AlmSettingDto almSettingDto = getAlmSetting(dbSession, almSetting); + dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, + new ProjectAlmSettingDto().setProjectUuid(project.uuid()) + .setAlmSettingUuid(almSettingDto.getUuid()) + .setAlmRepo(null).setAlmSlug(null)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/UpdateGitlabAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/UpdateGitlabAction.java new file mode 100644 index 000000000..17828997e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/gitlab/UpdateGitlabAction.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Michael Clarke + * + * 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 com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.gitlab; + +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.AlmSettingsWsAction; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.user.UserSession; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +public class UpdateGitlabAction extends AlmSettingsWsAction { + + private static final String PARAM_KEY = "key"; + private static final String PARAM_NEW_KEY = "newKey"; + private static final String PARAM_PERSONAL_ACCESS_TOKEN = "personalAccessToken"; + + private final DbClient dbClient; + private final UserSession userSession; + + public UpdateGitlabAction(DbClient dbClient, UserSession userSession) { + super(dbClient); + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("update_gitlab").setPost(true).setHandler(this); + + action.createParam(PARAM_KEY).setRequired(true).setMaximumLength(200); + action.createParam(PARAM_NEW_KEY).setMaximumLength(200); + action.createParam(PARAM_PERSONAL_ACCESS_TOKEN).setRequired(true).setMaximumLength(2000); + } + + @Override + public void handle(Request request, Response response) { + userSession.checkIsSystemAdministrator(); + + String key = request.mandatoryParam(PARAM_KEY); + String newKey = request.param(PARAM_NEW_KEY); + String token = request.mandatoryParam(PARAM_PERSONAL_ACCESS_TOKEN); + + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = getAlmSetting(dbSession, key); + if (isNotBlank(newKey) && !newKey.equals(key)) { + checkAlmSettingDoesNotAlreadyExist(dbSession, newKey); + } + dbClient.almSettingDao().update(dbSession, almSettingDto.setKey(isNotBlank(newKey) ? newKey : key) + .setPersonalAccessToken(token)); + dbSession.commit(); + } + + response.noContent(); + } + +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java index b77039317..90d6e9b78 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -99,7 +99,7 @@ public void testComputeEngineSideLoad() { testCase.load(context); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Class.class); - verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); + verify(context).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); assertEquals(Arrays.asList(CommunityReportAnalysisComponentProvider.class, CommunityBranchEditionProvider.class), @@ -117,7 +117,7 @@ public void testServerSideLoad() { testCase.load(context); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); + verify(context).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); assertEquals(22, argumentCaptor.getAllValues().size()); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java index 6b34d31cf..72772a3cf 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -77,7 +77,7 @@ public void testNoBranchDetailsNoExistingBranchThrowsException() { @Test public void testNoBranchDetailsExistingBranchMatch() { BranchDto branchDto = mock(BranchDto.class); - when(branchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH); when(branchDto.getKey()).thenReturn("branchKey"); when(branchDto.getMergeBranchUuid()).thenReturn("mergeBranchUuid"); when(branchDto.getProjectUuid()).thenReturn("projectUuid"); @@ -97,10 +97,9 @@ public void testNoBranchDetailsExistingBranchMatch() { ArgumentCaptor branchArgumentCaptor = ArgumentCaptor.forClass(CommunityBranch.class); verify(metadataHolder).setBranch(branchArgumentCaptor.capture()); - assertEquals(BranchType.SHORT, branchArgumentCaptor.getValue().getType()); - assertNull(branchArgumentCaptor.getValue().getMergeBranchUuid()); + assertEquals(BranchType.BRANCH, branchArgumentCaptor.getValue().getType()); + assertNull(branchArgumentCaptor.getValue().getTargetBranchName()); assertEquals("branchKey", branchArgumentCaptor.getValue().getName()); - assertFalse(branchArgumentCaptor.getValue().isLegacyFeature()); assertFalse(branchArgumentCaptor.getValue().isMain()); assertFalse(branchArgumentCaptor.getValue().supportsCrossProjectCpd()); assertNull(branchArgumentCaptor.getValue().getTargetBranchName()); @@ -128,9 +127,9 @@ public void testBranchNameNoMatchingBranch() { } @Test - public void testBranchNameMatchingShortBranch() { + public void testBranchNameMatchingBranch() { BranchDto branchDto = mock(BranchDto.class); - when(branchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH); when(branchDto.getKey()).thenReturn("branchKey"); when(branchDto.getMergeBranchUuid()).thenReturn("mergeBranchUuid"); when(branchDto.getProjectUuid()).thenReturn("projectUuid"); @@ -141,7 +140,7 @@ public void testBranchNameMatchingShortBranch() { ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("branch") - .setBranchType(ScannerReport.Metadata.BranchType.SHORT).build(); + .setBranchType(ScannerReport.Metadata.BranchType.BRANCH).build(); DbClient dbClient = mock(DbClient.class); when(dbClient.branchDao()).thenReturn(branchDao); @@ -154,52 +153,14 @@ public void testBranchNameMatchingShortBranch() { ArgumentCaptor branchArgumentCaptor = ArgumentCaptor.forClass(CommunityBranch.class); verify(metadataHolder).setBranch(branchArgumentCaptor.capture()); - assertEquals(BranchType.SHORT, branchArgumentCaptor.getValue().getType()); - assertEquals("projectUuid", branchArgumentCaptor.getValue().getMergeBranchUuid()); + assertEquals(BranchType.BRANCH, branchArgumentCaptor.getValue().getType()); + assertEquals("projectUuid", branchArgumentCaptor.getValue().getReferenceBranchUuid()); assertEquals("branch", branchArgumentCaptor.getValue().getName()); - assertFalse(branchArgumentCaptor.getValue().isLegacyFeature()); assertFalse(branchArgumentCaptor.getValue().isMain()); assertFalse(branchArgumentCaptor.getValue().supportsCrossProjectCpd()); assertNull(branchArgumentCaptor.getValue().getTargetBranchName()); } - @Test - public void testBranchNameMatchingLongBranch() { - BranchDto branchDto = mock(BranchDto.class); - when(branchDto.getBranchType()).thenReturn(BranchType.LONG); - when(branchDto.getKey()).thenReturn("branchKey"); - when(branchDto.getMergeBranchUuid()).thenReturn("mergeBranchUuid"); - when(branchDto.getProjectUuid()).thenReturn("projectUuid"); - when(branchDto.getUuid()).thenReturn("branchUuid"); - - BranchDao branchDao = mock(BranchDao.class); - when(branchDao.selectByBranchKey(any(), eq("projectUuid"), eq("branch"))).thenReturn(Optional.of(branchDto)); - - ScannerReport.Metadata metadata = - ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("branch") - .setTargetBranchName("targetBranchName") - .setBranchType(ScannerReport.Metadata.BranchType.LONG).build(); - - DbClient dbClient = mock(DbClient.class); - when(dbClient.branchDao()).thenReturn(branchDao); - - MutableAnalysisMetadataHolder metadataHolder = mock(MutableAnalysisMetadataHolder.class); - when(metadataHolder.getProject()).thenReturn(new Project("projectUuid", "key", "name", "description", new ArrayList<>())); - - new CommunityBranchLoaderDelegate(dbClient, metadataHolder).load(metadata); - - ArgumentCaptor branchArgumentCaptor = ArgumentCaptor.forClass(CommunityBranch.class); - - verify(metadataHolder).setBranch(branchArgumentCaptor.capture()); - assertEquals(BranchType.LONG, branchArgumentCaptor.getValue().getType()); - assertEquals("projectUuid", branchArgumentCaptor.getValue().getMergeBranchUuid()); - assertEquals("branch", branchArgumentCaptor.getValue().getName()); - assertFalse(branchArgumentCaptor.getValue().isLegacyFeature()); - assertFalse(branchArgumentCaptor.getValue().isMain()); - assertFalse(branchArgumentCaptor.getValue().supportsCrossProjectCpd()); - assertEquals("targetBranchName", branchArgumentCaptor.getValue().getTargetBranchName()); - } - @Test public void testBranchNamePullRequest() { BranchDto branchDto = mock(BranchDto.class); @@ -213,7 +174,7 @@ public void testBranchNamePullRequest() { ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("sourceBranch") - .setMergeBranchName("branch").setBranchType(ScannerReport.Metadata.BranchType.PULL_REQUEST) + .setReferenceBranchName("branch").setBranchType(ScannerReport.Metadata.BranchType.PULL_REQUEST) .setTargetBranchName("") .build(); @@ -229,9 +190,8 @@ public void testBranchNamePullRequest() { verify(metadataHolder).setBranch(branchArgumentCaptor.capture()); assertEquals(BranchType.PULL_REQUEST, branchArgumentCaptor.getValue().getType()); - assertEquals("mergeBranchUuid", branchArgumentCaptor.getValue().getMergeBranchUuid()); + assertEquals("mergeBranchUuid", branchArgumentCaptor.getValue().getReferenceBranchUuid()); assertEquals("sourceBranch", branchArgumentCaptor.getValue().getName()); - assertFalse(branchArgumentCaptor.getValue().isLegacyFeature()); assertFalse(branchArgumentCaptor.getValue().isMain()); assertFalse(branchArgumentCaptor.getValue().supportsCrossProjectCpd()); assertEquals("branch", branchArgumentCaptor.getValue().getTargetBranchName()); @@ -244,7 +204,7 @@ public void testBranchNamePullRequestNoSuchTarget() { ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("sourceBranch") - .setMergeBranchName("branch").setBranchType(ScannerReport.Metadata.BranchType.PULL_REQUEST) + .setReferenceBranchName("branch").setBranchType(ScannerReport.Metadata.BranchType.PULL_REQUEST) .build(); DbClient dbClient = mock(DbClient.class); @@ -262,16 +222,16 @@ public void testBranchNamePullRequestNoSuchTarget() { @Test - public void testBranchNameMatchingShortBranchWithTargetBranch() { + public void testBranchNameMatchingBranchWithTargetBranch() { BranchDto sourceBranchDto = mock(BranchDto.class); - when(sourceBranchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(sourceBranchDto.getBranchType()).thenReturn(BranchType.BRANCH); when(sourceBranchDto.getKey()).thenReturn("branchKey"); when(sourceBranchDto.getMergeBranchUuid()).thenReturn("mergeBranchUuid"); when(sourceBranchDto.getProjectUuid()).thenReturn("projectUuid"); when(sourceBranchDto.getUuid()).thenReturn("branchUuid"); BranchDto targetBranchDto = mock(BranchDto.class); - when(targetBranchDto.getBranchType()).thenReturn(BranchType.LONG); + when(targetBranchDto.getBranchType()).thenReturn(BranchType.PULL_REQUEST); when(targetBranchDto.getKey()).thenReturn("targetBranchKey"); when(targetBranchDto.getMergeBranchUuid()).thenReturn("targetMergeBranchUuid"); when(targetBranchDto.getProjectUuid()).thenReturn("projectUuid"); @@ -285,7 +245,8 @@ public void testBranchNameMatchingShortBranchWithTargetBranch() { ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("branch") - .setBranchType(ScannerReport.Metadata.BranchType.SHORT).setMergeBranchName("mergeBranchName") + .setBranchType(ScannerReport.Metadata.BranchType.BRANCH) + .setReferenceBranchName("mergeBranchName") .setTargetBranchName("targetBranchThatDoesNotMatchMergeBranch") .build(); @@ -300,8 +261,8 @@ public void testBranchNameMatchingShortBranchWithTargetBranch() { ArgumentCaptor branchArgumentCaptor = ArgumentCaptor.forClass(Branch.class); verify(metadataHolder).setBranch(branchArgumentCaptor.capture()); - assertEquals(BranchType.SHORT, branchArgumentCaptor.getValue().getType()); - assertEquals("targetBranchUuid", branchArgumentCaptor.getValue().getMergeBranchUuid()); + assertEquals(BranchType.BRANCH, branchArgumentCaptor.getValue().getType()); + assertEquals("targetBranchUuid", branchArgumentCaptor.getValue().getReferenceBranchUuid()); assertEquals("branch", branchArgumentCaptor.getValue().getName()); assertFalse(branchArgumentCaptor.getValue().isMain()); assertFalse(branchArgumentCaptor.getValue().supportsCrossProjectCpd()); @@ -309,9 +270,9 @@ public void testBranchNameMatchingShortBranchWithTargetBranch() { } @Test - public void testBranchNameMatchingShortBranchWithTargetBranchMissingTargetBranch() { + public void testBranchNameMatchingBranchWithTargetBranchMissingTargetBranch() { BranchDto sourceBranchDto = mock(BranchDto.class); - when(sourceBranchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(sourceBranchDto.getBranchType()).thenReturn(BranchType.BRANCH); when(sourceBranchDto.getKey()).thenReturn("branchKey"); when(sourceBranchDto.getMergeBranchUuid()).thenReturn("mergeBranchUuid"); when(sourceBranchDto.getProjectUuid()).thenReturn("projectUuid"); @@ -324,7 +285,8 @@ public void testBranchNameMatchingShortBranchWithTargetBranchMissingTargetBranch ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance().toBuilder().setBranchName("branch") - .setBranchType(ScannerReport.Metadata.BranchType.SHORT).setMergeBranchName("mergeBranchName") + .setBranchType(ScannerReport.Metadata.BranchType.BRANCH) + .setReferenceBranchName("mergeBranchName") .build(); DbClient dbClient = mock(DbClient.class); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchTest.java index cacd64d92..e3a9b72c8 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -68,8 +68,8 @@ public void testGenerateKeyNonMainBranchNonNullFileOfPathContentPullRequest() { } @Test - public void testGenerateKeyNonMainBranchNonNullFileOfPathContentShortBranch() { - CommunityBranch testCase = new CommunityBranch("name", BranchType.SHORT, false, null, null, null); + public void testGenerateKeyNonMainBranchNonNullFileOfPathContentBranch() { + CommunityBranch testCase = new CommunityBranch("name", BranchType.BRANCH, false, null, null, null); assertEquals("projectKey:path:BRANCH:name", testCase.generateKey("projectKey", "path")); } @@ -87,7 +87,7 @@ public void testGetPulRequestKeyNonPullRequest() { .expectMessage(IsEqual.equalTo("Only a branch of type PULL_REQUEST can have a pull request ID")); expectedException.expect(IllegalStateException.class); - new CommunityBranch("name", BranchType.SHORT, false, null, "prKey", null).getPullRequestKey(); + new CommunityBranch("name", BranchType.BRANCH, false, null, "prKey", null).getPullRequestKey(); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java index 923f13fdf..208cfa44e 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,13 +28,12 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.ListItem; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.ce.posttask.Analysis; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.measures.CoreMetrics; @@ -47,7 +46,6 @@ import org.sonar.ce.task.projectanalysis.metric.MetricRepository; import org.sonar.core.issue.DefaultIssue; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -57,7 +55,6 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.any; @@ -80,10 +77,11 @@ public void testGetBranchName() { Analysis analysis = mock(Analysis.class); Project project = mock(Project.class); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals("branchName", testCase.getBranchName()); } @@ -99,10 +97,11 @@ public void testGetCommitSha() { Analysis analysis = mock(Analysis.class); Project project = mock(Project.class); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals("commitId", testCase.getCommitSha()); } @@ -117,10 +116,11 @@ public void testGetQualityGateStatus() { Analysis analysis = mock(Analysis.class); Project project = mock(Project.class); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals(QualityGate.Status.ERROR, testCase.getQualityGateStatus()); } @@ -135,10 +135,11 @@ public void testGetAnalysisDate() { doReturn(new Date()).when(analysis).getDate(); Project project = mock(Project.class); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals(analysis.getDate(), testCase.getAnalysisDate()); } @@ -153,10 +154,11 @@ public void testGetAnalysisId() { doReturn("Analysis ID").when(analysis).getAnalysisUuid(); Project project = mock(Project.class); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals("Analysis ID", testCase.getAnalysisId()); } @@ -171,10 +173,11 @@ public void testGetAnalysisProjectKey() { Project project = mock(Project.class); doReturn("Project Key").when(project).getKey(); Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null); + project, configuration, null, scannerContext); assertEquals("Project Key", testCase.getAnalysisProjectKey()); } @@ -290,9 +293,11 @@ public void testCreateAnalysisSummary() { Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); + AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000"); + project, configuration, "http://localhost:9000", scannerContext); Formatter formatter = mock(Formatter.class); doReturn("formatted content").when(formatter).format(any(), any()); @@ -399,9 +404,11 @@ public void testCreateAnalysisSummary2() { Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); + AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000"); + project, configuration, "http://localhost:9000", scannerContext); Formatter formatter = mock(Formatter.class); doReturn("formatted content").when(formatter).format(any(), any()); @@ -504,9 +511,11 @@ public void testCreateAnalysisSummary3() { doReturn(Optional.of("http://host.name/path")).when(configuration) .get(eq(AnalysisDetails.IMAGE_URL_BASE)); + ScannerContext scannerContext = mock(ScannerContext.class); + AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000"); + project, configuration, "http://localhost:9000", scannerContext); Formatter formatter = mock(Formatter.class); doReturn("formatted content").when(formatter).format(any(), any()); @@ -601,9 +610,11 @@ public void testCreateAnalysisSummary4() { Configuration configuration = mock(Configuration.class); + ScannerContext scannerContext = mock(ScannerContext.class); + AnalysisDetails testCase = new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000"); + project, configuration, "http://localhost:9000", scannerContext); Formatter formatter = mock(Formatter.class); doReturn("formatted content").when(formatter).format(any(), any()); @@ -697,7 +708,8 @@ public void testCorrectPostAnalysisIssueVisitorReturned() { AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), postAnalysisIssueVisitor, mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), mock(Configuration.class), null); + mock(Analysis.class), mock(Project.class), mock(Configuration.class), null, + mock(ScannerContext.class)); assertSame(postAnalysisIssueVisitor, analysisDetails.getPostAnalysisIssueVisitor()); } @@ -708,26 +720,4 @@ public void testCorrectBranchDetailsReturned() { assertEquals("commitId", branchDetails.getCommitId()); } - @Test - public void testReflectiveOperationPropagated() { - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - MeasureRepository measureRepository = mock(MeasureRepository.class); - doReturn(Optional.of(Measure.newMeasureBuilder().create(2))).when(measureRepository) - .getRawMeasure(any(), any()); - doReturn(mock(TreeRootHolder.class)).when(measuresHolder).getTreeRootHolder(); - doReturn(mock(MetricRepository.class)).when(measuresHolder).getMetricRepository(); - doReturn(measureRepository).when(measuresHolder).getMeasureRepository(); - - QualityGate qualityGate = mock(QualityGate.class); - doReturn(new ArrayList<>()).when(qualityGate).getConditions(); - - AnalysisDetails testCase = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - qualityGate, measuresHolder, mock(Analysis.class), mock(Project.class), - mock(Configuration.class), null); - assertThatThrownBy(() -> testCase.createAnalysisSummary(mock(FormatterFactory.class))) - .hasMessage("Could not invoke getDoubleValue").isExactlyInstanceOf(IllegalStateException.class) - .hasCauseExactlyInstanceOf(InvocationTargetException.class); - } - } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index 2ccaa38f1..9c69a530a 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,12 +25,20 @@ import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.config.Configuration; import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; import org.sonar.ce.task.projectanalysis.metric.MetricRepository; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDao; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDao; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.util.ArrayList; import java.util.Date; @@ -39,11 +47,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class PullRequestPostAnalysisTaskTest { @@ -51,9 +61,12 @@ public class PullRequestPostAnalysisTaskTest { public void testFinishedNonPullRequest() { PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); Branch branch = mock(Branch.class); - doReturn(Branch.Type.LONG).when(branch).getType(); + doReturn(Branch.Type.BRANCH).when(branch).getType(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -62,11 +75,13 @@ public void testFinishedNonPullRequest() { MeasureRepository measureRepository = mock(MeasureRepository.class); TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); + DbClient dbClient = mock(DbClient.class); + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(branch).getType(); verify(branch, never()).getName(); @@ -80,6 +95,9 @@ public void testFinishedNoBranchName() { doReturn(Optional.empty()).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -88,11 +106,13 @@ public void testFinishedNoBranchName() { MeasureRepository measureRepository = mock(MeasureRepository.class); TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); + DbClient dbClient = mock(DbClient.class); + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(branch).getName(); verify(configurationRepository, never()).getConfiguration(); @@ -106,6 +126,13 @@ public void testFinishedNoProviderSet() { doReturn(Optional.of("branchName")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + Project project = mock(Project.class); + doReturn("projectUuid").when(project).getUuid(); + doReturn(project).when(projectAnalysis).getProject(); + + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -115,17 +142,37 @@ public void testFinishedNoProviderSet() { TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); Configuration configuration = mock(Configuration.class); - doReturn(Optional.empty()).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); + DbClient dbClient = mock(DbClient.class); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(configurationRepository).getConfiguration(); - verify(configuration).get("sonar.pullrequest.provider"); verify(projectAnalysis, never()).getAnalysis(); } @@ -137,6 +184,13 @@ public void testFinishedNoProviderMatchingName() { doReturn(Optional.of("branchName")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + Project project = mock(Project.class); + doReturn("projectUuid").when(project).getUuid(); + doReturn(project).when(projectAnalysis).getProject(); + + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -151,22 +205,43 @@ public void testFinishedNoProviderMatchingName() { PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-2").when(decorator2).name(); + doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("missing-provider")).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); + DbClient dbClient = mock(DbClient.class); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.AZURE_DEVOPS); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(configurationRepository).getConfiguration(); - verify(configuration).get("sonar.pullrequest.provider"); - verify(decorator1).name(); - verify(decorator2).name(); + verify(decorator1).alm(); + verify(decorator2).alm(); verify(projectAnalysis, never()).getAnalysis(); } @@ -178,6 +253,13 @@ public void testFinishedNoAnalysis() { doReturn(Optional.of("pull-request")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + Project project = mock(Project.class); + doReturn("projectUuid").when(project).getUuid(); + doReturn(project).when(projectAnalysis).getProject(); + + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -194,21 +276,42 @@ public void testFinishedNoAnalysis() { PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-2").when(decorator2).name(); + doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("decorator-name-2")).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); + DbClient dbClient = mock(DbClient.class); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(configurationRepository).getConfiguration(); verify(projectAnalysis).getAnalysis(); - verify(decorator2, never()).decorateQualityGateStatus(any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); } @@ -220,6 +323,13 @@ public void testFinishedAnalysisWithNoRevision() { doReturn(Optional.of("pull-request")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + Project project = mock(Project.class); + doReturn("projectUuid").when(project).getUuid(); + doReturn(project).when(projectAnalysis).getProject(); + + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -238,22 +348,44 @@ public void testFinishedAnalysisWithNoRevision() { PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-2").when(decorator2).name(); + doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("decorator-name-2")).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); + DbClient dbClient = mock(DbClient.class); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(configurationRepository).getConfiguration(); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis, never()).getQualityGate(); - verify(decorator2, never()).decorateQualityGateStatus(any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); } @Test @@ -264,6 +396,13 @@ public void testFinishedAnalysisWithNoQualityGate() { doReturn(Optional.of("pull-request")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + Project project = mock(Project.class); + doReturn("projectUuid").when(project).getUuid(); + doReturn(project).when(projectAnalysis).getProject(); + + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Server server = mock(Server.class); ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); List pullRequestBuildStatusDecorators = new ArrayList<>(); @@ -276,28 +415,48 @@ public void testFinishedAnalysisWithNoQualityGate() { doReturn(Optional.of("revision")).when(analysis).getRevision(); doReturn(Optional.of(analysis)).when(projectAnalysis).getAnalysis(); + DbClient dbClient = mock(DbClient.class); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-1").when(decorator1).name(); pullRequestBuildStatusDecorators.add(decorator1); PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-2").when(decorator2).name(); + doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("decorator-name-2")).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); verify(configurationRepository).getConfiguration(); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis).getQualityGate(); - verify(decorator2, never()).decorateQualityGateStatus(any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); } @Test @@ -308,7 +467,11 @@ public void testFinishedAnalysisDecorationRequest() { doReturn(Optional.of("pull-request")).when(branch).getName(); doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); + PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + doReturn(projectAnalysis).when(context).getProjectAnalysis(); + Project project = mock(Project.class); + doReturn("uuid").when(project).getUuid(); doReturn(project).when(projectAnalysis).getProject(); Server server = mock(Server.class); @@ -329,35 +492,64 @@ public void testFinishedAnalysisDecorationRequest() { PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-1").when(decorator1).name(); + doReturn(ALM.BITBUCKET).when(decorator1).alm(); pullRequestBuildStatusDecorators.add(decorator1); PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-2").when(decorator2).name(); + doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("decorator-name-2")).when(configuration).get(eq("sonar.pullrequest.provider")); doReturn(configuration).when(configurationRepository).getConfiguration(); + DbClient dbClient = mock(DbClient.class); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + PullRequestPostAnalysisTask testCase = new PullRequestPostAnalysisTask(server, configurationRepository, pullRequestBuildStatusDecorators, postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder); - testCase.finished(projectAnalysis); + treeRootHolder, dbClient); + testCase.finished(context); ArgumentCaptor analysisDetailsArgumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); + ArgumentCaptor almSettingDtoArgumentCaptor = ArgumentCaptor.forClass(AlmSettingDto.class); + ArgumentCaptor projectAlmSettingDtoArgumentCaptor = + ArgumentCaptor.forClass(ProjectAlmSettingDto.class); verify(configurationRepository).getConfiguration(); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis).getQualityGate(); - verify(decorator2).decorateQualityGateStatus(analysisDetailsArgumentCaptor.capture()); + verify(decorator2).decorateQualityGateStatus(analysisDetailsArgumentCaptor.capture(), + almSettingDtoArgumentCaptor.capture(), + projectAlmSettingDtoArgumentCaptor.capture()); + assertThat(almSettingDtoArgumentCaptor.getValue()).isSameAs(almSettingDto); + assertThat(projectAlmSettingDtoArgumentCaptor.getValue()).isSameAs(projectAlmSettingDto); AnalysisDetails analysisDetails = new AnalysisDetails(new AnalysisDetails.BranchDetails("pull-request", "revision"), postAnalysisIssueVisitor, qualityGate, new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder), analysis, project, - configuration, null); + configuration, null, scannerContext); assertThat(analysisDetailsArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(analysisDetails); } @@ -365,7 +557,8 @@ public void testFinishedAnalysisDecorationRequest() { public void testCorrectDescriptionReturnedForTask() { assertThat(new PullRequestPostAnalysisTask(mock(Server.class), mock(ConfigurationRepository.class), new ArrayList<>(), mock(PostAnalysisIssueVisitor.class), mock(MetricRepository.class), - mock(MeasureRepository.class), mock(TreeRootHolder.class)) + mock(MeasureRepository.class), mock(TreeRootHolder.class), + mock(DbClient.class)) .getDescription()).isEqualTo("Pull Request Decoration"); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java index ebeca67ab..a388dcb7e 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java @@ -1,8 +1,25 @@ +/* + * Copyright (C) 2019 Oliver Jedinger + * + * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.server; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.SummaryComment; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity.ActivityPage; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.DiffPage; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.apache.commons.io.FileUtils; @@ -17,12 +34,16 @@ import java.util.HashMap; import java.util.Map; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; public class BitbucketServerPullRequestDecoratorTest { @@ -37,154 +58,37 @@ public class BitbucketServerPullRequestDecoratorTest { /** * configure these settings if you want to trigger your server instead of the test * APITOKEN: use a real api token - * ACTIVITYURL: use for your bitbucket url (http://localhost:7990/rest/api/1.0/users/repo.owner/repos/testrepo/pull-requests/1/activities) */ private static final String APITOKEN = "APITOKEN"; - private static final String ACTIVITYURL = "http://localhost:8089/activities"; - private static final String DIFFURL = "http://localhost:8089/diff"; private static final String COMMENTURL = "http://localhost:8089/comments"; @Before public void setUp() { - bitbucketServerPullRequestDecorator = new BitbucketServerPullRequestDecorator(null); + bitbucketServerPullRequestDecorator = new BitbucketServerPullRequestDecorator(); headers = new HashMap<>(); headers.put("Authorization", String.format("Bearer %s", APITOKEN)); headers.put("Accept", "application/json"); } - @Test - public void getPageActivityClass() throws Exception { - stubFor( - get(urlEqualTo("/activities")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("") - ) - ); - assertThat(bitbucketServerPullRequestDecorator.getPage(ACTIVITYURL, headers, ActivityPage.class), nullValue()); - - stubFor( - get(urlEqualTo("/activities")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/activity.json"))) - ) - ); - ActivityPage activityPage = bitbucketServerPullRequestDecorator.getPage(ACTIVITYURL, headers, ActivityPage.class); - assertThat(activityPage, notNullValue()); - assertThat(activityPage.getSize(), is(3)); - } - - @Test(expected = IllegalStateException.class) - public void getPageActivityClassError() { - - stubFor( - get(urlEqualTo("/activities")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(400) - .withHeader("Content-Type", "application/json") - .withBody("{}") - ) - ); - bitbucketServerPullRequestDecorator.getPage(ACTIVITYURL, headers, ActivityPage.class); - } - @Test public void getPageDiffClass() throws Exception { - stubFor( - get(urlEqualTo("/diff")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/diff.json"))) - ) - ); + stubFor(get(urlEqualTo("/diff")).withHeader("Accept", equalTo("application/json")).willReturn( + aResponse().withStatus(200).withHeader("Content-Type", "application/json") + .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/diff.json"))))); DiffPage page = bitbucketServerPullRequestDecorator.getPage(DIFFURL, headers, DiffPage.class); assertThat(page, notNullValue()); assertThat(page.getDiffs().size(), is(1)); } - @Test - public void getCommentsToDelete() throws Exception { - ActivityPage activityPage = new ObjectMapper().readValue(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/delete/activityPageCase1.json")), ActivityPage.class); - assertThat(bitbucketServerPullRequestDecorator.getCommentsToDelete("susi.sonar", activityPage).size() , is(0)); - - activityPage = new ObjectMapper().readValue(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/delete/activityPageCase2.json")), ActivityPage.class); - assertThat(bitbucketServerPullRequestDecorator.getCommentsToDelete("susi.sonar", activityPage).size() , is(0)); - - activityPage = new ObjectMapper().readValue(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/delete/activityPageCase3.json")), ActivityPage.class); - assertThat(bitbucketServerPullRequestDecorator.getCommentsToDelete("susi.sonar", activityPage).size() , is(0)); - - activityPage = new ObjectMapper().readValue(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/delete/activityPageCase4.json")), ActivityPage.class); - assertThat(bitbucketServerPullRequestDecorator.getCommentsToDelete("susi.sonar", activityPage).size() , is(1)); - } - - @Test - public void deleteComments() throws Exception { - assertThat(bitbucketServerPullRequestDecorator.deleteComments(ACTIVITYURL, COMMENTURL, "susi.sonar", headers, false), is(false)); - - stubFor( - get(urlEqualTo("/activities")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/activity.json"))) - ) - ); - - stubFor( - delete(urlMatching("/comments/([0-9]*)\\?version=([0-9]*)")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("{}") - ) - ); - - assertThat(bitbucketServerPullRequestDecorator.deleteComments(ACTIVITYURL, COMMENTURL, "susi.sonar", headers, true), is(false)); - - stubFor( - delete(urlMatching("/comments/([0-9]*)\\?version=([0-9]*)")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(204) - .withHeader("Content-Type", "application/json") - .withBody("{}") - ) - ); - assertThat(bitbucketServerPullRequestDecorator.deleteComments(ACTIVITYURL, COMMENTURL, "susi.sonar", headers, true), is(true)); - } - @Test public void getIssueType() throws Exception{ - stubFor( - get(urlEqualTo("/diff")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/diff.json"))) - ) - ); + stubFor(get(urlEqualTo("/diff")).withHeader("Accept", equalTo("application/json")).willReturn( + aResponse().withStatus(200).withHeader("Content-Type", "application/json") + .withBody(FileUtils.readFileToByteArray(new File("src/test/resources/bitbucket/diff.json"))))); DiffPage diffPage = bitbucketServerPullRequestDecorator.getPage(DIFFURL, headers, DiffPage.class); // wrong file @@ -202,30 +106,13 @@ public void getIssueType() throws Exception{ @Test public void postComment() throws Exception{ StringEntity summaryComment = new StringEntity(new ObjectMapper().writeValueAsString(new SummaryComment("summaryComment")), ContentType.APPLICATION_JSON); - assertThat(bitbucketServerPullRequestDecorator.postComment(COMMENTURL, headers, summaryComment, false), is(false)); - - stubFor( - post(urlEqualTo("/comments")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(400) - .withHeader("Content-Type", "application/json") - .withBody("{}") - ) - ); - assertThat(bitbucketServerPullRequestDecorator.postComment(COMMENTURL, headers, summaryComment, true), is(false)); - - stubFor( - post(urlEqualTo("/comments")) - .withHeader("Accept" , equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{}") - ) - ); - assertThat(bitbucketServerPullRequestDecorator.postComment(COMMENTURL, headers, summaryComment, true), is(true)); + + stubFor(post(urlEqualTo("/comments")).withHeader("Accept", equalTo("application/json")).willReturn( + aResponse().withStatus(400).withHeader("Content-Type", "application/json").withBody("{}"))); + assertThat(bitbucketServerPullRequestDecorator.postComment(COMMENTURL, headers, summaryComment), is(false)); + + stubFor(post(urlEqualTo("/comments")).withHeader("Accept", equalTo("application/json")).willReturn( + aResponse().withStatus(201).withHeader("Content-Type", "application/json").withBody("{}"))); + assertThat(bitbucketServerPullRequestDecorator.postComment(COMMENTURL, headers, summaryComment), is(true)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java index 20531ad1a..d370ddd57 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,6 +21,8 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.security.GeneralSecurityException; @@ -28,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -47,8 +50,12 @@ public void testDecorateQualityGatePropagateException() throws IOException, Gene AnalysisDetails analysisDetails = mock(AnalysisDetails.class); GithubPullRequestDecorator testCase = new GithubPullRequestDecorator(checkRunProvider); - doThrow(dummyException).when(checkRunProvider).createCheckRun(any()); - assertThatThrownBy(() -> testCase.decorateQualityGateStatus(analysisDetails)) + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + + doThrow(dummyException).when(checkRunProvider).createCheckRun(any(), any(), any()); + assertThatThrownBy( + () -> testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) .hasMessage("Could not decorate Pull Request on Github") .isExactlyInstanceOf(IllegalStateException.class).hasCause(dummyException); } @@ -59,10 +66,13 @@ public void testDecorateQualityGateReturnValue() throws IOException, GeneralSecu AnalysisDetails analysisDetails = mock(AnalysisDetails.class); GithubPullRequestDecorator testCase = new GithubPullRequestDecorator(checkRunProvider); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); - testCase.decorateQualityGateStatus(analysisDetails); - verify(checkRunProvider).createCheckRun(argumentCaptor.capture()); + testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); + verify(checkRunProvider).createCheckRun(argumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto)); assertEquals(analysisDetails, argumentCaptor.getValue()); } } \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java index 779dc956c..e4edd6d2c 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -34,15 +34,13 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.config.Configuration; -import org.sonar.api.config.PropertyDefinition; -import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.platform.Server; import org.sonar.api.rule.Severity; import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.component.ReportAttributes; import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.security.GeneralSecurityException; @@ -73,78 +71,6 @@ public class GraphqlCheckRunProviderTest { - @Test - public void createCheckRunThrowsExceptionOnMissingProperty() { - GraphqlProvider graphqlProvider = mock(GraphqlProvider.class); - Clock clock = mock(Clock.class); - GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider = - mock(GithubApplicationAuthenticationProvider.class); - Server server = mock(Server.class); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - - Configuration configuration = mock(Configuration.class); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); - - GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails)) - .isExactlyInstanceOf(IllegalStateException.class) - .hasMessage("sonar.pullrequest.github.endpoint must be specified in the project configuration"); - } - - @Test - public void createCheckRunTreatsEmptyStringAsNullDefaultPropertyValue() { - GraphqlProvider graphqlProvider = mock(GraphqlProvider.class); - Clock clock = mock(Clock.class); - GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider = - mock(GithubApplicationAuthenticationProvider.class); - Server server = mock(Server.class); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - - Configuration configuration = mock(Configuration.class); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - - PropertyDefinition propertyDefinition = - PropertyDefinition.builder("sonar.pullrequest.github.endpoint").defaultValue("").build(); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(propertyDefinition); - - GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails)) - .isExactlyInstanceOf(IllegalStateException.class) - .hasMessage("sonar.pullrequest.github.endpoint must be specified in the project configuration"); - } - - @Test - public void createCheckRunOnMissingPropertyWithBlankDefault() { - GraphqlProvider graphqlProvider = mock(GraphqlProvider.class); - Clock clock = mock(Clock.class); - GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider = - mock(GithubApplicationAuthenticationProvider.class); - Server server = mock(Server.class); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - - Configuration configuration = mock(Configuration.class); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - - PropertyDefinition propertyDefinition = PropertyDefinition.builder("sonar.pullrequest.github.endpoint").build(); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(propertyDefinition); - - GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails)) - .isExactlyInstanceOf(IllegalStateException.class) - .hasMessage("sonar.pullrequest.github.endpoint must be specified in the project configuration"); - } - @Test public void createCheckRunExceptionOnErrorResponse() throws IOException, GeneralSecurityException { GraphqlProvider graphqlProvider = mock(GraphqlProvider.class, RETURNS_DEEP_STUBS); @@ -153,7 +79,6 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException, General mock(GithubApplicationAuthenticationProvider.class); Server server = mock(Server.class); when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); when(postAnalysisIssueVisitor.getIssues()).thenReturn(new ArrayList<>()); @@ -168,12 +93,6 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException, General when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - Configuration configuration = mock(Configuration.class); - when(configuration.get(anyString())) - .then(i -> "sonar.pullrequest.github.endpoint".equals(i.getArguments()[0]) ? Optional.empty() : - Optional.of(i.getArguments()[0])); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(repositoryAuthenticationToken.getAuthenticationToken()).thenReturn("dummyAuthToken"); when(repositoryAuthenticationToken.getRepositoryId()).thenReturn("repository ID"); @@ -190,22 +109,22 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException, General when(graphQLTemplate.mutate(any(), eq(CreateCheckRun.class))).thenReturn(graphQLResponseEntity); when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); - PropertyDefinition propertyDefinition = - PropertyDefinition.builder("sonar.pullrequest.github.endpoint").defaultValue("http://host.name") - .build(); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(propertyDefinition); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails)).hasMessage( + new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); + assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + .hasMessage( "An error was returned in the response from the Github API:" + System.lineSeparator() + "- Error{message='example message', locations=[]}").isExactlyInstanceOf(IllegalStateException.class); verify(githubApplicationAuthenticationProvider) - .getInstallationToken(eq("http://host.name"), eq("sonar.alm.github.app.id"), - eq("sonar.alm.github.app.privateKey.secured"), - eq("sonar.pullrequest.github.repository")); + .getInstallationToken(eq("http://host.name"), eq("app id"), eq("private key"), eq("dummy/repo")); } @@ -220,7 +139,6 @@ public void createCheckRunExceptionOnInvalidIssueSeverity() throws IOException, .thenReturn(mock(RepositoryAuthenticationToken.class)); Server server = mock(Server.class); when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); ReportAttributes reportAttributes = mock(ReportAttributes.class); when(reportAttributes.getScmPath()).thenReturn(Optional.of("path")); @@ -249,22 +167,13 @@ public void createCheckRunExceptionOnInvalidIssueSeverity() throws IOException, when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - Configuration configuration = mock(Configuration.class); - when(configuration.get(anyString())) - .then(i -> "sonar.pullrequest.github.endpoint".equals(i.getArguments()[0]) ? Optional.empty() : - Optional.of(i.getArguments()[0])); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - - - PropertyDefinition propertyDefinition = - PropertyDefinition.builder("sonar.pullrequest.github.endpoint").defaultValue("http://host.name") - .build(); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(propertyDefinition); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails)).hasMessage("Unknown severity value: dummy") + new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); + assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + .hasMessage("Unknown severity value: dummy") .isExactlyInstanceOf(IllegalArgumentException.class); } @@ -287,7 +196,6 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti mock(GithubApplicationAuthenticationProvider.class); Server server = mock(Server.class); when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); DefaultIssue issue1 = mock(DefaultIssue.class); when(issue1.getLine()).thenReturn(2); @@ -377,12 +285,6 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - Configuration configuration = mock(Configuration.class); - when(configuration.get(anyString())).then(i -> "sonar.pullrequest.github.endpoint".equals(i.getArguments()[0]) ? - Optional.of("http://host.name") : - Optional.of(i.getArguments()[0])); - when(configurationRepository.getConfiguration()).thenReturn(configuration); - ArgumentCaptor authenticationProviderArgumentCaptor = ArgumentCaptor.forClass(String.class); RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(repositoryAuthenticationToken.getAuthenticationToken()).thenReturn("dummyAuthToken"); @@ -432,12 +334,16 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti .thenReturn(graphQLResponseEntity); when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); GraphqlCheckRunProvider testCase = - new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions); - testCase.createCheckRun(analysisDetails); + new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); + testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); assertEquals(1, requestBuilders.size()); @@ -457,12 +363,9 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti assertEquals("createCheckRun", argumentsArgumentCaptor.getValue().getDotPath()); assertEquals(1, argumentsArgumentCaptor.getValue().getArguments().size()); assertEquals("input", argumentsArgumentCaptor.getValue().getArguments().get(0).getKey()); -// assertThat(argumentsArgumentCaptor.getValue().getArguments().get(0).getValue()).usingRecursiveComparison().isEqualTo(inputObjects.get(1)); - assertEquals( - Arrays.asList("http://host.name", "sonar.alm.github.app.id", "sonar.alm.github.app.privateKey.secured", - "sonar.pullrequest.github.repository"), - authenticationProviderArgumentCaptor.getAllValues()); + assertEquals(Arrays.asList("http://host.name", "app id", "private key", "dummy/repo"), + authenticationProviderArgumentCaptor.getAllValues()); List> expectedAnnotationObjects = new ArrayList<>(); int position = 0; @@ -508,7 +411,7 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti assertThat(annotationArgumentCaptor.getValue()).isEqualTo(expectedAnnotationObjects); verify(inputObjectBuilders.get(position + 1)).put(eq("repositoryId"), eq("repository ID")); - verify(inputObjectBuilders.get(position + 1)).put(eq("name"), eq("sonar.alm.github.app.name Results")); + verify(inputObjectBuilders.get(position + 1)).put(eq("name"), eq("Sonarqube Results")); verify(inputObjectBuilders.get(position + 1)).put(eq("headSha"), eq("commit SHA")); verify(inputObjectBuilders.get(position + 1)).put(eq("status"), eq(RequestableCheckStatusState.COMPLETED)); verify(inputObjectBuilders.get(position + 1)).put(eq("conclusion"), eq(status == QualityGate.Status.OK ? @@ -529,12 +432,9 @@ public void checkCorrectDefaultValuesInjected() { GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider = mock(GithubApplicationAuthenticationProvider.class); Server server = mock(Server.class); - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); - PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); - assertThat(new GraphqlCheckRunProvider(clock, githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions)).usingRecursiveComparison() + assertThat(new GraphqlCheckRunProvider(clock, githubApplicationAuthenticationProvider, server)) + .usingRecursiveComparison() .isEqualTo(new GraphqlCheckRunProvider(new DefaultGraphqlProvider(), clock, - githubApplicationAuthenticationProvider, server, - configurationRepository, propertyDefinitions)); + githubApplicationAuthenticationProvider, server)); } } \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index 204f9e8b8..e42860a21 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -1,29 +1,46 @@ +/* + * Copyright (C) 2019 Markus Heberling + * + * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Optional; - import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfo; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Optional; import static com.github.tomakehurst.wiremock.client.WireMock.created; import static com.github.tomakehurst.wiremock.client.WireMock.delete; @@ -35,6 +52,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,22 +74,20 @@ public void decorateQualityGateStatus() { String filePath = "/path/to/file"; int lineNumber = 5; - ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); - Configuration configuration = mock(Configuration.class); - - when(configurationRepository.getConfiguration()).thenReturn(configuration); - when(configuration.get(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL)).thenReturn(Optional.of(wireMockRule.baseUrl())); - when(configuration.get(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_TOKEN)).thenReturn(Optional.of("token")); - when(configuration.get(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)).thenReturn(Optional.of(repositorySlug)); - when(configuration.get(PullRequestBuildStatusDecorator.PULL_REQUEST_COMMENT_SUMMARY_ENABLED)).thenReturn(Optional.of("true")); - when(configuration.get(PullRequestBuildStatusDecorator.PULL_REQUEST_DELETE_COMMENTS_ENABLED)).thenReturn(Optional.of("true")); - when(configuration.get(PullRequestBuildStatusDecorator.PULL_REQUEST_FILE_COMMENT_ENABLED)).thenReturn(Optional.of("true")); - QualityGate.Condition coverage = mock(QualityGate.Condition.class); when(coverage.getStatus()).thenReturn(QualityGate.EvaluationStatus.OK); when(coverage.getValue()).thenReturn("10"); + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getPersonalAccessToken()).thenReturn("token"); + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL))) + .thenReturn(Optional.of(wireMockRule.baseUrl())); + when(analysisDetails + .getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG))) + .thenReturn(Optional.of(repositorySlug)); when(analysisDetails.getAnalysisProjectKey()).thenReturn(projectKey); when(analysisDetails.getBranchName()).thenReturn(branchName); when(analysisDetails.getCommitSha()).thenReturn(commitSHA); @@ -96,70 +112,89 @@ public void decorateQualityGateStatus() { when(scmInfo.getChangesetForLine(anyInt())).thenReturn(Changeset.newChangesetBuilder().setDate(0L).setRevision(commitSHA).build()); when(scmInfoRepository.getScmInfo(component)).thenReturn(Optional.of(scmInfo)); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/user")).withHeader("PRIVATE-TOKEN", equalTo("token")).willReturn(okJson("{\n" + - " \"id\": 1,\n" + - " \"username\": \"" + user + "\"}"))); + " \"id\": 1,\n" + + " \"username\": \"" + + user + + "\"}"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName)).willReturn(okJson("{\n" + - " \"id\": 15235,\n" + - " \"iid\": " + branchName + ",\n" + - " \"diff_refs\": {\n" + - " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + - " \"head_sha\":\"" + commitSHA + "\",\n" + - " \"start_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\"}\n" + - "}"))); + " \"id\": 15235,\n" + + " \"iid\": " + + branchName + + ",\n" + + " \"diff_refs\": {\n" + + " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + + " \"head_sha\":\"" + + commitSHA + + "\",\n" + + " \"start_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\"}\n" + + "}"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/commits")).willReturn(okJson("[\n" + - " {\n" + - " \"id\": \"" + commitSHA + "\"\n" + - " }]"))); + " {\n" + + " \"id\": \"" + + commitSHA + + "\"\n" + + " }]"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")).willReturn(okJson("[\n" + - " {\n" + - " \"id\": \"" + discussionId + "\",\n" + - " \"individual_note\": false,\n" + - " \"notes\": [\n" + - " {\n" + - " \"id\": " + noteId + ",\n" + - " \"type\": \"DiscussionNote\",\n" + - " \"body\": \"discussion text\",\n" + - " \"attachment\": null,\n" + - " \"author\": {\n" + - " \"id\": 1,\n" + - " \"username\": \"" + user + "\"\n" + - " }}]}]"))); + " {\n" + + " \"id\": \"" + + discussionId + + "\",\n" + + " \"individual_note\": false,\n" + + " \"notes\": [\n" + + " {\n" + + " \"id\": " + + noteId + + ",\n" + + " \"type\": \"DiscussionNote\",\n" + + " \"body\": \"discussion text\",\n" + + " \"attachment\": null,\n" + + " \"author\": {\n" + + " \"id\": 1,\n" + + " \"username\": \"" + + user + + "\"\n" + + " }}]}]"))); wireMockRule.stubFor(delete(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions/" + discussionId + "/notes/" + noteId)).willReturn(noContent())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/statuses/" + commitSHA)) - .withQueryParam("name", equalTo("SonarQube")) - .withQueryParam("state", equalTo("failed")) - .withQueryParam("target_url", equalTo(sonarRootUrl + "/dashboard?id=" + projectKey + "&pullRequest=" + branchName)) - .withQueryParam("coverage", equalTo(coverage.getValue())) - .willReturn(created())); + .withQueryParam("name", equalTo("SonarQube")) + .withQueryParam("state", equalTo("failed")).withQueryParam("target_url", + equalTo(sonarRootUrl + + "/dashboard?id=" + + projectKey + + "&pullRequest=" + + branchName)) + .withQueryParam("coverage", equalTo(coverage.getValue())).willReturn(created())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) - .withRequestBody(equalTo("body=summary")) - .willReturn(created())); + .withRequestBody(equalTo("body=summary")).willReturn(created())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) - .withRequestBody(equalTo("body=issue&" + - urlEncode("position[base_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + - urlEncode("position[start_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + - urlEncode("position[head_sha]") + "=" + commitSHA + "&" + - urlEncode("position[new_path]") + "=" + urlEncode(filePath) + "&" + - urlEncode("position[new_line]") + "=" + lineNumber + "&" + - urlEncode("position[position_type]") + "=text")) - .willReturn(created())); + .withRequestBody(equalTo("body=issue&" + urlEncode("position[base_sha]") + + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[start_sha]") + + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[head_sha]") + "=" + commitSHA + "&" + + urlEncode("position[new_path]") + "=" + + urlEncode(filePath) + "&" + + urlEncode("position[new_line]") + "=" + lineNumber + "&" + + urlEncode("position[position_type]") + "=text")) + .willReturn(created())); Server server = mock(Server.class); when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); - GitlabServerPullRequestDecorator pullRequestDecorator = new GitlabServerPullRequestDecorator(server, configurationRepository, scmInfoRepository); + GitlabServerPullRequestDecorator pullRequestDecorator = + new GitlabServerPullRequestDecorator(server, scmInfoRepository); - pullRequestDecorator.decorateQualityGateStatus(analysisDetails); + pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } - private String urlEncode(String value) { + private static String urlEncode(String value) { try { return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java index f484f34e5..05e37da68 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,7 +23,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.sonar.api.CoreProperties; import org.sonar.api.utils.MessageException; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchInfo; @@ -34,13 +33,11 @@ import java.util.HashMap; import java.util.Map; -import java.util.function.Supplier; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,7 +46,6 @@ */ public class CommunityBranchConfigurationLoaderTest { - private final Supplier> supplier = mock(Supplier.class); private final ExpectedException expectedException = ExpectedException.none(); @Rule @@ -169,7 +165,7 @@ public void testValidBranchInfoWhenAllBranchParametersSpecified() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("masterBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); @@ -178,8 +174,8 @@ public void testValidBranchInfoWhenAllBranchParametersSpecified() { assertEquals("master", result.targetBranchName()); assertEquals("feature/shortLivedFeatureBranch", result.branchName()); - assertEquals("masterBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("masterBranchInfo", result.referenceBranchName()); + assertFalse(result.isPullRequest()); expectedException .expectMessage(IsEqual.equalTo("Only a branch of type PULL_REQUEST can have a Pull Request key")); @@ -196,7 +192,7 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); @@ -206,8 +202,8 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists() { assertEquals("masterxxx", result.targetBranchName()); assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("defaultBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("defaultBranchInfo", result.referenceBranchName()); + assertFalse(result.isPullRequest()); } @Test @@ -219,7 +215,7 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists2() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); @@ -229,8 +225,8 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists2() { assertEquals("masterxxx", result.targetBranchName()); assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("defaultBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("defaultBranchInfo", result.referenceBranchName()); + assertFalse(result.isPullRequest()); } @Test @@ -241,7 +237,7 @@ public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExists() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); @@ -252,34 +248,6 @@ public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExists() { } - - @Test - public void testShortLivedBranchInvalidTarget() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "feature/otherShortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("feature/otherShortLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.SHORT); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("feature/otherShortLivedBranch")).thenReturn(mockTargetBranchInfo); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo("Could not target requested branch")); - expectedException.expectCause(new CustomMatcher("Cause checker") { - @Override - public boolean matches(Object item) { - return item instanceof IllegalStateException && - ((IllegalStateException) item).getMessage().equals("Expected branch type of LONG but got SHORT"); - } - }); - - testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - } - @Test public void testUnknownTargetBranch() { CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); @@ -302,73 +270,15 @@ public boolean matches(Object item) { testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); } - - @Test - public void testShortLivedBranchExistingSourceAllParametersCorrect() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "longLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - BranchInfo mockSourceBranchInfo = mock(BranchInfo.class); - when(mockSourceBranchInfo.name()).thenReturn("shortLivedBranch"); - when(mockSourceBranchInfo.type()).thenReturn(BranchType.SHORT); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("longLivedBranch")).thenReturn(mockTargetBranchInfo); - when(projectBranches.get("feature/shortLivedBranch")).thenReturn(mockSourceBranchInfo); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("longLivedBranch", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("longLivedBranch", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - @Test - public void testExistingShortLivedBranchOnlySourceParametersRetargetMaster() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - BranchInfo mockSourceBranchInfo = mock(BranchInfo.class); - when(mockSourceBranchInfo.name()).thenReturn("shortLivedBranch"); - when(mockSourceBranchInfo.branchTargetName()).thenReturn("otherLongLivedBranch"); - when(mockSourceBranchInfo.type()).thenReturn(BranchType.SHORT); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.get("feature/shortLivedBranch")).thenReturn(mockSourceBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testExistingLongLivedBranchOnlySourceParameters() { + public void testExistingBranchOnlySourceParameters() { CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "longLivedBranch"); BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); @@ -378,8 +288,8 @@ public void testExistingLongLivedBranchOnlySourceParameters() { assertNull(result.targetBranchName()); assertEquals("longLivedBranch", result.branchName()); - assertEquals("longLivedBranch", result.longLivingSonarReferenceBranch()); - assertFalse(result.isShortOrPullRequest()); + assertEquals("longLivedBranch", result.referenceBranchName()); + assertFalse(result.isPullRequest()); } @Test @@ -392,7 +302,7 @@ public void testPullRequestAllParameters() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("targetInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); @@ -402,8 +312,8 @@ public void testPullRequestAllParameters() { assertEquals("target", result.targetBranchName()); assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("target", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("target", result.referenceBranchName()); + assertTrue(result.isPullRequest()); assertEquals("pr-key", result.pullRequestKey()); } @@ -417,7 +327,7 @@ public void testPullRequestMandatoryParameters() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); @@ -428,8 +338,8 @@ public void testPullRequestMandatoryParameters() { assertEquals("master", result.targetBranchName()); assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("master", result.referenceBranchName()); + assertTrue(result.isPullRequest()); } @Test @@ -442,7 +352,7 @@ public void testPullRequestMandatoryParameters2() { BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); + when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); ProjectBranches projectBranches = mock(ProjectBranches.class); @@ -453,8 +363,8 @@ public void testPullRequestMandatoryParameters2() { assertEquals("master", result.targetBranchName()); assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); + assertEquals("master", result.referenceBranchName()); + assertTrue(result.isPullRequest()); } @@ -482,518 +392,4 @@ public boolean matches(Object item) { testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); } - @Test - public void testComputeBranchType() { - BranchInfo branchInfo = mock(BranchInfo.class); - when(branchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.defaultBranchName()).thenReturn("master"); - when(projectBranches.get(eq("master"))).thenReturn(branchInfo); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "release/1.2"); - parameters.put(CoreProperties.LONG_LIVED_BRANCHES_REGEX, "(master|release/.+)"); - - assertEquals(BranchType.LONG, new CommunityBranchConfigurationLoader() - .load(parameters, projectBranches, mock(ProjectPullRequests.class)).branchType()); - - parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master-dummy"); - - assertEquals(BranchType.SHORT, new CommunityBranchConfigurationLoader() - .load(parameters, projectBranches, mock(ProjectPullRequests.class)).branchType()); - - } - - - @Test - public void testExceptionWhenNoExistingBranchAndBranchParamsPresentPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "dummy"); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo( - "No branches currently exist in this project. Please scan the main branch without passing any branch parameters.")); - - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)); - } - - @Test - public void testDefaultConfigWhenNoExistingBranchAndBranchNameParamMasterPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master"); - - assertEquals(DefaultBranchConfiguration.class, - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)).getClass()); - } - - @Test - public void testErrorWhenNoExistingBranchAndBranchTargetMasterButNoSourceBranchPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.source", null); - parameters.put("sonar.branch.target", "master"); - - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo( - "No branches currently exist in this project. Please scan the main branch without passing any branch parameters.")); - - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)); - } - - - @Test - public void testDefaultConfigWhenNoExistingBranchAndBranchParamsAllMasterPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master"); - parameters.put("sonar.branch.target", "master"); - - assertEquals(DefaultBranchConfiguration.class, - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)).getClass()); - } - - @Test - public void testExceptionWhenNoExistingBranchAndPullRequestAndBranchParametersPresentPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "dummy"); - parameters.put("sonar.pullrequest.branch", "dummy2"); - - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo( - "No branches currently exist in this project. Please scan the main branch without passing any branch parameters.")); - - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)); - } - - @Test - public void testDefaultBranchInfoWhenNoBranchParametersSpecifiedAndNoBranchesExistPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("dummy", "dummy"); - - - assertEquals(DefaultBranchConfiguration.class, - testCase.load(parameters, supplier, branchInfo, mock(ProjectPullRequests.class)).getClass()); - } - - @Test - public void testDefaultBranchInfoWhenNoParametersSpecifiedPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - assertEquals(DefaultBranchConfiguration.class, - testCase.load(new HashMap<>(), supplier, mock(ProjectBranches.class), - mock(ProjectPullRequests.class)).getClass()); - } - - @Test - public void testValidBranchInfoWhenAllBranchParametersSpecifiedPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedFeatureBranch"); - parameters.put("sonar.branch.target", "master"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/shortLivedFeatureBranch", result.branchName()); - assertEquals("masterBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - - expectedException - .expectMessage(IsEqual.equalTo("Only a branch of type PULL_REQUEST can have a Pull Request key")); - expectedException.expect(IllegalStateException.class); - - result.pullRequestKey(); - } - - @Test - public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExistsPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("masterxxx"); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("masterxxx", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("defaultBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists2Pre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", ""); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("masterxxx"); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("masterxxx", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("defaultBranchInfo", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExistsPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - - when(supplier.get()).thenReturn(new HashMap<>()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo("Could not target requested branch")); - - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - } - - - @Test - public void testShortLivedBranchInvalidTargetPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "feature/otherShortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("feature/otherShortLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.SHORT); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("feature/otherShortLivedBranch")).thenReturn(mockTargetBranchInfo); - - when(supplier.get()).thenReturn(new HashMap<>()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo("Could not target requested branch")); - expectedException.expectCause(new CustomMatcher("Cause checker") { - @Override - public boolean matches(Object item) { - return item instanceof IllegalStateException && - ((IllegalStateException) item).getMessage().equals("Expected branch type of LONG but got SHORT"); - } - }); - - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - } - - @Test - public void testUnknownTargetBranchPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "feature/otherShortLivedBranch"); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - - when(supplier.get()).thenReturn(new HashMap<>()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo("Could not target requested branch")); - expectedException.expectCause(new CustomMatcher("Cause checker") { - @Override - public boolean matches(Object item) { - return item instanceof IllegalStateException && ((IllegalStateException) item).getMessage() - .equals("Target branch 'feature/otherShortLivedBranch' does not exist"); - } - }); - - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - } - - - @Test - public void testShortLivedBranchExistingSourceAllParametersCorrectPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "longLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - BranchInfo mockSourceBranchInfo = mock(BranchInfo.class); - when(mockSourceBranchInfo.name()).thenReturn("shortLivedBranch"); - when(mockSourceBranchInfo.type()).thenReturn(BranchType.SHORT); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("longLivedBranch")).thenReturn(mockTargetBranchInfo); - when(projectBranches.get("feature/shortLivedBranch")).thenReturn(mockSourceBranchInfo); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("longLivedBranch", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("longLivedBranch", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testExistingShortLivedBranchOnlySourceParametersRetargetMasterPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - BranchInfo mockSourceBranchInfo = mock(BranchInfo.class); - when(mockSourceBranchInfo.name()).thenReturn("shortLivedBranch"); - when(mockSourceBranchInfo.branchTargetName()).thenReturn("otherLongLivedBranch"); - when(mockSourceBranchInfo.type()).thenReturn(BranchType.SHORT); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.get("feature/shortLivedBranch")).thenReturn(mockSourceBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testExistingLongLivedBranchOnlySourceParametersPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "longLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("longLivedBranch")).thenReturn(mockTargetBranchInfo); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertNull(result.targetBranchName()); - assertEquals("longLivedBranch", result.branchName()); - assertEquals("longLivedBranch", result.longLivingSonarReferenceBranch()); - assertFalse(result.isShortOrPullRequest()); - } - - @Test - public void testPullRequestAllParametersPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.base", "target"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("targetInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("target")).thenReturn(mockTargetBranchInfo); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("target", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("target", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - assertEquals("pr-key", result.pullRequestKey()); - } - - - @Test - public void testPullRequestMandatoryParametersPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - @Test - public void testPullRequestMandatoryParameters2Pre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.key", "pr-key"); - parameters.put("sonar.pullrequest.base", ""); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.LONG); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - when(supplier.get()).thenReturn(new HashMap<>()); - - BranchConfiguration result = - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.longLivingSonarReferenceBranch()); - assertTrue(result.isShortOrPullRequest()); - } - - - @Test - public void testPullRequestNoSuchTargetPre79() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.base", "missingTarget"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - - when(supplier.get()).thenReturn(new HashMap<>()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage(IsEqual.equalTo("Could not target requested branch")); - expectedException.expectCause(new CustomMatcher("Cause checker") { - @Override - public boolean matches(Object item) { - return item instanceof IllegalStateException && ((IllegalStateException) item).getMessage() - .equals("Target branch 'missingTarget' does not exist"); - } - }); - - testCase.load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)); - } - - @Test - public void testComputeBranchTypePre79() { - Map settings = new HashMap<>(); - settings.put(CoreProperties.LONG_LIVED_BRANCHES_REGEX, "(master|release/.+)"); - when(supplier.get()).thenReturn(settings); - - BranchInfo branchInfo = mock(BranchInfo.class); - when(branchInfo.type()).thenReturn(BranchType.LONG); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.defaultBranchName()).thenReturn("master"); - when(projectBranches.get(eq("master"))).thenReturn(branchInfo); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "release/1.2"); - - assertEquals(BranchType.LONG, new CommunityBranchConfigurationLoader() - .load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)).branchType()); - - parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master-dummy"); - - assertEquals(BranchType.SHORT, new CommunityBranchConfigurationLoader() - .load(parameters, supplier, projectBranches, mock(ProjectPullRequests.class)).branchType()); - - } - - } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidatorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidatorTest.java index 7911a57ea..ec00da402 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidatorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchParamsValidatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,76 +19,22 @@ package com.github.mc1arke.sonarqube.plugin.scanner; import org.junit.Test; -import org.sonar.scanner.bootstrap.GlobalConfiguration; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** * @author Michael Clarke */ public class CommunityBranchParamsValidatorTest { - @Test - public void testNoMessagesOnNoParams() { - List messages = new ArrayList<>(); - - GlobalConfiguration globalConfiguration = mock(GlobalConfiguration.class); - new CommunityBranchParamsValidator(globalConfiguration).validate(messages, null); - assertTrue(messages.isEmpty()); - } - - @Test - public void testNoMessagesOnlyLegacyBranch() { - List messages = new ArrayList<>(); - - GlobalConfiguration globalConfiguration = mock(GlobalConfiguration.class); - new CommunityBranchParamsValidator(globalConfiguration).validate(messages, "legacy"); - assertTrue(messages.isEmpty()); - } - - @Test - public void testMessagesBranchParamsAndLegacyBranch() { - List messages = new ArrayList<>(); - - GlobalConfiguration globalConfiguration = mock(GlobalConfiguration.class); - when(globalConfiguration.hasKey(any())).thenReturn(true); - new CommunityBranchParamsValidator(globalConfiguration).validate(messages, "legacy"); - assertEquals(1, messages.size()); - assertEquals( - "The legacy 'sonar.branch' parameter cannot be used at the same time as 'sonar.branch.name' or 'sonar.branch.target'", - messages.get(0)); - } - - @Test - public void testMessagesBranchParamsAndLegacyBranch2() { - List messages = new ArrayList<>(); - - GlobalConfiguration globalConfiguration = mock(GlobalConfiguration.class); - when(globalConfiguration.hasKey(eq("sonar.branch.target"))).thenReturn(true); - when(globalConfiguration.hasKey(eq("sonar.branch"))).thenReturn(true); - new CommunityBranchParamsValidator(globalConfiguration).validate(messages, "legacy"); - assertEquals(1, messages.size()); - assertEquals( - "The legacy 'sonar.branch' parameter cannot be used at the same time as 'sonar.branch.name' or 'sonar.branch.target'", - messages.get(0)); - } - @Test public void testNoMessagesOnValidate() { List messages = new ArrayList<>(); - GlobalConfiguration globalConfiguration = mock(GlobalConfiguration.class); - when(globalConfiguration.hasKey(eq("sonar.branch.target"))).thenReturn(true); - when(globalConfiguration.hasKey(eq("sonar.branch"))).thenReturn(true); - new CommunityBranchParamsValidator(globalConfiguration).validate(messages); + new CommunityBranchParamsValidator().validate(messages); assertEquals(0, messages.size()); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoaderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoaderTest.java index 3ccb20256..70a5a71cd 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoaderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityProjectBranchesLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -79,7 +79,7 @@ public void testAllBranchesFromNonEmptyServerResponse() { List infos = new ArrayList<>(); for (int i = 0; i < 10; i++) { - infos.add(new BranchInfo("key" + i, BranchType.LONG, i == 1, "target" + i)); + infos.add(new BranchInfo("key" + i, BranchType.BRANCH, i == 1, "target" + i)); } StringReader stringReader = new StringReader( diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapperTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapperTest.java deleted file mode 100644 index b34e522c8..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerWsClientWrapperTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * 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 com.github.mc1arke.sonarqube.plugin.scanner; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.core.IsEqual; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.scanner.bootstrap.ScannerWsClient; -import org.sonarqube.ws.client.WsRequest; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; - -public class ScannerWsClientWrapperTest { - - private final ExpectedException expectedException = ExpectedException.none(); - - @Rule - public ExpectedException expectedException() { - return expectedException; - } - - @Test - public void testRuntimeExceptionPropagated() { - ScannerWsClient scannerWsClient = mock(ScannerWsClient.class); - doThrow(new IllegalStateException("Whoops")).when(scannerWsClient).call(any()); - - expectedException.expectMessage(IsEqual.equalTo("Whoops")); - expectedException.expect(IllegalStateException.class); - - new ScannerWsClientWrapper(scannerWsClient).call(mock(WsRequest.class)); - } - - @Test - public void testErrorPropagated() { - ScannerWsClient scannerWsClient = mock(ScannerWsClient.class); - doThrow(new ClassFormatError("Whoops")).when(scannerWsClient).call(any()); - - expectedException.expectMessage(IsEqual.equalTo("Whoops")); - expectedException.expect(ClassFormatError.class); - - new ScannerWsClientWrapper(scannerWsClient).call(mock(WsRequest.class)); - } - - @Test - public void testCheckedExceptionWrapped() { - ScannerWsClient scannerWsClient = mock(ScannerWsClient.class); - doAnswer(i -> { - throw new IOException("Whoops"); - }).when(scannerWsClient).call(any()); - - expectedException.expectMessage(IsEqual.equalTo("Could not execute ScannerWsClient")); - expectedException.expect(IllegalStateException.class); - expectedException.expectCause(new BaseMatcher() { - @Override - public boolean matches(Object item) { - return item instanceof InvocationTargetException && - ((InvocationTargetException) item).getCause() instanceof IOException && - "Whoops".equals(((InvocationTargetException) item).getCause().getMessage()); - } - - @Override - public void describeTo(Description description) { - description.appendText("Exception checked"); - } - }); - - new ScannerWsClientWrapper(scannerWsClient).call(mock(WsRequest.class)); - } - - - @Test - public void testNonInvocationExceptionWrapped() { - expectedException.expectMessage(IsEqual.equalTo("Could not execute ScannerWsClient")); - expectedException.expect(IllegalStateException.class); - expectedException.expectCause(new BaseMatcher() { - @Override - public boolean matches(Object item) { - return item instanceof NoSuchMethodException && - "java.lang.Object.call(org.sonarqube.ws.client.WsRequest)" - .equals(((NoSuchMethodException) item).getMessage()); - } - - @Override - public void describeTo(Description description) { - description.appendText("NoSuchMethodException"); - } - }); - - Object scannerWsClient = new Object(); - new ScannerWsClientWrapper(scannerWsClient).call(mock(WsRequest.class)); - } -} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegateTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegateTest.java index 37ff88e8f..e5e59a848 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegateTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/CommunityBranchSupportDelegateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2020 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -63,32 +63,12 @@ public ExpectedException expectedException() { } @Test - public void testCreateComponentKeyShortBranch() { - Map params = new HashMap<>(); - params.put("branch", "feature/dummy"); - params.put("branchType", "SHORT"); - - CommunityComponentKey componentKey = - new CommunityBranchSupportDelegate(new SequenceUuidFactory(), mock(DbClient.class), mock(Clock.class)) - .createComponentKey("xxx", params); - assertEquals("xxx:BRANCH:feature/dummy", componentKey.getDbKey()); - assertEquals("xxx", componentKey.getKey()); - assertFalse(componentKey.getPullRequestKey().isPresent()); - assertFalse(componentKey.isMainBranch()); - assertTrue(componentKey.getBranch().isPresent()); - assertEquals("feature/dummy", componentKey.getBranch().get().getName()); - assertFalse(componentKey.getDeprecatedBranchName().isPresent()); - assertTrue(componentKey.getMainBranchComponentKey().isMainBranch()); - assertEquals(BranchType.SHORT, componentKey.getBranch().get().getType()); - } - - @Test - public void testCreateComponentKeyLongBranch() { + public void testCreateComponentKeyBranchType() { Map params = new HashMap<>(); params.put("branch", "release-1.1"); - params.put("branchType", "LONG"); + params.put("branchType", "BRANCH"); - CommunityComponentKey componentKey = + BranchSupport.ComponentKey componentKey = new CommunityBranchSupportDelegate(new SequenceUuidFactory(), mock(DbClient.class), mock(Clock.class)) .createComponentKey("yyy", params); @@ -98,9 +78,8 @@ public void testCreateComponentKeyLongBranch() { assertFalse(componentKey.isMainBranch()); assertTrue(componentKey.getBranch().isPresent()); assertEquals("release-1.1", componentKey.getBranch().get().getName()); - assertFalse(componentKey.getDeprecatedBranchName().isPresent()); assertTrue(componentKey.getMainBranchComponentKey().isMainBranch()); - assertEquals(BranchType.LONG, componentKey.getBranch().get().getType()); + assertEquals(BranchType.BRANCH, componentKey.getBranch().get().getType()); } @Test @@ -117,7 +96,6 @@ public void testCreateComponentKeyPullRequest() { assertEquals("pullrequestkey", componentKey.getPullRequestKey().get()); assertFalse(componentKey.isMainBranch()); assertFalse(componentKey.getBranch().isPresent()); - assertFalse(componentKey.getDeprecatedBranchName().isPresent()); assertTrue(componentKey.getMainBranchComponentKey().isMainBranch()); CommunityComponentKey mainBranchComponentKey = componentKey.getMainBranchComponentKey(); assertSame(mainBranchComponentKey, mainBranchComponentKey.getMainBranchComponentKey()); @@ -148,18 +126,6 @@ public void testCreateComponentKeyInvalidBranchTypeParameter() { .createComponentKey("xxx", params); } - @Test - public void testCreateComponentKeyInvalidBranchTypeParameter2() { - Map params = new HashMap<>(); - params.put("branchType", "PULL_REQUEST"); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage(IsEqual.equalTo("Unsupported branch type 'PULL_REQUEST'")); - - new CommunityBranchSupportDelegate(new SequenceUuidFactory(), mock(DbClient.class), mock(Clock.class)) - .createComponentKey("xxx", params); - } - @Test public void testCreateBranchComponentComponentKeyComponentDtoKeyMismatch() { DbSession dbSession = mock(DbSession.class); @@ -174,7 +140,7 @@ public void testCreateBranchComponentComponentKeyComponentDtoKeyMismatch() { BranchDto branchDto = mock(BranchDto.class); when(branchDto.getUuid()).thenReturn("componentUuid"); - when(branchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH); Clock clock = mock(Clock.class); when(clock.millis()).thenReturn(12345678901234L); @@ -182,7 +148,7 @@ public void testCreateBranchComponentComponentKeyComponentDtoKeyMismatch() { BranchSupport.ComponentKey componentKey = mock(BranchSupport.ComponentKey.class); when(componentKey.getKey()).thenReturn("componentKey"); when(componentKey.getDbKey()).thenReturn("dbKey"); - when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.LONG))); + when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.BRANCH))); when(componentKey.getPullRequestKey()).thenReturn(Optional.empty()); ComponentDao componentDao = spy(mock(ComponentDao.class)); @@ -222,7 +188,7 @@ public void testCreateBranchComponent() { BranchSupport.ComponentKey componentKey = mock(BranchSupport.ComponentKey.class); when(componentKey.getKey()).thenReturn("componentKey"); when(componentKey.getDbKey()).thenReturn("dbKey"); - when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.LONG))); + when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.BRANCH))); when(componentKey.getPullRequestKey()).thenReturn(Optional.empty()); ComponentDao componentDao = spy(mock(ComponentDao.class)); @@ -273,7 +239,7 @@ public void testCreateBranchComponentUseExistingDto() { BranchDto branchDto = mock(BranchDto.class); when(branchDto.getUuid()).thenReturn("componentUuid"); when(branchDto.getKey()).thenReturn("dummy"); - when(branchDto.getBranchType()).thenReturn(BranchType.LONG); + when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH); Clock clock = mock(Clock.class); when(clock.millis()).thenReturn(1234567890123L); @@ -281,7 +247,7 @@ public void testCreateBranchComponentUseExistingDto() { BranchSupport.ComponentKey componentKey = mock(BranchSupport.ComponentKey.class); when(componentKey.getKey()).thenReturn("componentKey"); when(componentKey.getDbKey()).thenReturn("dbKey"); - when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.LONG))); + when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.BRANCH))); when(componentKey.getPullRequestKey()).thenReturn(Optional.empty()); ComponentDao componentDao = spy(mock(ComponentDao.class)); @@ -313,7 +279,7 @@ public void testCreateBranchComponentUseExistingDto2() { BranchDto branchDto = mock(BranchDto.class); when(branchDto.getUuid()).thenReturn("componentUuid"); when(branchDto.getKey()).thenReturn("dummy"); - when(branchDto.getBranchType()).thenReturn(BranchType.SHORT); + when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH); Clock clock = mock(Clock.class); when(clock.millis()).thenReturn(1234567890123L); From a228fc0cb63ba312bf7c340449eb3f3336cb3da2 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Tue, 11 Feb 2020 17:22:07 +0100 Subject: [PATCH 02/26] [GITLAB] URL needs to point to the GitLab API Fixes #84 --- .../gitlab/GitlabServerPullRequestDecorator.java | 11 +++++------ .../scanner/ScannerPullRequestPropertySensor.java | 6 +++--- .../gitlab/GitlabServerPullRequestDecoratorTest.java | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 983b4ee06..cca39265d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -67,8 +67,8 @@ public class GitlabServerPullRequestDecorator implements PullRequestBuildStatusDecorator { - public static final String PULLREQUEST_GITLAB_URL = - "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.url"; + public static final String PULLREQUEST_GITLAB_API_URL = + "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.api.url"; public static final String PULLREQUEST_GITLAB_REPOSITORY_SLUG = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.repositorySlug"; @@ -93,10 +93,10 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al String revision = analysis.getCommitSha(); try { - final String hostURL = analysis.getScannerProperty(PULLREQUEST_GITLAB_URL).orElseThrow( + final String apiURL = analysis.getScannerProperty(PULLREQUEST_GITLAB_API_URL).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate Gitlab merge request. '%s' has not been set in scanner properties", - PULLREQUEST_GITLAB_URL))); + PULLREQUEST_GITLAB_API_URL))); final String apiToken = almSettingDto.getPersonalAccessToken(); final String repositorySlug = analysis.getScannerProperty(PULLREQUEST_GITLAB_REPOSITORY_SLUG).orElseThrow( () -> new IllegalStateException(String.format( @@ -104,8 +104,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al PULLREQUEST_GITLAB_REPOSITORY_SLUG))); final String pullRequestId = analysis.getBranchName(); - final String restURL = String.format("%s/api/v4", hostURL); - final String projectURL = restURL + String.format("/projects/%s", URLEncoder.encode(repositorySlug, StandardCharsets.UTF_8.name())); + final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(repositorySlug, StandardCharsets.UTF_8.name())); final String statusUrl = projectURL + String.format("/statuses/%s", revision); final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); final String prCommitsURL = mergeRequestURl + "/commits"; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java index 660c5a61d..6b1880977 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java @@ -44,12 +44,12 @@ public void describe(SensorDescriptor sensorDescriptor) { public void execute(SensorContext sensorContext) { if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { Optional.ofNullable(system2.envVariable("CI_API_V4_URL")).ifPresent( - v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL, v)); + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); Optional.ofNullable(system2.envVariable("CI_PROJECT_PATH")).ifPresent(v -> sensorContext .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, v)); } else { - Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL)).ifPresent( - v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)) .ifPresent(v -> sensorContext .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index e42860a21..02ae292f7 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -83,8 +83,8 @@ public void decorateQualityGateStatus() { when(almSettingDto.getPersonalAccessToken()).thenReturn("token"); AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_URL))) - .thenReturn(Optional.of(wireMockRule.baseUrl())); + when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL))) + .thenReturn(Optional.of(wireMockRule.baseUrl()+"/api/v4")); when(analysisDetails .getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG))) .thenReturn(Optional.of(repositorySlug)); From 3dfad7de8f483f23c908b743a8d1c375e98e190e Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Tue, 11 Feb 2020 17:21:39 +0100 Subject: [PATCH 03/26] [Gitlab] reenable deletion of comments --- .../GitlabServerPullRequestDecorator.java | 47 ++++++++- .../gitlab/response/Discussion.java | 44 +++++++++ .../ce/pullrequest/gitlab/response/Note.java | 49 ++++++++++ .../ce/pullrequest/gitlab/response/User.java | 35 +++++++ .../GitlabServerPullRequestDecoratorTest.java | 98 ++++++++----------- 5 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index cca39265d..3948c28c2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -25,7 +25,10 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Commit; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Discussion; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.MergeRequest; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Note; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.User; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; import org.apache.commons.io.IOUtils; import org.apache.http.Header; @@ -33,6 +36,7 @@ import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; @@ -105,6 +109,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al final String pullRequestId = analysis.getBranchName(); final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(repositorySlug, StandardCharsets.UTF_8.name())); + final String userURL = apiURL + "/user"; final String statusUrl = projectURL + String.format("/statuses/%s", revision); final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); final String prCommitsURL = mergeRequestURl + "/commits"; @@ -113,15 +118,39 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al LOGGER.info(String.format("Status url is: %s ", statusUrl)); LOGGER.info(String.format("PR commits url is: %s ", prCommitsURL)); LOGGER.info(String.format("MR discussion url is: %s ", mergeRequestDiscussionURL)); + LOGGER.info(String.format("User url is: %s ", userURL)); Map headers = new HashMap<>(); headers.put("PRIVATE-TOKEN", apiToken); headers.put("Accept", "application/json"); + User user = getSingle(userURL, headers, User.class); + LOGGER.info(String.format("Using user: %s ", user.getUsername())); + List commits = getPagedList(prCommitsURL, headers, new TypeReference>() { }).stream().map(Commit::getId).collect(Collectors.toList()); MergeRequest mergeRequest = getSingle(mergeRequestURl, headers, MergeRequest.class); + List discussions = getPagedList(mergeRequestDiscussionURL, headers, new TypeReference>() { + }); + + LOGGER.info(String.format("Discussions in MR: %s ", discussions + .stream() + .map(Discussion::getId) + .collect(Collectors.joining(", ")))); + + for (Discussion discussion : discussions) { + for (Note note : discussion.getNotes()) { + if (!note.isSystem() && note.getAuthor() != null && note.getAuthor().getUsername().equals(user.getUsername())) { + //delete only our own comments + deleteCommitDiscussionNote(mergeRequestDiscussionURL + String.format("/%s/notes/%s", + discussion.getId(), + note.getId()), + headers); + } + } + } + QualityGate.Condition newCoverageCondition = analysis.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) .orElseThrow(() -> new IllegalStateException("Could not find New Coverage Condition in analysis")); String coverageValue = newCoverageCondition.getStatus().equals(QualityGate.EvaluationStatus.NO_VALUE) ? "0" : newCoverageCondition.getValue(); @@ -239,8 +268,22 @@ private List getPagedList(String commitDiscussionURL, Map return discussions; } - private void postCommitComment(String commitCommentUrl, Map headers, List params) - throws IOException { + private void deleteCommitDiscussionNote(String commitDiscussionNoteURL, Map headers) throws IOException { + //https://docs.gitlab.com/ee/api/discussions.html#delete-a-commit-thread-note + HttpDelete httpDelete = new HttpDelete(commitDiscussionNoteURL); + for (Map.Entry entry : headers.entrySet()) { + httpDelete.addHeader(entry.getKey(), entry.getValue()); + } + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + LOGGER.info("Deleting {} with headers {}", commitDiscussionNoteURL, headers); + + HttpResponse httpResponse = httpClient.execute(httpDelete); + validateGitlabResponse(httpResponse, 204, "Commit discussions note deleted"); + } + } + + private void postCommitComment(String commitCommentUrl, Map headers, List params) throws IOException { //https://docs.gitlab.com/ee/api/commits.html#post-comment-to-commit HttpPost httpPost = new HttpPost(commitCommentUrl); for (Map.Entry entry : headers.entrySet()) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java new file mode 100644 index 000000000..17b487f1e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Discussion.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 Markus Heberling + * + * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Discussion { + private final String id; + + private final List notes; + + @JsonCreator + public Discussion(@JsonProperty("id") String id, @JsonProperty("notes") List notes) { + this.id = id; + this.notes = notes; + } + + public String getId() { + return id; + } + + public List getNotes() { + return notes; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java new file mode 100644 index 000000000..edb22c9f9 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/Note.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 Markus Heberling + * + * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Note { + private final long id; + + private final boolean system; + + private final User author; + + @JsonCreator + public Note(@JsonProperty("id") long id, @JsonProperty("system") boolean system, @JsonProperty("author") User author) { + this.id = id; + this.system = system; + this.author = author; + } + + public long getId() { + return id; + } + + public boolean isSystem() { + return system; + } + + public User getAuthor() { + return author; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java new file mode 100644 index 000000000..c96d0ebf9 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/response/User.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 Markus Heberling + * + * 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class User { + private final String username; + + @JsonCreator + public User(@JsonProperty("username") String username) { + this.username = username; + } + + public String getUsername() { + return username; + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index 02ae292f7..d6131f954 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -112,78 +112,60 @@ public void decorateQualityGateStatus() { when(scmInfo.getChangesetForLine(anyInt())).thenReturn(Changeset.newChangesetBuilder().setDate(0L).setRevision(commitSHA).build()); when(scmInfoRepository.getScmInfo(component)).thenReturn(Optional.of(scmInfo)); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/user")).withHeader("PRIVATE-TOKEN", equalTo("token")).willReturn(okJson("{\n" + - " \"id\": 1,\n" + - " \"username\": \"" + - user + - "\"}"))); + " \"id\": 1,\n" + + " \"username\": \"" + user + "\"}"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName)).willReturn(okJson("{\n" + - " \"id\": 15235,\n" + - " \"iid\": " + - branchName + - ",\n" + - " \"diff_refs\": {\n" + - " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + - " \"head_sha\":\"" + - commitSHA + - "\",\n" + - " \"start_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\"}\n" + - "}"))); + " \"id\": 15235,\n" + + " \"iid\": " + branchName + ",\n" + + " \"diff_refs\": {\n" + + " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + + " \"head_sha\":\"" + commitSHA + "\",\n" + + " \"start_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\"}\n" + + "}"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/commits")).willReturn(okJson("[\n" + - " {\n" + - " \"id\": \"" + - commitSHA + - "\"\n" + - " }]"))); + " {\n" + + " \"id\": \"" + commitSHA + "\"\n" + + " }]"))); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")).willReturn(okJson("[\n" + - " {\n" + - " \"id\": \"" + - discussionId + - "\",\n" + - " \"individual_note\": false,\n" + - " \"notes\": [\n" + - " {\n" + - " \"id\": " + - noteId + - ",\n" + - " \"type\": \"DiscussionNote\",\n" + - " \"body\": \"discussion text\",\n" + - " \"attachment\": null,\n" + - " \"author\": {\n" + - " \"id\": 1,\n" + - " \"username\": \"" + - user + - "\"\n" + - " }}]}]"))); + " {\n" + + " \"id\": \"" + discussionId + "\",\n" + + " \"individual_note\": false,\n" + + " \"notes\": [\n" + + " {\n" + + " \"id\": " + noteId + ",\n" + + " \"type\": \"DiscussionNote\",\n" + + " \"body\": \"discussion text\",\n" + + " \"attachment\": null,\n" + + " \"author\": {\n" + + " \"id\": 1,\n" + + " \"username\": \"" + user + "\"\n" + + " }}]}]"))); wireMockRule.stubFor(delete(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions/" + discussionId + "/notes/" + noteId)).willReturn(noContent())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/statuses/" + commitSHA)) - .withQueryParam("name", equalTo("SonarQube")) - .withQueryParam("state", equalTo("failed")).withQueryParam("target_url", - equalTo(sonarRootUrl + - "/dashboard?id=" + - projectKey + - "&pullRequest=" + - branchName)) - .withQueryParam("coverage", equalTo(coverage.getValue())).willReturn(created())); + .withQueryParam("name", equalTo("SonarQube")) + .withQueryParam("state", equalTo("failed")) + .withQueryParam("target_url", equalTo(sonarRootUrl + "/dashboard?id=" + projectKey + "&pullRequest=" + branchName)) + .withQueryParam("coverage", equalTo(coverage.getValue())) + .willReturn(created())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) - .withRequestBody(equalTo("body=summary")).willReturn(created())); + .withRequestBody(equalTo("body=summary")) + .willReturn(created())); wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) - .withRequestBody(equalTo("body=issue&" + urlEncode("position[base_sha]") + - "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + - urlEncode("position[start_sha]") + - "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + - urlEncode("position[head_sha]") + "=" + commitSHA + "&" + - urlEncode("position[new_path]") + "=" + - urlEncode(filePath) + "&" + - urlEncode("position[new_line]") + "=" + lineNumber + "&" + - urlEncode("position[position_type]") + "=text")) - .willReturn(created())); + .withRequestBody(equalTo("body=issue&" + + urlEncode("position[base_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[start_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[head_sha]") + "=" + commitSHA + "&" + + urlEncode("position[new_path]") + "=" + urlEncode(filePath) + "&" + + urlEncode("position[new_line]") + "=" + lineNumber + "&" + + urlEncode("position[position_type]") + "=text")) + .willReturn(created())); Server server = mock(Server.class); when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); From 5776c975d272a16297ff6918e14a6f4f7ea4b5b6 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 12 Feb 2020 07:54:18 +0100 Subject: [PATCH 04/26] [GITLAB] allow overrding of autodetected gitlab ci values --- .../scanner/ScannerPullRequestPropertySensor.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java index 6b1880977..8b0ac3a0b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java @@ -47,14 +47,14 @@ public void execute(SensorContext sensorContext) { v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); Optional.ofNullable(system2.envVariable("CI_PROJECT_PATH")).ifPresent(v -> sensorContext .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, v)); - } else { - Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL)).ifPresent( - v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); - Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)) - .ifPresent(v -> sensorContext - .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, - v)); } + + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)) + .ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, + v)); } } From edb6e126a43fad1eb16b9da1f2da72f64d1cb735 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 12 Feb 2020 08:46:31 +0100 Subject: [PATCH 05/26] [GITLAB] auto config branch/MR --- .../CommunityBranchConfigurationLoader.java | 33 +++++++++++++++++ ...ommunityBranchConfigurationLoaderTest.java | 35 ++++++++++--------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java index 63aa23b25..10cca9e61 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java @@ -20,6 +20,7 @@ import org.apache.commons.lang.StringUtils; import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.System2; import org.sonar.core.config.ScannerProperties; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchConfigurationLoader; @@ -30,8 +31,11 @@ import org.sonar.scanner.scan.branch.ProjectPullRequests; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -45,10 +49,18 @@ public class CommunityBranchConfigurationLoader implements BranchConfigurationLo private static final Set PULL_REQUEST_ANALYSIS_PARAMETERS = new HashSet<>( Arrays.asList(ScannerProperties.PULL_REQUEST_BRANCH, ScannerProperties.PULL_REQUEST_KEY, ScannerProperties.PULL_REQUEST_BASE)); + private final System2 system2; + + public CommunityBranchConfigurationLoader(System2 system2) { + this.system2 = system2; + } + @Override public BranchConfiguration load(Map localSettings, ProjectBranches projectBranches, ProjectPullRequests pullRequests) { + localSettings = autoConfigure(localSettings); + if (projectBranches.isEmpty()) { if (isTargetingDefaultBranch(localSettings)) { return new DefaultBranchConfiguration(); @@ -73,6 +85,27 @@ public BranchConfiguration load(Map localSettings, ProjectBranch return new DefaultBranchConfiguration(); } + private Map autoConfigure(Map localSettings) { + Map mutableLocalSettings=new HashMap<>(localSettings); + if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { + //GitLab CI auto configuration + if (system2.envVariable("CI_MERGE_REQUEST_IID") != null) { + // we are inside a merge request + Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_IID")).ifPresent( + v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_KEY, v)); + Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")).ifPresent( + v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BRANCH, v)); + Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")).ifPresent( + v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BASE, v)); + } else { + // branch or tag + Optional.ofNullable(system2.envVariable("CI_COMMIT_REF_NAME")).ifPresent( + v -> mutableLocalSettings.putIfAbsent(ScannerProperties.BRANCH_NAME, v)); + } + } + return Collections.unmodifiableMap(mutableLocalSettings); + } + private static boolean isTargetingDefaultBranch(Map localSettings) { String name = StringUtils.trimToNull(localSettings.get(ScannerProperties.BRANCH_NAME)); String target = StringUtils.trimToNull(localSettings.get(ScannerProperties.BRANCH_TARGET)); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java index 05e37da68..b237b29fa 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.System2; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchInfo; import org.sonar.scanner.scan.branch.BranchType; @@ -55,7 +56,7 @@ public ExpectedException expectedException() { @Test public void testExceptionWhenNoExistingBranchAndBranchParamsPresent() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -71,7 +72,7 @@ public void testExceptionWhenNoExistingBranchAndBranchParamsPresent() { @Test public void testDefaultConfigWhenNoExistingBranchAndBranchNameParamMaster() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -84,7 +85,7 @@ public void testDefaultConfigWhenNoExistingBranchAndBranchNameParamMaster() { @Test public void testErrorWhenNoExistingBranchAndBranchTargetMasterButNoSourceBranch() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -103,7 +104,7 @@ public void testErrorWhenNoExistingBranchAndBranchTargetMasterButNoSourceBranch( @Test public void testDefaultConfigWhenNoExistingBranchAndBranchParamsAllMaster() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -117,7 +118,7 @@ public void testDefaultConfigWhenNoExistingBranchAndBranchParamsAllMaster() { @Test public void testExceptionWhenNoExistingBranchAndPullRequestAndBranchParametersPresent() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -136,7 +137,7 @@ public void testExceptionWhenNoExistingBranchAndPullRequestAndBranchParametersPr @Test public void testDefaultBranchInfoWhenNoBranchParametersSpecifiedAndNoBranchesExist() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); ProjectBranches branchInfo = mock(ProjectBranches.class); when(branchInfo.isEmpty()).thenReturn(true); @@ -151,14 +152,14 @@ public void testDefaultBranchInfoWhenNoBranchParametersSpecifiedAndNoBranchesExi @Test public void testDefaultBranchInfoWhenNoParametersSpecified() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); assertEquals(DefaultBranchConfiguration.class, testCase.load(new HashMap<>(), mock(ProjectBranches.class), mock(ProjectPullRequests.class)).getClass()); } @Test public void testValidBranchInfoWhenAllBranchParametersSpecified() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "feature/shortLivedFeatureBranch"); parameters.put("sonar.branch.target", "master"); @@ -186,7 +187,7 @@ public void testValidBranchInfoWhenAllBranchParametersSpecified() { @Test public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "feature/shortLivedBranch"); @@ -208,7 +209,7 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists() { @Test public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists2() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "feature/shortLivedBranch"); parameters.put("sonar.branch.target", ""); @@ -231,7 +232,7 @@ public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists2() { @Test public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExists() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "feature/shortLivedBranch"); @@ -250,7 +251,7 @@ public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExists() { @Test public void testUnknownTargetBranch() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "feature/shortLivedBranch"); parameters.put("sonar.branch.target", "feature/otherShortLivedBranch"); @@ -272,7 +273,7 @@ public boolean matches(Object item) { @Test public void testExistingBranchOnlySourceParameters() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.branch.name", "longLivedBranch"); @@ -294,7 +295,7 @@ public void testExistingBranchOnlySourceParameters() { @Test public void testPullRequestAllParameters() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); parameters.put("sonar.pullrequest.base", "target"); @@ -320,7 +321,7 @@ public void testPullRequestAllParameters() { @Test public void testPullRequestMandatoryParameters() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); parameters.put("sonar.pullrequest.key", "pr-key"); @@ -344,7 +345,7 @@ public void testPullRequestMandatoryParameters() { @Test public void testPullRequestMandatoryParameters2() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); parameters.put("sonar.pullrequest.key", "pr-key"); @@ -370,7 +371,7 @@ public void testPullRequestMandatoryParameters2() { @Test public void testPullRequestNoSuchTarget() { - CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(); + CommunityBranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(System2.INSTANCE); Map parameters = new HashMap<>(); parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); parameters.put("sonar.pullrequest.base", "missingTarget"); From 4090510a17fdd550b9d975371954a4e8621a9cb6 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 12 Feb 2020 09:43:21 +0100 Subject: [PATCH 06/26] [GITLAB] use same property names as sonar developer edition --- .../GitlabServerPullRequestDecorator.java | 20 ++++++++++--------- .../ScannerPullRequestPropertySensor.java | 20 ++++++++++--------- .../GitlabServerPullRequestDecoratorTest.java | 4 ++-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 3948c28c2..6e57c9772 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -71,10 +71,12 @@ public class GitlabServerPullRequestDecorator implements PullRequestBuildStatusDecorator { - public static final String PULLREQUEST_GITLAB_API_URL = - "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.api.url"; - public static final String PULLREQUEST_GITLAB_REPOSITORY_SLUG = - "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.repositorySlug"; + public static final String PULLREQUEST_GITLAB_INSTANCE_URL = + "sonar.pullrequest.gitlab.instanceUrl"; + public static final String PULLREQUEST_GITLAB_PROJECT_ID = + "sonar.pullrequest.gitlab.projectId"; + public static final String PULLREQUEST_GITLAB_PROJECT_URL = + "sonar.pullrequest.gitlab.projectUrl"; private static final Logger LOGGER = Loggers.get(GitlabServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = @@ -97,18 +99,18 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al String revision = analysis.getCommitSha(); try { - final String apiURL = analysis.getScannerProperty(PULLREQUEST_GITLAB_API_URL).orElseThrow( + final String apiURL = analysis.getScannerProperty(PULLREQUEST_GITLAB_INSTANCE_URL).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate Gitlab merge request. '%s' has not been set in scanner properties", - PULLREQUEST_GITLAB_API_URL))); + PULLREQUEST_GITLAB_INSTANCE_URL))); final String apiToken = almSettingDto.getPersonalAccessToken(); - final String repositorySlug = analysis.getScannerProperty(PULLREQUEST_GITLAB_REPOSITORY_SLUG).orElseThrow( + final String projectId = analysis.getScannerProperty(PULLREQUEST_GITLAB_PROJECT_ID).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate Gitlab merge request. '%s' has not been set in scanner properties", - PULLREQUEST_GITLAB_REPOSITORY_SLUG))); + PULLREQUEST_GITLAB_PROJECT_ID))); final String pullRequestId = analysis.getBranchName(); - final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(repositorySlug, StandardCharsets.UTF_8.name())); + final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(projectId, StandardCharsets.UTF_8.name())); final String userURL = apiURL + "/user"; final String statusUrl = projectURL + String.format("/statuses/%s", revision); final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java index 8b0ac3a0b..5a101a64b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java @@ -43,18 +43,20 @@ public void describe(SensorDescriptor sensorDescriptor) { @Override public void execute(SensorContext sensorContext) { if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { - Optional.ofNullable(system2.envVariable("CI_API_V4_URL")).ifPresent( - v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); + Optional.ofNullable(system2.envVariable("CI_API_V4_URL")).ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL, v)); Optional.ofNullable(system2.envVariable("CI_PROJECT_PATH")).ifPresent(v -> sensorContext - .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, v)); + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID, v)); + Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_PROJECT_URL")).ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL, v)); } - Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL)).ifPresent( - v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL, v)); - Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG)) - .ifPresent(v -> sensorContext - .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG, - v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL, v)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index d6131f954..039c22c9c 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -83,10 +83,10 @@ public void decorateQualityGateStatus() { when(almSettingDto.getPersonalAccessToken()).thenReturn("token"); AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_API_URL))) + when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL))) .thenReturn(Optional.of(wireMockRule.baseUrl()+"/api/v4")); when(analysisDetails - .getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_REPOSITORY_SLUG))) + .getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID))) .thenReturn(Optional.of(repositorySlug)); when(analysisDetails.getAnalysisProjectKey()).thenReturn(projectKey); when(analysisDetails.getBranchName()).thenReturn(branchName); From 2d858358b9bc0bd2d695b09f93757e8d3da16d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uwe=20Ja=CC=88ger?= Date: Tue, 14 Jan 2020 10:42:27 +0100 Subject: [PATCH 07/26] Fix adding discussions for gitlab --- .../pullrequest/gitlab/GitlabServerPullRequestDecorator.java | 1 + .../gitlab/GitlabServerPullRequestDecoratorTest.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 6e57c9772..2eadd60ac 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -185,6 +185,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al new BasicNameValuePair("position[base_sha]", mergeRequest.getDiffRefs().getBaseSha()), new BasicNameValuePair("position[start_sha]", mergeRequest.getDiffRefs().getStartSha()), new BasicNameValuePair("position[head_sha]", mergeRequest.getDiffRefs().getHeadSha()), + new BasicNameValuePair("position[old_path]", path), new BasicNameValuePair("position[new_path]", path), new BasicNameValuePair("position[new_line]", String.valueOf(issue.getIssue().getLine())), new BasicNameValuePair("position[position_type]", "text")); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index 039c22c9c..bff354d02 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -162,6 +162,7 @@ public void decorateQualityGateStatus() { urlEncode("position[base_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + urlEncode("position[start_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + urlEncode("position[head_sha]") + "=" + commitSHA + "&" + + urlEncode("position[old_path]") + "=" + urlEncode(filePath) + "&" + urlEncode("position[new_path]") + "=" + urlEncode(filePath) + "&" + urlEncode("position[new_line]") + "=" + lineNumber + "&" + urlEncode("position[position_type]") + "=text")) @@ -183,4 +184,4 @@ private static String urlEncode(String value) { throw new Error("No support for UTF-8!", e); } } -} \ No newline at end of file +} From 9e9c59ca9dd3c7faec5d5e44e7f42c78268d1b4f Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 12 Feb 2020 10:03:18 +0100 Subject: [PATCH 08/26] [GITLAB] don't fail, if no new coverage condition is provided Fixes #85 --- .../gitlab/GitlabServerPullRequestDecorator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 2eadd60ac..84d1d4313 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -153,10 +153,10 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al } } - QualityGate.Condition newCoverageCondition = analysis.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) - .orElseThrow(() -> new IllegalStateException("Could not find New Coverage Condition in analysis")); - String coverageValue = newCoverageCondition.getStatus().equals(QualityGate.EvaluationStatus.NO_VALUE) ? "0" : newCoverageCondition.getValue(); - + String coverageValue = analysis.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) + .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) + .map(QualityGate.Condition::getValue) + .orElse("0"); List openIssues = analysis.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())).collect(Collectors.toList()); From cae91314f9d5bf338751d83e6213aa274d448298 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 12 Feb 2020 10:30:57 +0100 Subject: [PATCH 09/26] [GITLAB] support multiple pipelines on a single commit --- .../GitlabServerPullRequestDecorator.java | 22 ++++++++++--------- .../ScannerPullRequestPropertySensor.java | 4 ++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 84d1d4313..29a51a6db 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -77,6 +77,8 @@ public class GitlabServerPullRequestDecorator implements PullRequestBuildStatusD "sonar.pullrequest.gitlab.projectId"; public static final String PULLREQUEST_GITLAB_PROJECT_URL = "sonar.pullrequest.gitlab.projectUrl"; + public static final String PULLREQUEST_GITLAB_PIPELINE_ID = + "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.pipelineId"; private static final Logger LOGGER = Loggers.get(GitlabServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = @@ -163,7 +165,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto al String summaryComment = analysis.createAnalysisSummary(new MarkdownFormatterFactory()); List summaryContentParams = Collections.singletonList(new BasicNameValuePair("body", summaryComment)); - postStatus(statusUrl, headers, analysis, coverageValue); + postStatus(new StringBuilder(statusUrl), headers, analysis, coverageValue); postCommitComment(mergeRequestDiscussionURL, headers, summaryContentParams); @@ -302,22 +304,22 @@ private void postCommitComment(String commitCommentUrl, Map head } } - private void postStatus(String statusPostUrl, Map headers, AnalysisDetails analysis, + private void postStatus(StringBuilder statusPostUrl, Map headers, AnalysisDetails analysis, String coverage) throws IOException { //See https://docs.gitlab.com/ee/api/commits.html#post-the-build-status-to-a-commit - statusPostUrl += "?name=SonarQube"; + statusPostUrl.append("?name=SonarQube"); String status = (analysis.getQualityGateStatus() == QualityGate.Status.OK ? "success" : "failed"); - statusPostUrl += "&state=" + status; - statusPostUrl += "&target_url=" + URLEncoder.encode(String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), + statusPostUrl.append("&state=").append(status); + statusPostUrl.append("&target_url=").append(URLEncoder.encode(String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), URLEncoder.encode(analysis.getAnalysisProjectKey(), StandardCharsets.UTF_8.name()), URLEncoder .encode(analysis.getBranchName(), - StandardCharsets.UTF_8.name())), StandardCharsets.UTF_8.name()); - statusPostUrl+="&description="+URLEncoder.encode("SonarQube Status", StandardCharsets.UTF_8.name()); - statusPostUrl+="&coverage="+coverage; - //TODO: add pipelineId if we have it + StandardCharsets.UTF_8.name())), StandardCharsets.UTF_8.name())); + statusPostUrl.append("&description=").append(URLEncoder.encode("SonarQube Status", StandardCharsets.UTF_8.name())); + statusPostUrl.append("&coverage=").append(coverage); + analysis.getScannerProperty(PULLREQUEST_GITLAB_PIPELINE_ID).ifPresent(pipelineId -> statusPostUrl.append("&pipeline_id=").append(pipelineId)); - HttpPost httpPost = new HttpPost(statusPostUrl); + HttpPost httpPost = new HttpPost(statusPostUrl.toString()); for (Map.Entry entry : headers.entrySet()) { httpPost.addHeader(entry.getKey(), entry.getValue()); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java index 5a101a64b..b17bb71b1 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerPullRequestPropertySensor.java @@ -49,6 +49,8 @@ public void execute(SensorContext sensorContext) { .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID, v)); Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_PROJECT_URL")).ifPresent(v -> sensorContext .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL, v)); + Optional.ofNullable(system2.envVariable("CI_PIPELINE_ID")).ifPresent(v -> sensorContext + .addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PIPELINE_ID, v)); } Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL)).ifPresent( @@ -57,6 +59,8 @@ public void execute(SensorContext sensorContext) { v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID, v)); Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL)).ifPresent( v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_URL, v)); + Optional.ofNullable(system2.property(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PIPELINE_ID)).ifPresent( + v -> sensorContext.addContextProperty(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PIPELINE_ID, v)); } } From 87fc1b537a223c38e6297369a10d731587c24749 Mon Sep 17 00:00:00 2001 From: Natan Deitch Date: Thu, 20 Feb 2020 05:39:32 -0300 Subject: [PATCH 10/26] #90: Use correct field to retrieve Github repository name The Github ALM Binding Web Service uses the `AlmRepo` field to store the repository name, but the Github decorator was using `AlmSlug` to try and retrieve the repository name, so was getting a `null` value back and failing to find a matching repository. Switching to using `AlmRepo` in the decorator overcomes this issues. --- .../ce/pullrequest/github/v4/GraphqlCheckRunProvider.java | 2 +- .../pullrequest/github/v4/GraphqlCheckRunProviderTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java index 6918c3ba5..ac400d407 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java @@ -89,7 +89,7 @@ public void createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSet ProjectAlmSettingDto projectAlmSettingDto) throws IOException, GeneralSecurityException { String apiUrl = almSettingDto.getUrl(); String apiPrivateKey = almSettingDto.getPrivateKey(); - String projectPath = projectAlmSettingDto.getAlmSlug(); + String projectPath = projectAlmSettingDto.getAlmRepo(); String appId = almSettingDto.getAppId(); RepositoryAuthenticationToken repositoryAuthenticationToken = diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java index e4edd6d2c..5c2196490 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java @@ -110,7 +110,7 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException, General when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); AlmSettingDto almSettingDto = mock(AlmSettingDto.class); when(almSettingDto.getUrl()).thenReturn("http://host.name"); when(almSettingDto.getAppId()).thenReturn("app id"); @@ -335,7 +335,7 @@ private void createCheckRunHappyPath(QualityGate.Status status) throws IOExcepti when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); AlmSettingDto almSettingDto = mock(AlmSettingDto.class); when(almSettingDto.getUrl()).thenReturn("http://host.name"); when(almSettingDto.getAppId()).thenReturn("app id"); @@ -437,4 +437,4 @@ public void checkCorrectDefaultValuesInjected() { .isEqualTo(new GraphqlCheckRunProvider(new DefaultGraphqlProvider(), clock, githubApplicationAuthenticationProvider, server)); } -} \ No newline at end of file +} From 5d4b432d733e1c7189dd388ee7926ebc480ca3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=B2=D1=81=D0=BA=D0=B8=D0=B9?= Date: Sun, 1 Mar 2020 17:56:25 +0300 Subject: [PATCH 11/26] iloer@mail.ru Initial commit with test --- ...munityReportAnalysisComponentProvider.java | 4 +- ...AzureDevOpsServerPullRequestDecorator.java | 81 +++++++++++++++++++ ...tyReportAnalysisComponentProviderTest.java | 2 +- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java index 104bf8b54..89632b696 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java @@ -20,6 +20,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestPostAnalysisTask; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.AzureDevOpsServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.RestApplicationAuthenticationProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4.GraphqlCheckRunProvider; @@ -40,7 +41,8 @@ public List getComponents() { return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class, PostAnalysisIssueVisitor.class, GithubPullRequestDecorator.class, GraphqlCheckRunProvider.class, RestApplicationAuthenticationProvider.class, - BitbucketServerPullRequestDecorator.class, GitlabServerPullRequestDecorator.class); + BitbucketServerPullRequestDecorator.class, GitlabServerPullRequestDecorator.class, + AzureDevOpsServerPullRequestDecorator.class); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java new file mode 100644 index 000000000..a1f887b11 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -0,0 +1,81 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import org.sonar.api.issue.Issue; +import org.sonar.api.platform.Server; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { + + private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); + private static final List OPEN_ISSUE_STATUSES = + Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) + .collect(Collectors.toList()); + + public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.azuredevops.instanceUrl"; + public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.azuredevops.projectId"; + public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_URL = "sonar.pullrequest.azuredevops.projectUrl"; + public static final String PULLREQUEST_AZUREDEVOPS_BUILD_ID = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.azuredevops.buildId"; + + private final Server server; + private final ScmInfoRepository scmInfoRepository; + + public AzureDevOpsServerPullRequestDecorator(Server server, ScmInfoRepository scmInfoRepository) { + super(); + this.server = server; + this.scmInfoRepository = scmInfoRepository; + } + + @Override + public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + LOGGER.info("starting to analyze with " + analysisDetails.toString()); + String revision = analysisDetails.getCommitSha(); + + try { + final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse(""); + final String apiToken = almSettingDto.getPersonalAccessToken(); + final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse(""); + final String pullRequestId = analysisDetails.getBranchName(); + + final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(projectId, StandardCharsets.UTF_8.name())); + final String userURL = apiURL + "/user"; + final String statusUrl = projectURL + String.format("/statuses/%s", revision); + final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); + final String prCommitsURL = mergeRequestURl + "/commits"; + final String mergeRequestDiscussionURL = mergeRequestURl + "/discussions"; + + + + + LOGGER.info(String.format("Status url is: %s ", statusUrl)); + LOGGER.info(String.format("PR commits url is: %s ", prCommitsURL)); + LOGGER.info(String.format("MR discussion url is: %s ", mergeRequestDiscussionURL)); + LOGGER.info(String.format("User url is: %s ", userURL)); + } + catch (IOException ex){ + throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); + } + } + + @Override + public String name() { + return "AzureDevOpsServer"; + } + + @Override + public ALM alm() { + return ALM.AZURE_DEVOPS; + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java index 3e2802bf8..5737399a3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java @@ -32,7 +32,7 @@ public class CommunityReportAnalysisComponentProviderTest { @Test public void testGetComponents() { List result = new CommunityReportAnalysisComponentProvider().getComponents(); - assertEquals(8, result.size()); + assertEquals(9, result.size()); assertEquals(CommunityBranchLoaderDelegate.class, result.get(0)); } } From a93110a64f6f3aa75efa639bdd5f9f3ab34d22ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=B2=D1=81=D0=BA=D0=B8=D0=B9?= Date: Wed, 4 Mar 2020 00:58:00 +0300 Subject: [PATCH 12/26] iloer@mail.ru add getContextProperty method --- .../PullRequestPostAnalysisTask.java | 47 ++++++++++- ...AzureDevOpsServerPullRequestDecorator.java | 82 +++++++++++++------ 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index b559adf50..c63f2a1bb 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -18,6 +18,7 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.ce.posttask.Analysis; import org.sonar.api.ce.posttask.Branch; import org.sonar.api.ce.posttask.PostProjectAnalysisTask; @@ -36,8 +37,12 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask { @@ -77,6 +82,11 @@ public String getDescription() { @Override public void finished(Context context) { ProjectAnalysis projectAnalysis = context.getProjectAnalysis(); + + ScannerContext scannerContext = projectAnalysis.getScannerContext().getProperties().size() > 0 + ? projectAnalysis.getScannerContext() + : GetScannerContext(projectAnalysis.getCeTask().getId()); + LOGGER.debug("found " + pullRequestDecorators.size() + " pull request decorators"); Optional optionalPullRequest = projectAnalysis.getBranch().filter(branch -> Branch.Type.PULL_REQUEST == branch.getType()); @@ -108,7 +118,6 @@ public void finished(Context context) { projectAlmSettingDto = optionalProjectAlmSettingDto.get(); String almSettingUuid = projectAlmSettingDto.getAlmSettingUuid(); optionalAlmSettingDto = dbClient.almSettingDao().selectByUuid(dbSession, almSettingUuid); - } if (!optionalAlmSettingDto.isPresent()) { @@ -154,13 +163,47 @@ public void finished(Context context) { new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder), analysis, projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), - projectAnalysis.getScannerContext()); + scannerContext); PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get(); LOGGER.info("using pull request decorator" + pullRequestDecorator.name()); pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } + private ScannerContext GetScannerContext(String ceTaskId) + { + ScannerContext scannerContext = new ScannerContext() { + private final Map props = new HashMap(); + @Override + public Map getProperties() { return this.props; } + }; + + try (DbSession dbSession = dbClient.openSession(false)) { + + try { + String scannerContextStr = dbClient.ceScannerContextDao().selectScannerContext(dbSession, ceTaskId).orElse(""); + + LOGGER.trace("GetScannerContext: ScannerContextStr =" + scannerContextStr); + + Pattern p = Pattern.compile("(\\w.+)=(.+)"); + Matcher m = p.matcher(scannerContextStr); + HashMap map = new HashMap(); + + while (m.find()) { + map.put(m.group(1), m.group(2)); + } + LOGGER.trace("GetScannerContext: map =" + map); + + scannerContext.getProperties().putAll(map); + + } catch (Exception ex) + { + LOGGER.error("AZURE: ***ERROR***:" + ex); + } + return scannerContext; + } + } + private static Optional findCurrentPullRequestStatusDecorator( AlmSettingDto almSetting, List pullRequestDecorators) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index a1f887b11..1f5de1aa9 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -11,9 +11,6 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -24,10 +21,12 @@ public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildSt Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList()); - public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.azuredevops.instanceUrl"; - public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.azuredevops.projectId"; - public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_URL = "sonar.pullrequest.azuredevops.projectUrl"; - public static final String PULLREQUEST_AZUREDEVOPS_BUILD_ID = "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.azuredevops.buildId"; + public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; + public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; + public static final String PULLREQUEST_AZUREDEVOPS_BRANCH = "sonar.pullrequest.branch"; + public static final String PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID = "sonar.pullrequest.key"; + public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; + public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository"; private final Server server; private final ScmInfoRepository scmInfoRepository; @@ -44,34 +43,65 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin String revision = analysisDetails.getCommitSha(); try { - final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse(""); + /*final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))); + final String baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_BASE_BRANCH))); + final String branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_BRANCH))); + final String pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))); + final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_PROJECT_ID))); + final String repositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME)));*/ + + //Configuration configuration = configurationRepository.getConfiguration(); + + //LOGGER.info(String.format("AZURE: ***prop count*** : %s ", analysisDetails.getScannerContextProp().size() )); + //analysisDetails.getScannerContextProp().forEach((sn, ss) -> LOGGER.info(String.format("AZURE: ***%s*** : %s ", sn, ss))); + + final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("Not found: PULLREQUEST_AZUREDEVOPS_INSTANCE_URL"); + final String baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElse("Not found: PULLREQUEST_AZUREDEVOPS_BASE_BRANCH"); + final String branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElse("Not found: PULLREQUEST_AZUREDEVOPS_BRANCH"); + + final String pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID"); + final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); + final String repositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("Not found: PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME"); final String apiToken = almSettingDto.getPersonalAccessToken(); - final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse(""); - final String pullRequestId = analysisDetails.getBranchName(); + final String sonarBranch = analysisDetails.getBranchName(); + + LOGGER.info(String.format("AZURE: apiURL is: %s ", apiToken)); + LOGGER.info(String.format("AZURE: apiURL is: %s ", apiURL)); + LOGGER.info(String.format("AZURE: baseBranch is: %s ", baseBranch)); + LOGGER.info(String.format("AZURE: branch is: %s ", branch)); + LOGGER.info(String.format("AZURE: pullRequestId is: %s ", pullRequestId)); + LOGGER.info(String.format("AZURE: projectId is: %s ", projectId)); + LOGGER.info(String.format("AZURE: repositoryName is: %s ", repositoryName)); + LOGGER.info(String.format("AZURE: revision Commit/revision is: %s ", revision)); + LOGGER.info(String.format("AZURE: sonarBranch is: %s ", sonarBranch)); - final String projectURL = apiURL + String.format("/projects/%s", URLEncoder.encode(projectId, StandardCharsets.UTF_8.name())); - final String userURL = apiURL + "/user"; - final String statusUrl = projectURL + String.format("/statuses/%s", revision); - final String mergeRequestURl = projectURL + String.format("/merge_requests/%s", pullRequestId); - final String prCommitsURL = mergeRequestURl + "/commits"; - final String mergeRequestDiscussionURL = mergeRequestURl + "/discussions"; - - - - - LOGGER.info(String.format("Status url is: %s ", statusUrl)); - LOGGER.info(String.format("PR commits url is: %s ", prCommitsURL)); - LOGGER.info(String.format("MR discussion url is: %s ", mergeRequestDiscussionURL)); - LOGGER.info(String.format("User url is: %s ", userURL)); } - catch (IOException ex){ + catch (IllegalStateException ex){ throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); } } @Override public String name() { - return "AzureDevOpsServer"; + return "Azure"; } @Override From 5f327b099bf95a3c2166e67350993dc9c11b4807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=B2=D1=81=D0=BA=D0=B8=D0=B9?= Date: Wed, 4 Mar 2020 02:11:16 +0300 Subject: [PATCH 13/26] iloer@mail.ru fix test --- .../PullRequestPostAnalysisTask.java | 11 +++++------ .../PullRequestPostAnalysisTaskTest.java | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index c63f2a1bb..24f897ff0 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -82,11 +82,6 @@ public String getDescription() { @Override public void finished(Context context) { ProjectAnalysis projectAnalysis = context.getProjectAnalysis(); - - ScannerContext scannerContext = projectAnalysis.getScannerContext().getProperties().size() > 0 - ? projectAnalysis.getScannerContext() - : GetScannerContext(projectAnalysis.getCeTask().getId()); - LOGGER.debug("found " + pullRequestDecorators.size() + " pull request decorators"); Optional optionalPullRequest = projectAnalysis.getBranch().filter(branch -> Branch.Type.PULL_REQUEST == branch.getType()); @@ -157,6 +152,10 @@ public void finished(Context context) { String commitId = revision.get(); + ScannerContext scannerContext = projectAnalysis.getScannerContext().getProperties().size() > 0 + ? projectAnalysis.getScannerContext() + : GetScannerContext(projectAnalysis.getCeTask().getId()); + AnalysisDetails analysisDetails = new AnalysisDetails(new AnalysisDetails.BranchDetails(optionalBranchName.get(), commitId), postAnalysisIssueVisitor, qualityGate, @@ -170,7 +169,7 @@ public void finished(Context context) { pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } - private ScannerContext GetScannerContext(String ceTaskId) + public ScannerContext GetScannerContext(String ceTaskId) { ScannerContext scannerContext = new ScannerContext() { private final Map props = new HashMap(); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index 9c69a530a..0cbddc6d3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -28,6 +28,7 @@ import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.config.Configuration; import org.sonar.api.platform.Server; +import org.sonar.ce.task.CeTask; import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; @@ -39,11 +40,9 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDao; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.ce.CeScannerContextDao; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -490,6 +489,10 @@ public void testFinishedAnalysisDecorationRequest() { QualityGate qualityGate = mock(QualityGate.class); doReturn(qualityGate).when(projectAnalysis).getQualityGate(); + org.sonar.api.ce.posttask.CeTask ceTask = mock(org.sonar.api.ce.posttask.CeTask.class); + doReturn(ceTask).when(projectAnalysis).getCeTask(); + when(ceTask.getId()).thenReturn(""); + PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-1").when(decorator1).name(); doReturn(ALM.BITBUCKET).when(decorator1).alm(); @@ -521,7 +524,12 @@ public void testFinishedAnalysisDecorationRequest() { when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - ScannerContext scannerContext = mock(ScannerContext.class); + //ScannerContext scannerContext = mock(ScannerContext.class); + ScannerContext scannerContext = new ScannerContext() { + private final Map props = new HashMap(); + @Override + public Map getProperties() { return this.props; } + }; doReturn(scannerContext).when(projectAnalysis).getScannerContext(); PullRequestPostAnalysisTask testCase = From e04f486a436bbe728f26212a8db24c1ddeecd473 Mon Sep 17 00:00:00 2001 From: iloer Date: Fri, 6 Mar 2020 13:46:43 +0300 Subject: [PATCH 14/26] iloer@mail.ru Add POST PR Status --- ...AzureDevOpsServerPullRequestDecorator.java | 112 ++++++++++++++---- .../model/GitPullRequestStatus.java | 24 ++++ .../azuredevops/model/GitStatusContext.java | 19 +++ .../azuredevops/model/GitStatusState.java | 32 +++++ .../model/GitStatusStateMapper.java | 21 ++++ 5 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index 1f5de1aa9..1a5a63630 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -1,7 +1,17 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitPullRequestStatus; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusContext; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusStateMapper; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.sonar.api.issue.Issue; import org.sonar.api.platform.Server; import org.sonar.api.utils.log.Logger; @@ -11,22 +21,30 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { + private static final String AZURE_API_VERSION = "api-version=5.0-preview.1"; + private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList()); - public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; - public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; - public static final String PULLREQUEST_AZUREDEVOPS_BRANCH = "sonar.pullrequest.branch"; - public static final String PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID = "sonar.pullrequest.key"; - public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; - public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository"; + public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; // sonar.pullrequest.vsts.instanceUrl=https://dev.azure.com/fabrikam/ + public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; // sonar.pullrequest.base=master + public static final String PULLREQUEST_AZUREDEVOPS_BRANCH = "sonar.pullrequest.branch"; // sonar.pullrequest.branch=feature/some-feature + public static final String PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID = "sonar.pullrequest.key"; // sonar.pullrequest.key=222 + public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; // sonar.pullrequest.vsts.project=MyProject + public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository";//sonar.pullrequest.vsts.repository=MyReposytory private final Server server; private final ScmInfoRepository scmInfoRepository; @@ -43,7 +61,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin String revision = analysisDetails.getCommitSha(); try { - /*final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( + final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))); @@ -59,31 +77,15 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin () -> new IllegalStateException(String.format( "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))); - final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElseThrow( - () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", - PULLREQUEST_AZUREDEVOPS_PROJECT_ID))); final String repositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", - PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME)));*/ - - //Configuration configuration = configurationRepository.getConfiguration(); + PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))); - //LOGGER.info(String.format("AZURE: ***prop count*** : %s ", analysisDetails.getScannerContextProp().size() )); - //analysisDetails.getScannerContextProp().forEach((sn, ss) -> LOGGER.info(String.format("AZURE: ***%s*** : %s ", sn, ss))); - - final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("Not found: PULLREQUEST_AZUREDEVOPS_INSTANCE_URL"); - final String baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElse("Not found: PULLREQUEST_AZUREDEVOPS_BASE_BRANCH"); - final String branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElse("Not found: PULLREQUEST_AZUREDEVOPS_BRANCH"); - - final String pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID"); final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); - final String repositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("Not found: PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME"); final String apiToken = almSettingDto.getPersonalAccessToken(); final String sonarBranch = analysisDetails.getBranchName(); - LOGGER.info(String.format("AZURE: apiURL is: %s ", apiToken)); LOGGER.info(String.format("AZURE: apiURL is: %s ", apiURL)); LOGGER.info(String.format("AZURE: baseBranch is: %s ", baseBranch)); LOGGER.info(String.format("AZURE: branch is: %s ", branch)); @@ -93,12 +95,72 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.info(String.format("AZURE: revision Commit/revision is: %s ", revision)); LOGGER.info(String.format("AZURE: sonarBranch is: %s ", sonarBranch)); + String encodeBytes = Base64.getEncoder().encodeToString((":" + apiToken).getBytes()); + Map headers = new HashMap<>(); + headers.put("Accept", "application/json"); + headers.put("Content-Type", "application/json; charset=utf-8"); + headers.put("Authorization", "Basic " + encodeBytes); + + postStatus(headers, analysisDetails); + } - catch (IllegalStateException ex){ + catch (IOException ex){ throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); } } + private void postStatus(Map headers, AnalysisDetails analysisDetails) throws IOException { + // POST https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.1-preview.1 + final String GIT_STATUS_CONTEXT_GENRE = "SonarQube"; + final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; + final String GIT_STATUS_DESCRIPTION = "SonarQube Status"; + + StringBuilder statusPostUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).get()); + statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).get()); + statusPostUrl.append("/_apis/git/repositories/"); + statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).get()); + statusPostUrl.append("/pullRequests/"); + statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).get()); + statusPostUrl.append("/statuses?"); + statusPostUrl.append(AZURE_API_VERSION); + + GitPullRequestStatus status = new GitPullRequestStatus(); + status.state = GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()); + status.description = GIT_STATUS_DESCRIPTION; + status.context = new GitStatusContext(GIT_STATUS_CONTEXT_GENRE, GIT_STATUS_CONTEXT_NAME); // "SonarQube/PullRequestDecoration" + status.targetUrl = String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), + StandardCharsets.UTF_8.name()), + URLEncoder.encode(analysisDetails.getBranchName(), + StandardCharsets.UTF_8.name())); + + LOGGER.trace(String.format("AZURE: postStatus-URL: %s ", statusPostUrl.toString())); + LOGGER.trace(String.format("AZURE: postStatus-BODY: %s ", new ObjectMapper().writeValueAsString(status))); + + HttpPost httpPost = new HttpPost(statusPostUrl.toString()); + for (Map.Entry entry : headers.entrySet()) { + httpPost.addHeader(entry.getKey(), entry.getValue()); + } + StringEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(status), StandardCharsets.UTF_8); + httpPost.setEntity(entity); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPost); + validateAzureDevOpsResponse(httpResponse, 200, "Status posted"); + } + } + + private void validateAzureDevOpsResponse(HttpResponse httpResponse, int expectedStatus, String successLogMessage) throws IOException { + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != expectedStatus) { + LOGGER.error(httpResponse.toString()); + LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug(httpResponse.toString()); + LOGGER.info(successLogMessage); + } + } + @Override public String name() { return "Azure"; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java new file mode 100644 index 000000000..b675419ef --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java @@ -0,0 +1,24 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class GitPullRequestStatus { + /** + * ID of the iteration to associate status with. Minimum value is 1. + */ + public Integer iterationId = null; + /** + * State of the status. + */ + public GitStatusState state; + /** + * Description of the status + */ + public String description; + /** + * Status context that uniquely identifies the status. + */ + public GitStatusContext context; + /** + * TargetUrl of the status + */ + public String targetUrl; +} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java new file mode 100644 index 000000000..ac73686b9 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java @@ -0,0 +1,19 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +/** + * Status context that uniquely identifies the status. + */ +public class GitStatusContext { + /** + * Name identifier of the status, cannot be null or empty. + */ + public final String name; + /** + * Genre of the status. Typically name of the service/tool generating the status, can be empty. + */ + public final String genre; + public GitStatusContext(String genre, String name){ + this.genre = genre; + this.name = name; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java new file mode 100644 index 000000000..b27953381 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java @@ -0,0 +1,32 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +/** + * State of the status. + */ +public enum GitStatusState +{ + /** + * Status state not set. Default state. + */ + NotSet, + /** + * Status pending. + */ + Pending, + /** + * Status succeeded. + */ + Succeeded, + /** + * Status failed. + */ + Failed, + /** + * Status with an error. + */ + Error, + /** + * Status is not applicable to the target object. + */ + NotApplicable +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java new file mode 100644 index 000000000..abea6af12 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java @@ -0,0 +1,21 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import org.sonar.api.ce.posttask.QualityGate; + +public class GitStatusStateMapper { + + private GitStatusStateMapper() { + super(); + } + + public static GitStatusState toGitStatusState(QualityGate.Status AnnalyzeStatus) { + switch (AnnalyzeStatus) { + case OK: + return GitStatusState.Succeeded; + case ERROR: + return GitStatusState.Error; + default: + return GitStatusState.NotSet; + } + } +} From 3e7d57c3cc31b233a340a13fc44f7e9d59ef5e2e Mon Sep 17 00:00:00 2001 From: iloer Date: Mon, 9 Mar 2020 01:47:09 +0300 Subject: [PATCH 15/26] iloer@mail.ru #102 add Azure object class and small refactoring --- ...AzureDevOpsServerPullRequestDecorator.java | 187 +++++++++++++----- .../azuredevops/model/Comment.java | 85 ++++++++ .../azuredevops/model/CommentPosition.java | 27 +++ .../azuredevops/model/CommentThread.java | 75 +++++++ .../model/CommentThreadContext.java | 62 ++++++ .../model/GitPullRequestStatus.java | 2 + .../azuredevops/model/IdentityRef.java | 51 +++++ .../model/PropertiesCollection.java | 4 + .../azuredevops/model/ReferenceLinks.java | 4 + .../azuredevops/model/SubjectDescriptor.java | 6 + .../model/enums/CommentThreadStatus.java | 36 ++++ .../azuredevops/model/enums/CommentType.java | 20 ++ .../model/{ => enums}/GitStatusState.java | 2 +- .../mappers/CommentThreadStatusMapper.java | 20 ++ .../{ => mappers}/GitStatusStateMapper.java | 3 +- 15 files changed, 528 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/{ => enums}/GitStatusState.java (96%) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/{ => mappers}/GitStatusStateMapper.java (82%) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index 1a5a63630..da1abe74c 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThread; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitPullRequestStatus; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusContext; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusStateMapper; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers.GitStatusStateMapper; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; @@ -24,27 +26,30 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { - + private enum ApiZone { + status, + thread + } + private String authorizationHeader; private static final String AZURE_API_VERSION = "api-version=5.0-preview.1"; - private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList()); - + // SCANNER PROPERTY public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; // sonar.pullrequest.vsts.instanceUrl=https://dev.azure.com/fabrikam/ public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; // sonar.pullrequest.base=master public static final String PULLREQUEST_AZUREDEVOPS_BRANCH = "sonar.pullrequest.branch"; // sonar.pullrequest.branch=feature/some-feature public static final String PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID = "sonar.pullrequest.key"; // sonar.pullrequest.key=222 public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; // sonar.pullrequest.vsts.project=MyProject public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository";//sonar.pullrequest.vsts.repository=MyReposytory + // SONAR URL MASK + public static final String SONAR_ISSUE_URL_MASK = "%s/project/issues?id=%s&issues=%s&open=%s&pullRequest=%s"; //http://sonarqube.shtormtech.ru/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + public static final String SONAR_RULE_URL_MASK = "%s/coding_rules?open=%s&rule_key=%s"; //http://sonarqube.shtormtech.ru/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 private final Server server; private final ScmInfoRepository scmInfoRepository; @@ -61,7 +66,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin String revision = analysisDetails.getCommitSha(); try { - final String apiURL = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( + final String azureUrl = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( () -> new IllegalStateException(String.format( "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))); @@ -83,10 +88,13 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))); final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); - final String apiToken = almSettingDto.getPersonalAccessToken(); final String sonarBranch = analysisDetails.getBranchName(); + if (almSettingDto.getPersonalAccessToken() == null ) { + throw new IllegalStateException("Could not decorate AzureDevops pullRequest. Access token has not been set"); + } + setAuthorizationHeader(almSettingDto.getPersonalAccessToken()); - LOGGER.info(String.format("AZURE: apiURL is: %s ", apiURL)); + LOGGER.info(String.format("AZURE: azureUrl is: %s ", azureUrl)); LOGGER.info(String.format("AZURE: baseBranch is: %s ", baseBranch)); LOGGER.info(String.format("AZURE: branch is: %s ", branch)); LOGGER.info(String.format("AZURE: pullRequestId is: %s ", pullRequestId)); @@ -95,69 +103,140 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.info(String.format("AZURE: revision Commit/revision is: %s ", revision)); LOGGER.info(String.format("AZURE: sonarBranch is: %s ", sonarBranch)); - String encodeBytes = Base64.getEncoder().encodeToString((":" + apiToken).getBytes()); - Map headers = new HashMap<>(); - headers.put("Accept", "application/json"); - headers.put("Content-Type", "application/json; charset=utf-8"); - headers.put("Authorization", "Basic " + encodeBytes); + sendPost( + getApiUrl(ApiZone.status, analysisDetails), + getGitPullRequestStatus(analysisDetails) + ); + + List openIssues = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())).collect(Collectors.toList()); + LOGGER.info(String.format("AZURE: issue count: %s ", openIssues.size())); + + for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { + String filePath = "/" + analysisDetails.getSCMPathForIssue(issue).orElse(null); + Integer line = issue.getIssue().getLine(); + if (filePath != null && line != null) { + try { + LOGGER.info(String.format("AZURE ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); + LOGGER.info(String.format("AZURE ISSUE: type: %s ", issue.getIssue().type().toString())); + LOGGER.info(String.format("AZURE ISSUE: type: %s ", issue.getIssue().severity())); + LOGGER.info(String.format("AZURE ISSUE: changes size: %s ", issue.getIssue().changes().size())); + LOGGER.info(String.format("AZURE ISSUE: key: %s ", issue.getIssue().key())); + LOGGER.info(String.format("AZURE ISSUE: selectedAt: %s ", issue.getIssue().selectedAt())); + LOGGER.info(String.format("AZURE ISSUE: componentKey: %s ", issue.getIssue().componentKey())); + LOGGER.info(String.format("AZURE ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); + LOGGER.info(String.format("AZURE ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); + LOGGER.info(String.format("AZURE COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); + LOGGER.info(String.format("AZURE COMPONENT: getFileAttributes: %s ", issue.getComponent().getFileAttributes().toString())); + LOGGER.info(String.format("AZURE COMPONENT: getReportAttributes: %s ", issue.getComponent().getReportAttributes().toString())); + LOGGER.info(String.format("AZURE COMPONENT: getViewAttributes: %s ", issue.getComponent().getViewAttributes().toString())); + LOGGER.info(String.format("AZURE COMPONENT: getSubViewAttributes: %s ", issue.getComponent().getSubViewAttributes().toString())); + + //LOGGER.info(String.format("AZURE ISSUE: currentChange.diffs: %s ", issue.getIssue().currentChange().diffs().size())); + + CommentThread thread = new CommentThread(filePath, line, issue.getIssue().getMessage()); + LOGGER.info(String.format("AZURE: thread: %s ", new ObjectMapper().writeValueAsString(thread))); + } catch (Exception e) { + LOGGER.error(e.toString()); + } + } + + } + } catch (IOException ex) { + throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); + } + } - postStatus(headers, analysisDetails); + private void setAuthorizationHeader(String apiToken) { + String encodeBytes = Base64.getEncoder().encodeToString((":" + apiToken).getBytes()); + authorizationHeader = "Basic " + encodeBytes; + } + public String getIssueUrl(String projectKey, String issueKey, String pullRequestId) throws IOException + { + //ISSUE http://sonarqube.shtormtech.ru/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + return String.format(SONAR_ISSUE_URL_MASK, + server.getPublicRootUrl(), + URLEncoder.encode(projectKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(pullRequestId, StandardCharsets.UTF_8.name()) + ); + } + public String getRuleUrlWithRuleKey(String ruleKey) throws IOException + { + //RULE http://sonarqube.shtormtech.ru/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 + return String.format(SONAR_RULE_URL_MASK, + server.getPublicRootUrl(), + URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()) + ); + } + private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails) throws UnsupportedOperationException { + StringBuilder postUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).get()); //instance + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).get()); //project + postUrl.append("/_apis/git/repositories/"); + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).get()); // repositoryId + postUrl.append("/pullRequests/"); + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).get()); // pullRequestId + + switch (apiZone) { + case status: { + // POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.0-preview.1 + postUrl.append("/statuses?"); + break; + } + case thread: { + //POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/threads?api-version=5.0 + postUrl.append("/threads?"); + break; + } + default: { + throw new UnsupportedOperationException("Not implemented method"); + } } - catch (IOException ex){ - throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); - } + postUrl.append(AZURE_API_VERSION); + return postUrl.toString(); } - private void postStatus(Map headers, AnalysisDetails analysisDetails) throws IOException { - // POST https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.1-preview.1 + private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { final String GIT_STATUS_CONTEXT_GENRE = "SonarQube"; final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; final String GIT_STATUS_DESCRIPTION = "SonarQube Status"; - StringBuilder statusPostUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).get()); - statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).get()); - statusPostUrl.append("/_apis/git/repositories/"); - statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).get()); - statusPostUrl.append("/pullRequests/"); - statusPostUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).get()); - statusPostUrl.append("/statuses?"); - statusPostUrl.append(AZURE_API_VERSION); - GitPullRequestStatus status = new GitPullRequestStatus(); status.state = GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()); status.description = GIT_STATUS_DESCRIPTION; status.context = new GitStatusContext(GIT_STATUS_CONTEXT_GENRE, GIT_STATUS_CONTEXT_NAME); // "SonarQube/PullRequestDecoration" status.targetUrl = String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), - URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), - StandardCharsets.UTF_8.name()), - URLEncoder.encode(analysisDetails.getBranchName(), - StandardCharsets.UTF_8.name())); + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), + StandardCharsets.UTF_8.name()), + URLEncoder.encode(analysisDetails.getBranchName(), + StandardCharsets.UTF_8.name()) + ); - LOGGER.trace(String.format("AZURE: postStatus-URL: %s ", statusPostUrl.toString())); - LOGGER.trace(String.format("AZURE: postStatus-BODY: %s ", new ObjectMapper().writeValueAsString(status))); + return new ObjectMapper().writeValueAsString(status); + } - HttpPost httpPost = new HttpPost(statusPostUrl.toString()); - for (Map.Entry entry : headers.entrySet()) { - httpPost.addHeader(entry.getKey(), entry.getValue()); - } - StringEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(status), StandardCharsets.UTF_8); + private void sendPost(String apiUrl, String body) throws IOException { + LOGGER.trace(String.format("AZURE: sendPost-URL: %s ", apiUrl)); + LOGGER.trace(String.format("AZURE: sendPost-BODY: %s ", body)); + HttpPost httpPost = new HttpPost(apiUrl); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/json; charset=utf-8"); + httpPost.addHeader("Authorization", authorizationHeader); + StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8); httpPost.setEntity(entity); try (CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpResponse httpResponse = httpClient.execute(httpPost); - validateAzureDevOpsResponse(httpResponse, 200, "Status posted"); - } - } - - private void validateAzureDevOpsResponse(HttpResponse httpResponse, int expectedStatus, String successLogMessage) throws IOException { - if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != expectedStatus) { - LOGGER.error(httpResponse.toString()); - LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); - throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); - } else if (null != httpResponse) { - LOGGER.debug(httpResponse.toString()); - LOGGER.info(successLogMessage); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error(httpResponse.toString()); + LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug(httpResponse.toString()); + LOGGER.info("Post success!"); + } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java new file mode 100644 index 000000000..c3cb03d51 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java @@ -0,0 +1,85 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentType; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + + +/** + * Represents a comment which is one of potentially many in a comment thread. + */ +public class Comment implements Serializable { + + final String content; + final CommentType commentType; + private Integer parentCommentId; + + public Comment(String content){ + this.content = content; + this.parentCommentId = 0; + this.commentType = CommentType.Text; + } + /** + * The ID of the parent comment. This is used for replies. + */ + public Integer getParentCommentId(){ + return this.parentCommentId; + }; + /** + * The comment content. + */ + public String getContent(){ + return this.content; + } + /** + * The ID of the parent comment. This is used for replies. + */ + public void setParentCommentId(Integer value){ + this.parentCommentId = value; + } + /** + * The comment type at the time of creation. + */ + public CommentType privateCommentType(){ + return this.commentType; + }; + /** + * The comment ID. IDs start at 1 and are unique to a pull request. + */ + private Integer id; + /** + * The parent thread ID. Used for internal server purposes only -- note + * that this field is not exposed to the REST client. + */ + private Integer threadId; + /** + * The author of the comment. + */ + private IdentityRef author; + /** + * The date the comment was first published.; + */ + private Date publishedDate; + /** + * The date the comment was last updated. + */ + private Date lastUpdatedDate; + /** + * The date the comment's content was last updated. + */ + private Date lastContentUpdatedDate; + /** + * Whether or not this comment was soft-deleted. + */ + private Boolean isDeleted; + /** + * A list of the users who have liked this comment. + */ + private List usersLiked; + /** + * Links to other related objects. + */ + private ReferenceLinks links; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java new file mode 100644 index 000000000..491acb74f --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java @@ -0,0 +1,27 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import java.io.Serializable; + +public class CommentPosition implements Serializable { + final Integer line; + final Integer offset; + + CommentPosition(Integer line, Integer offset){ + this.line = line; + this.offset = offset; + } + /** + *The line number of a thread's position. Starts at 1. /// + */ + public Integer getLine() + { + return this.line; + }; + + /** + *The character offset of a thread's position inside of a line. Starts at 0. + */ + public Integer getOffset(){ + return this.offset; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java new file mode 100644 index 000000000..5a6c95cbc --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -0,0 +1,75 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +/** + * Represents a comment thread of a pull request. A thread contains meta data about the file + * it was left on along with one or more comments (an initial comment and the subsequent replies). + */ +public class CommentThread implements Serializable { + + final CommentThreadStatus status; + private List comments; + private CommentThreadContext threadContext; + + public CommentThread(String filePath, Integer line, String message){ + comments = Arrays.asList( + new Comment(message) + ); + status = CommentThreadStatus.Active; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); + threadContext = new CommentThreadContext(filePath, line); + + } + /** + * A list of the comments. + */ + public List getComments(){ + return this.comments; + }; + /** + * The status of the comment thread. + */ + public CommentThreadStatus getStatus(){ + return this.status; + }; + /** + * Specify thread context such as position in left/right file. + */ + public CommentThreadContext getThreadContext() { + return this.threadContext; + }; + + /** + * The comment thread id. + */ + private int id; + /** + * The time this thread was published. + */ + private Date publishedDate; + /** + * The time this thread was last updated. + */ + private Date lastUpdatedDate; + /** + * Optional properties associated with the thread as a collection of key-value + */ + private PropertiesCollection properties; + /** + * Set of identities related to this thread + */ + private HashMap identities; + /** + * Specify if the thread is deleted which happens when all comments are deleted. + */ + private Boolean isDeleted; + /** + * Links to other related objects. + */ + private ReferenceLinks links; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java new file mode 100644 index 000000000..f813dc629 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java @@ -0,0 +1,62 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import org.sonar.core.issue.DefaultIssue; + +import java.io.Serializable; + +public class CommentThreadContext implements Serializable { + + final String filePath; + final CommentPosition leftFileStart; + final CommentPosition leftFileEnd; + final CommentPosition rightFileStart; + final CommentPosition rightFileEnd; + + public CommentThreadContext(String filePath, Integer line){ + this.filePath = filePath; + this.leftFileEnd = null; + this.leftFileStart = null; + this.rightFileEnd = new CommentPosition( + line, + 1 + ); + this.rightFileStart = new CommentPosition( + line, + 0 + ); + } + /** + * File path relative to the root of the repository. It's up to the client to + */ + public String getFilePath(){ + return this.filePath; + }; + + /** + * Position of first character of the thread's span in left file. /// + */ + public CommentPosition getLeftFileStart(){ + return this.leftFileStart; + }; + + /** + * Position of last character of the thread's span in left file. /// + */ + public CommentPosition getLeftFileEnd(){ + return this.leftFileEnd; + }; + + /** + * Position of first character of the thread's span in right file. /// + */ + public CommentPosition getRightFileStart(){ + return this.rightFileStart; + }; + + /** + * Position of last character of the thread's span in right file. /// + */ + public CommentPosition getRightFileEnd(){ + return this.rightFileEnd; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java index b675419ef..c7272049b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java @@ -1,5 +1,7 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; + public class GitPullRequestStatus { /** * ID of the iteration to associate status with. Minimum value is 1. diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java new file mode 100644 index 000000000..a40cd961c --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java @@ -0,0 +1,51 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class IdentityRef { + public SubjectDescriptor Descriptor; + public String DisplayName; + public String Url; + public ReferenceLinks Links; + public String Id; + /** + * Deprecated - use Domain+PrincipalName instead + */ + public String UniqueName; + /** + * Deprecated - Can be retrieved by querying the Graph user referenced in the + * "self" entry of the IdentityRef "_links" dictionary + */ + public String DirectoryAlias; + /** + * Deprecated - not in use in most preexisting implementations of ToIdentityRef + */ + public String ProfileUrl; + /** + * Deprecated - Available in the "avatar" entry of the IdentityRef "_links" dictionary + */ + public String ImageUrl; + /** + * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsGroupType) + */ + public Boolean IsContainer; + + /** + * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsAadUserType/Descriptor.IsAadGroupType) + */ + public Boolean IsAadIdentity; + /** + * Deprecated - Can be retrieved by querying the Graph membership state referenced + * in the "membershipState" entry of the GraphUser "_links" dictionary + */ + public Boolean Inactive; + public Boolean IsDeletedInOrigin; + + /** + * This property is for xml compat only. + */ + public String DisplayNameForXmlSerialization; + + /** + * This property is for xml compat only. + */ + public String UrlForXmlSerialization; +} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java new file mode 100644 index 000000000..fd5ba6608 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java @@ -0,0 +1,4 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class PropertiesCollection { +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java new file mode 100644 index 000000000..a649a4d8c --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java @@ -0,0 +1,4 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class ReferenceLinks { +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java new file mode 100644 index 000000000..bf2d177a1 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java @@ -0,0 +1,6 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class SubjectDescriptor { + public String SubjectType; + public String Identifier; +} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java new file mode 100644 index 000000000..3f6eeb4e9 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java @@ -0,0 +1,36 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; + +/** + * The status of a comment thread + */ +public enum CommentThreadStatus { + /** + * The thread status is unknown. + */ + Unknown, + /** + * The thread status is active. + */ + Active, + /** + * The thread status is resolved as fixed. + */ + Fixed, + /** + * The thread status is resolved as won't fix. + */ + WontFix, + /** + * The thread status is closed. + */ + Closed, + /** + * The thread status is resolved as by design. + */ + ByDesign, + /** + * The thread status is pending. + */ + Pending +} + diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java new file mode 100644 index 000000000..4bb3c0964 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java @@ -0,0 +1,20 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; + +public enum CommentType { + /** + * The comment type is not known. + */ + Unknown, + /** + * This is a regular user comment. + */ + Text, + /** + * The comment comes as a result of a code change. + */ + CodeChange, + /** + * The comment represents a system message. + */ + System +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java similarity index 96% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java index b27953381..bac2566f1 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusState.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java @@ -1,4 +1,4 @@ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; /** * State of the status. diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java new file mode 100644 index 000000000..5e947ebe8 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -0,0 +1,20 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; + + +public class CommentThreadStatusMapper { + + private CommentThreadStatusMapper() { + super(); + } + + public static CommentThreadStatus toCommentThreadStatus(String issueStatus) { + switch (issueStatus) { + case "Open": + return CommentThreadStatus.Active; + default: + return CommentThreadStatus.Fixed; + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java similarity index 82% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java index abea6af12..b73dd47a2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusStateMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java @@ -1,5 +1,6 @@ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; import org.sonar.api.ce.posttask.QualityGate; public class GitStatusStateMapper { From b42d8e63e5759478261973a124d95a5c0a42c756 Mon Sep 17 00:00:00 2001 From: iloer Date: Tue, 10 Mar 2020 21:24:17 +0300 Subject: [PATCH 16/26] iloer@mail.ru #102 add azure thread with issue --- ...AzureDevOpsServerPullRequestDecorator.java | 188 +++++++++++++----- .../azuredevops/model/Comment.java | 35 ++-- .../azuredevops/model/CommentPosition.java | 10 +- .../azuredevops/model/CommentThread.java | 42 +++- .../model/CommentThreadContext.java | 59 +++++- .../model/CommentThreadResponse.java | 16 ++ .../azuredevops/model/IdentityRef.java | 1 + .../model/PropertiesCollection.java | 1 + .../azuredevops/model/ReferenceLinks.java | 1 + .../model/enums/CommentThreadStatus.java | 14 +- .../azuredevops/model/enums/CommentType.java | 8 +- .../mappers/CommentThreadStatusMapper.java | 4 +- 12 files changed, 278 insertions(+), 101 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index da1abe74c..7eff248aa 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -1,14 +1,16 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThread; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitPullRequestStatus; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusContext; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.*; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers.GitStatusStateMapper; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -22,18 +24,26 @@ import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.protobuf.DbIssues; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Base64; import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { private enum ApiZone { status, thread } + private String authorizationHeader; private static final String AZURE_API_VERSION = "api-version=5.0-preview.1"; private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); @@ -48,8 +58,8 @@ private enum ApiZone { public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; // sonar.pullrequest.vsts.project=MyProject public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository";//sonar.pullrequest.vsts.repository=MyReposytory // SONAR URL MASK - public static final String SONAR_ISSUE_URL_MASK = "%s/project/issues?id=%s&issues=%s&open=%s&pullRequest=%s"; //http://sonarqube.shtormtech.ru/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 - public static final String SONAR_RULE_URL_MASK = "%s/coding_rules?open=%s&rule_key=%s"; //http://sonarqube.shtormtech.ru/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 + public static final String SONAR_ISSUE_URL_MASK = "%s/project/issues?id=%s&issues=%s&open=%s&pullRequest=%s"; //http://localhost/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + public static final String SONAR_RULE_URL_MASK = "%s/coding_rules?open=%s&rule_key=%s"; //http://localhost/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 private final Server server; private final ScmInfoRepository scmInfoRepository; @@ -87,21 +97,21 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))); - final String projectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); + final String azureProjectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); final String sonarBranch = analysisDetails.getBranchName(); - if (almSettingDto.getPersonalAccessToken() == null ) { + if (almSettingDto.getPersonalAccessToken() == null) { throw new IllegalStateException("Could not decorate AzureDevops pullRequest. Access token has not been set"); } setAuthorizationHeader(almSettingDto.getPersonalAccessToken()); - LOGGER.info(String.format("AZURE: azureUrl is: %s ", azureUrl)); - LOGGER.info(String.format("AZURE: baseBranch is: %s ", baseBranch)); - LOGGER.info(String.format("AZURE: branch is: %s ", branch)); - LOGGER.info(String.format("AZURE: pullRequestId is: %s ", pullRequestId)); - LOGGER.info(String.format("AZURE: projectId is: %s ", projectId)); - LOGGER.info(String.format("AZURE: repositoryName is: %s ", repositoryName)); - LOGGER.info(String.format("AZURE: revision Commit/revision is: %s ", revision)); - LOGGER.info(String.format("AZURE: sonarBranch is: %s ", sonarBranch)); + LOGGER.trace(String.format("azureUrl is: %s ", azureUrl)); + LOGGER.trace(String.format("baseBranch is: %s ", baseBranch)); + LOGGER.trace(String.format("branch is: %s ", branch)); + LOGGER.trace(String.format("pullRequestId is: %s ", pullRequestId)); + LOGGER.trace(String.format("vstsProjectId is: %s ", azureProjectId)); + LOGGER.trace(String.format("repositoryName is: %s ", repositoryName)); + LOGGER.trace(String.format("revision Commit/revision is: %s ", revision)); + LOGGER.trace(String.format("sonarBranch is: %s ", sonarBranch)); sendPost( getApiUrl(ApiZone.status, analysisDetails), @@ -109,37 +119,82 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin ); List openIssues = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())).collect(Collectors.toList()); - LOGGER.info(String.format("AZURE: issue count: %s ", openIssues.size())); + LOGGER.trace(String.format("AZURE: issue count: %s ", openIssues.size())); + + ArrayList respCommentThreads = new ArrayList(Arrays.asList(sendGet(getApiUrl(ApiZone.thread, analysisDetails), CommentThreadResponse.class).getValue())); + LOGGER.trace(String.format("AZURE: respCommentThreads count: %s ", respCommentThreads.size())); + respCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted); + LOGGER.trace(String.format("AZURE: respCommentThreads AFTER REMOVE count: %s ", respCommentThreads.size())); for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { String filePath = "/" + analysisDetails.getSCMPathForIssue(issue).orElse(null); Integer line = issue.getIssue().getLine(); if (filePath != null && line != null) { try { - LOGGER.info(String.format("AZURE ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); - LOGGER.info(String.format("AZURE ISSUE: type: %s ", issue.getIssue().type().toString())); - LOGGER.info(String.format("AZURE ISSUE: type: %s ", issue.getIssue().severity())); - LOGGER.info(String.format("AZURE ISSUE: changes size: %s ", issue.getIssue().changes().size())); - LOGGER.info(String.format("AZURE ISSUE: key: %s ", issue.getIssue().key())); - LOGGER.info(String.format("AZURE ISSUE: selectedAt: %s ", issue.getIssue().selectedAt())); - LOGGER.info(String.format("AZURE ISSUE: componentKey: %s ", issue.getIssue().componentKey())); - LOGGER.info(String.format("AZURE ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); - LOGGER.info(String.format("AZURE ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); - LOGGER.info(String.format("AZURE COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); - LOGGER.info(String.format("AZURE COMPONENT: getFileAttributes: %s ", issue.getComponent().getFileAttributes().toString())); - LOGGER.info(String.format("AZURE COMPONENT: getReportAttributes: %s ", issue.getComponent().getReportAttributes().toString())); - LOGGER.info(String.format("AZURE COMPONENT: getViewAttributes: %s ", issue.getComponent().getViewAttributes().toString())); - LOGGER.info(String.format("AZURE COMPONENT: getSubViewAttributes: %s ", issue.getComponent().getSubViewAttributes().toString())); - - //LOGGER.info(String.format("AZURE ISSUE: currentChange.diffs: %s ", issue.getIssue().currentChange().diffs().size())); - - CommentThread thread = new CommentThread(filePath, line, issue.getIssue().getMessage()); - LOGGER.info(String.format("AZURE: thread: %s ", new ObjectMapper().writeValueAsString(thread))); + + LOGGER.trace(String.format("ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); + LOGGER.trace(String.format("ISSUE: key: %s ", issue.getIssue().key())); + LOGGER.trace(String.format("ISSUE: type: %s ", issue.getIssue().type().toString())); + LOGGER.trace(String.format("ISSUE: severity: %s ", issue.getIssue().severity())); + LOGGER.trace(String.format("ISSUE: changes size: %s ", issue.getIssue().changes().size())); + LOGGER.trace(String.format("ISSUE: selectedAt: %s ", issue.getIssue().selectedAt())); + LOGGER.trace(String.format("ISSUE: componentKey: %s ", issue.getIssue().componentKey())); + LOGGER.trace(String.format("ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); + LOGGER.trace(String.format("ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); + LOGGER.trace(String.format("COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); + LOGGER.trace(String.format("COMPONENT: getFileAttributes: %s ", issue.getComponent().getFileAttributes().toString())); + LOGGER.trace(String.format("COMPONENT: getReportAttributes: %s ", issue.getComponent().getReportAttributes().toString())); + + boolean isExitsThread = false; + for (CommentThread azureThread : respCommentThreads) { + LOGGER.trace(String.format("azureFilePath: %s", azureThread.getThreadContext().getFilePath())); + LOGGER.trace(String.format("filePath: %s (%s)", filePath, azureThread.getThreadContext().getFilePath().equals(filePath))); + LOGGER.trace(String.format("azureLine: %d", azureThread.getThreadContext().getRightFileStart().getLine())); + LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine().equals(line))); + if (azureThread.getThreadContext().getFilePath().equals(filePath) + && azureThread.getThreadContext().getRightFileStart().getLine().equals(line)) { + isExitsThread = true; + break; + } + } + if (isExitsThread) { + LOGGER.info(String.format("SKIPPED ISSUE: %s" + + System.lineSeparator() + + "File: %s" + + System.lineSeparator() + + "Line: %d" + + System.lineSeparator() + + " . Issue is already exist in azure", + issue.getIssue().getMessage(), + filePath, + line)); + continue; + } + + String message = String.format("%s: %s ([rule](%s))" + System.lineSeparator() + + System.lineSeparator() + + "[See in Sonar](%s)", + issue.getIssue().type().toString(), + issue.getIssue().getMessage(), + getRuleUrlWithRuleKey(issue.getIssue().getRuleKey().toString()), + getIssueUrl( + analysisDetails.getAnalysisProjectKey(), + issue.getIssue().key(), + pullRequestId) + ); + + DbIssues.Locations locate = Objects.requireNonNull(issue.getIssue().getLocations()); + CommentThread thread = new CommentThread(filePath, locate, message); + LOGGER.trace(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); + sendPost( + getApiUrl(ApiZone.thread, analysisDetails), + new ObjectMapper().writeValueAsString(thread) + ); + LOGGER.info("Thread created successfully"); } catch (Exception e) { LOGGER.error(e.toString()); } } - } } catch (IOException ex) { throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); @@ -151,9 +206,8 @@ private void setAuthorizationHeader(String apiToken) { authorizationHeader = "Basic " + encodeBytes; } - public String getIssueUrl(String projectKey, String issueKey, String pullRequestId) throws IOException - { - //ISSUE http://sonarqube.shtormtech.ru/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + public String getIssueUrl(String projectKey, String issueKey, String pullRequestId) throws IOException { + //ISSUE http://localhost/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 return String.format(SONAR_ISSUE_URL_MASK, server.getPublicRootUrl(), URLEncoder.encode(projectKey, StandardCharsets.UTF_8.name()), @@ -162,22 +216,23 @@ public String getIssueUrl(String projectKey, String issueKey, String pullRequest URLEncoder.encode(pullRequestId, StandardCharsets.UTF_8.name()) ); } - public String getRuleUrlWithRuleKey(String ruleKey) throws IOException - { - //RULE http://sonarqube.shtormtech.ru/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 + + public String getRuleUrlWithRuleKey(String ruleKey) throws IOException { + //RULE http://localhost/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 return String.format(SONAR_RULE_URL_MASK, server.getPublicRootUrl(), URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()), URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()) ); } + private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails) throws UnsupportedOperationException { - StringBuilder postUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).get()); //instance - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).get()); //project + StringBuilder postUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("fail")); //instance + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("fail")); //project postUrl.append("/_apis/git/repositories/"); - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).get()); // repositoryId + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("fail")); // repositoryId postUrl.append("/pullRequests/"); - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).get()); // pullRequestId + postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("fail")); // pullRequestId switch (apiZone) { case status: { @@ -218,8 +273,8 @@ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws I } private void sendPost(String apiUrl, String body) throws IOException { - LOGGER.trace(String.format("AZURE: sendPost-URL: %s ", apiUrl)); - LOGGER.trace(String.format("AZURE: sendPost-BODY: %s ", body)); + LOGGER.trace(String.format("sendPost: URL: %s ", apiUrl)); + LOGGER.trace(String.format("sendPost: BODY: %s ", body)); HttpPost httpPost = new HttpPost(apiUrl); httpPost.addHeader("Accept", "application/json"); httpPost.addHeader("Content-Type", "application/json; charset=utf-8"); @@ -229,13 +284,44 @@ private void sendPost(String apiUrl, String body) throws IOException { try (CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpResponse httpResponse = httpClient.execute(httpPost); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error("sendPost: " + httpResponse.toString()); + LOGGER.error("sendPost: " + EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug("sendPost: " + httpResponse.toString()); + LOGGER.info("sendPost: Post success!"); + } + } + } + + private T sendGet(String apiUrl, Class type) throws IOException { + LOGGER.info(String.format("sendGet: URL: %s ", apiUrl)); + HttpGet httpGet = new HttpGet(apiUrl); + httpGet.addHeader("Accept", "application/json"); + httpGet.addHeader("Content-Type", "application/json; charset=utf-8"); + httpGet.addHeader("Authorization", authorizationHeader); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpGet); if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { LOGGER.error(httpResponse.toString()); LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); } else if (null != httpResponse) { - LOGGER.debug(httpResponse.toString()); - LOGGER.info("Post success!"); + //LOGGER.info(httpResponse.toString()); + HttpEntity entity = httpResponse.getEntity(); + T obj = new ObjectMapper() + .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8), type); + + LOGGER.info(type + " received"); + + return obj; + } else { + throw new IOException("No response reveived"); } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java index c3cb03d51..63953e3d2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java @@ -12,14 +12,16 @@ */ public class Comment implements Serializable { - final String content; - final CommentType commentType; + private String content; + private CommentType commentType; private Integer parentCommentId; + public Comment(){} + public Comment(String content){ this.content = content; this.parentCommentId = 0; - this.commentType = CommentType.Text; + this.commentType = CommentType.text; } /** * The ID of the parent comment. This is used for replies. @@ -42,44 +44,51 @@ public void setParentCommentId(Integer value){ /** * The comment type at the time of creation. */ - public CommentType privateCommentType(){ + public CommentType getCommentType(){ return this.commentType; }; + /** + * The comment type at the time of creation. + */ + public void setCommentType(CommentType value){ + this.commentType = value; + }; + /** * The comment ID. IDs start at 1 and are unique to a pull request. */ - private Integer id; + public Integer id; /** * The parent thread ID. Used for internal server purposes only -- note * that this field is not exposed to the REST client. */ - private Integer threadId; + public Integer threadId; /** * The author of the comment. */ - private IdentityRef author; + public IdentityRef author; /** * The date the comment was first published.; */ - private Date publishedDate; + public Date publishedDate; /** * The date the comment was last updated. */ - private Date lastUpdatedDate; + public Date lastUpdatedDate; /** * The date the comment's content was last updated. */ - private Date lastContentUpdatedDate; + public Date lastContentUpdatedDate; /** * Whether or not this comment was soft-deleted. */ - private Boolean isDeleted; + public Boolean isDeleted; /** * A list of the users who have liked this comment. */ - private List usersLiked; + public List usersLiked; /** * Links to other related objects. */ - private ReferenceLinks links; + public ReferenceLinks links; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java index 491acb74f..65896f6a5 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java @@ -3,12 +3,14 @@ import java.io.Serializable; public class CommentPosition implements Serializable { - final Integer line; - final Integer offset; + private Integer line; + private Integer offset; - CommentPosition(Integer line, Integer offset){ + public CommentPosition() {}; + + public CommentPosition(Integer line, Integer offset){ this.line = line; - this.offset = offset; + this.offset = offset + 1; } /** *The line number of a thread's position. Starts at 1. /// diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java index 5a6c95cbc..3aece56c8 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -1,6 +1,8 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; +import org.sonar.db.protobuf.DbIssues; + import java.io.Serializable; import java.util.Arrays; import java.util.Date; @@ -13,16 +15,20 @@ */ public class CommentThread implements Serializable { - final CommentThreadStatus status; + private CommentThreadStatus status; private List comments; private CommentThreadContext threadContext; + public CommentThread(){}; - public CommentThread(String filePath, Integer line, String message){ + public CommentThread(String filePath, DbIssues.Locations locations, String message){ comments = Arrays.asList( new Comment(message) ); - status = CommentThreadStatus.Active; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); - threadContext = new CommentThreadContext(filePath, line); + status = CommentThreadStatus.active; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); + threadContext = new CommentThreadContext( + filePath, + locations + ); } /** @@ -31,31 +37,49 @@ public CommentThread(String filePath, Integer line, String message){ public List getComments(){ return this.comments; }; + /** + * A list of the comments. + */ + public void setComments(List value){ + this.comments = value; + }; /** * The status of the comment thread. */ public CommentThreadStatus getStatus(){ return this.status; }; + /** + * The status of the comment thread. + */ + public void setStatus(CommentThreadStatus value){ + this.status = value; + }; /** * Specify thread context such as position in left/right file. */ public CommentThreadContext getThreadContext() { return this.threadContext; }; + /** + * Specify thread context such as position in left/right file. + */ + public void setThreadContext(CommentThreadContext value) { + this.threadContext = value; + }; /** * The comment thread id. */ - private int id; + public int id; /** * The time this thread was published. */ - private Date publishedDate; + public Date publishedDate; /** * The time this thread was last updated. */ - private Date lastUpdatedDate; + public Date lastUpdatedDate; /** * Optional properties associated with the thread as a collection of key-value */ @@ -63,11 +87,11 @@ public CommentThreadContext getThreadContext() { /** * Set of identities related to this thread */ - private HashMap identities; + public HashMap identities; /** * Specify if the thread is deleted which happens when all comments are deleted. */ - private Boolean isDeleted; + public Boolean isDeleted; /** * Links to other related objects. */ diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java index f813dc629..70c9ab106 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java @@ -1,28 +1,30 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; -import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.protobuf.DbIssues; import java.io.Serializable; public class CommentThreadContext implements Serializable { - final String filePath; - final CommentPosition leftFileStart; - final CommentPosition leftFileEnd; - final CommentPosition rightFileStart; - final CommentPosition rightFileEnd; + private String filePath; + private CommentPosition leftFileStart; + private CommentPosition leftFileEnd; + private CommentPosition rightFileStart; + private CommentPosition rightFileEnd; - public CommentThreadContext(String filePath, Integer line){ + public CommentThreadContext() {}; + + public CommentThreadContext(String filePath, DbIssues.Locations locations){ this.filePath = filePath; this.leftFileEnd = null; this.leftFileStart = null; this.rightFileEnd = new CommentPosition( - line, - 1 + locations.getTextRange().getEndLine(), + locations.getTextRange().getEndOffset() ); this.rightFileStart = new CommentPosition( - line, - 0 + locations.getTextRange().getStartLine(), + locations.getTextRange().getStartOffset() ); } /** @@ -31,6 +33,12 @@ public CommentThreadContext(String filePath, Integer line){ public String getFilePath(){ return this.filePath; }; + /** + * File path relative to the root of the repository. It's up to the client to + */ + public void setFilePath(String value){ + this.filePath = value; + }; /** * Position of first character of the thread's span in left file. /// @@ -39,12 +47,27 @@ public CommentPosition getLeftFileStart(){ return this.leftFileStart; }; + /** + * Position of first character of the thread's span in left file. /// + */ + public void setLeftFileStart(CommentPosition value) + { + this.leftFileStart = value; + }; + /** * Position of last character of the thread's span in left file. /// */ public CommentPosition getLeftFileEnd(){ return this.leftFileEnd; }; + /** + * Position of last character of the thread's span in left file. /// + */ + public void setLeftFileEnd(CommentPosition value) + { + this.leftFileEnd = value; + }; /** * Position of first character of the thread's span in right file. /// @@ -52,6 +75,13 @@ public CommentPosition getLeftFileEnd(){ public CommentPosition getRightFileStart(){ return this.rightFileStart; }; + /** + * Position of first character of the thread's span in right file. /// + */ + public void setRightFileStart(CommentPosition value) + { + this.rightFileStart = value; + }; /** * Position of last character of the thread's span in right file. /// @@ -59,4 +89,11 @@ public CommentPosition getRightFileStart(){ public CommentPosition getRightFileEnd(){ return this.rightFileEnd; }; + /** + * Position of last character of the thread's span in right file. /// + */ + public void setRightFileEnd(CommentPosition value) + { + this.rightFileEnd = value; + }; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java new file mode 100644 index 000000000..57d0b118b --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java @@ -0,0 +1,16 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CommentThreadResponse { + private CommentThread[] value; + + @JsonCreator + public CommentThreadResponse(@JsonProperty("value") CommentThread[] value){ + this.value = value; + } + public CommentThread[] getValue(){ + return this.value; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java index a40cd961c..4823eacdd 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java @@ -1,6 +1,7 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; public class IdentityRef { + public IdentityRef(){}; public SubjectDescriptor Descriptor; public String DisplayName; public String Url; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java index fd5ba6608..544d42df4 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java @@ -1,4 +1,5 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; public class PropertiesCollection { + public PropertiesCollection(){}; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java index a649a4d8c..7304bb979 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java @@ -1,4 +1,5 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; public class ReferenceLinks { + public ReferenceLinks(){}; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java index 3f6eeb4e9..dfff1376f 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java @@ -7,30 +7,30 @@ public enum CommentThreadStatus { /** * The thread status is unknown. */ - Unknown, + unknown, /** * The thread status is active. */ - Active, + active, /** * The thread status is resolved as fixed. */ - Fixed, + fixed, /** * The thread status is resolved as won't fix. */ - WontFix, + wontFix, /** * The thread status is closed. */ - Closed, + closed, /** * The thread status is resolved as by design. */ - ByDesign, + byDesign, /** * The thread status is pending. */ - Pending + pending } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java index 4bb3c0964..c2ca09569 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java @@ -4,17 +4,17 @@ public enum CommentType { /** * The comment type is not known. */ - Unknown, + unknown, /** * This is a regular user comment. */ - Text, + text, /** * The comment comes as a result of a code change. */ - CodeChange, + codeChange, /** * The comment represents a system message. */ - System + system } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java index 5e947ebe8..3ac5ae6b3 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -12,9 +12,9 @@ private CommentThreadStatusMapper() { public static CommentThreadStatus toCommentThreadStatus(String issueStatus) { switch (issueStatus) { case "Open": - return CommentThreadStatus.Active; + return CommentThreadStatus.active; default: - return CommentThreadStatus.Fixed; + return CommentThreadStatus.fixed; } } } From 174483acd35ec283c6ea7878afea0432cca3ed75 Mon Sep 17 00:00:00 2001 From: iloer Date: Wed, 11 Mar 2020 00:10:21 +0300 Subject: [PATCH 17/26] iloer@mail.ru #102 add: closed issue in sonar, resolve in Azure --- ...AzureDevOpsServerPullRequestDecorator.java | 126 +++++++++++++----- .../azuredevops/model/CommentThread.java | 2 +- .../pullrequest/azuredevops/model/Link.java | 5 + .../azuredevops/model/ReferenceLinks.java | 2 +- .../mappers/CommentThreadStatusMapper.java | 13 +- 5 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index 7eff248aa..b8f723de2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -6,11 +6,13 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.*; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers.GitStatusStateMapper; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -45,11 +47,11 @@ private enum ApiZone { } private String authorizationHeader; - private static final String AZURE_API_VERSION = "api-version=5.0-preview.1"; + private static final String AZURE_API_VERSION = "?api-version=5.0-preview.1"; private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); - private static final List OPEN_ISSUE_STATUSES = + /*private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) - .collect(Collectors.toList()); + .collect(Collectors.toList());*/ // SCANNER PROPERTY public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; // sonar.pullrequest.vsts.instanceUrl=https://dev.azure.com/fabrikam/ public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; // sonar.pullrequest.base=master @@ -108,30 +110,35 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("baseBranch is: %s ", baseBranch)); LOGGER.trace(String.format("branch is: %s ", branch)); LOGGER.trace(String.format("pullRequestId is: %s ", pullRequestId)); - LOGGER.trace(String.format("vstsProjectId is: %s ", azureProjectId)); + LOGGER.trace(String.format("azureProjectId is: %s ", azureProjectId)); LOGGER.trace(String.format("repositoryName is: %s ", repositoryName)); LOGGER.trace(String.format("revision Commit/revision is: %s ", revision)); LOGGER.trace(String.format("sonarBranch is: %s ", sonarBranch)); sendPost( getApiUrl(ApiZone.status, analysisDetails), - getGitPullRequestStatus(analysisDetails) + getGitPullRequestStatus(analysisDetails), + "Status set successfully" ); - List openIssues = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())).collect(Collectors.toList()); - LOGGER.trace(String.format("AZURE: issue count: %s ", openIssues.size())); + List openIssues = analysisDetails.getPostAnalysisIssueVisitor() + .getIssues(); + /*.stream() + .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())) + .collect(Collectors.toList());*/ + LOGGER.trace(String.format("Analyze issue count: %s ", openIssues.size())); - ArrayList respCommentThreads = new ArrayList(Arrays.asList(sendGet(getApiUrl(ApiZone.thread, analysisDetails), CommentThreadResponse.class).getValue())); - LOGGER.trace(String.format("AZURE: respCommentThreads count: %s ", respCommentThreads.size())); - respCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted); - LOGGER.trace(String.format("AZURE: respCommentThreads AFTER REMOVE count: %s ", respCommentThreads.size())); + ArrayList azureCommentThreads = new ArrayList(Arrays.asList(sendGet(getApiUrl(ApiZone.thread, analysisDetails), CommentThreadResponse.class).getValue())); + LOGGER.trace(String.format("Azure commentThreads count: %s ", azureCommentThreads.size())); + azureCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted); + LOGGER.trace(String.format("Azure commentThreads AFTER REMOVE count: %s ", azureCommentThreads.size())); for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { - String filePath = "/" + analysisDetails.getSCMPathForIssue(issue).orElse(null); + String filePath = analysisDetails.getSCMPathForIssue(issue).orElse(null); Integer line = issue.getIssue().getLine(); if (filePath != null && line != null) { try { - + filePath = "/" + filePath; LOGGER.trace(String.format("ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); LOGGER.trace(String.format("ISSUE: key: %s ", issue.getIssue().key())); LOGGER.trace(String.format("ISSUE: type: %s ", issue.getIssue().type().toString())); @@ -146,25 +153,47 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("COMPONENT: getReportAttributes: %s ", issue.getComponent().getReportAttributes().toString())); boolean isExitsThread = false; - for (CommentThread azureThread : respCommentThreads) { + for (CommentThread azureThread : azureCommentThreads) { LOGGER.trace(String.format("azureFilePath: %s", azureThread.getThreadContext().getFilePath())); LOGGER.trace(String.format("filePath: %s (%s)", filePath, azureThread.getThreadContext().getFilePath().equals(filePath))); LOGGER.trace(String.format("azureLine: %d", azureThread.getThreadContext().getRightFileStart().getLine())); LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine().equals(line))); if (azureThread.getThreadContext().getFilePath().equals(filePath) && azureThread.getThreadContext().getRightFileStart().getLine().equals(line)) { + + if(!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN) + && azureThread.getStatus().equals(CommentThreadStatus.active)) { + azureThread.setStatus(CommentThreadStatus.closed); + Comment comment = new Comment("Closed in SonarQube"); + LOGGER.info("Issue closed in Sonar. try close in Azure"); + sendPost( + azureThread._links.self.href + "/comments" + AZURE_API_VERSION, + new ObjectMapper().writeValueAsString(comment), + "Comment added success" + ); + sendPatch( + //getApiUrl(ApiZone.thread, analysisDetails, azureThread.id), + azureThread._links.self.href + AZURE_API_VERSION, + "{\"status\":\"closed\"}" + ); + } isExitsThread = true; break; } } - if (isExitsThread) { + if (!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN)) { + LOGGER.info(String.format("SKIPPED ISSUE: Issue status is %s", issue.getIssue().getStatus())); + continue; + } + + if (isExitsThread || !issue.getIssue().getStatus().equals(Issue.STATUS_OPEN)) { LOGGER.info(String.format("SKIPPED ISSUE: %s" + System.lineSeparator() + "File: %s" + System.lineSeparator() + "Line: %d" + System.lineSeparator() - + " . Issue is already exist in azure", + + "Issue is already exist in azure", issue.getIssue().getMessage(), filePath, line)); @@ -173,8 +202,8 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin String message = String.format("%s: %s ([rule](%s))" + System.lineSeparator() + System.lineSeparator() - + "[See in Sonar](%s)", - issue.getIssue().type().toString(), + + "[See in SonarQube](%s)", + issue.getIssue().type().name(), issue.getIssue().getMessage(), getRuleUrlWithRuleKey(issue.getIssue().getRuleKey().toString()), getIssueUrl( @@ -185,12 +214,12 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin DbIssues.Locations locate = Objects.requireNonNull(issue.getIssue().getLocations()); CommentThread thread = new CommentThread(filePath, locate, message); - LOGGER.trace(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); + LOGGER.info(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); sendPost( getApiUrl(ApiZone.thread, analysisDetails), - new ObjectMapper().writeValueAsString(thread) + new ObjectMapper().writeValueAsString(thread), + "Thread created successfully" ); - LOGGER.info("Thread created successfully"); } catch (Exception e) { LOGGER.error(e.toString()); } @@ -227,30 +256,35 @@ public String getRuleUrlWithRuleKey(String ruleKey) throws IOException { } private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails) throws UnsupportedOperationException { - StringBuilder postUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("fail")); //instance - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("fail")); //project - postUrl.append("/_apis/git/repositories/"); - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("fail")); // repositoryId - postUrl.append("/pullRequests/"); - postUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("fail")); // pullRequestId - + return getApiUrl(apiZone, analysisDetails, null); + }; + private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails, Integer id) throws UnsupportedOperationException { + StringBuilder apiUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("fail")); //instance + apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("fail")); //project + apiUrl.append("/_apis/git/repositories/"); + apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("fail")); // repositoryId + apiUrl.append("/pullRequests/"); + apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("fail")); // pullRequestId switch (apiZone) { case status: { // POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.0-preview.1 - postUrl.append("/statuses?"); + apiUrl.append("/statuses"); break; } case thread: { //POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/threads?api-version=5.0 - postUrl.append("/threads?"); + apiUrl.append("/threads"); break; } default: { throw new UnsupportedOperationException("Not implemented method"); } } - postUrl.append(AZURE_API_VERSION); - return postUrl.toString(); + if (id != null){ + apiUrl.append("/").append(id); + } + apiUrl.append(AZURE_API_VERSION); + return apiUrl.toString(); } private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { @@ -273,6 +307,9 @@ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws I } private void sendPost(String apiUrl, String body) throws IOException { + sendPost(apiUrl, body, "POST success!"); + } + private void sendPost(String apiUrl, String body, String successMessage) throws IOException { LOGGER.trace(String.format("sendPost: URL: %s ", apiUrl)); LOGGER.trace(String.format("sendPost: BODY: %s ", body)); HttpPost httpPost = new HttpPost(apiUrl); @@ -290,7 +327,7 @@ private void sendPost(String apiUrl, String body) throws IOException { throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); } else if (null != httpResponse) { LOGGER.debug("sendPost: " + httpResponse.toString()); - LOGGER.info("sendPost: Post success!"); + LOGGER.info("sendPost: " + successMessage); } } } @@ -326,6 +363,29 @@ private T sendGet(String apiUrl, Class type) throws IOException { } } + private void sendPatch(String apiUrl, String body) throws IOException { + LOGGER.trace(String.format("sendPatch: URL: %s ", apiUrl)); + LOGGER.trace(String.format("sendPatch: BODY: %s ", body)); + HttpPatch httpPatch = new HttpPatch(apiUrl); + httpPatch.addHeader("Accept", "application/json"); + httpPatch.addHeader("Content-Type", "application/json; charset=utf-8"); + httpPatch.addHeader("Authorization", authorizationHeader); + StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8); + httpPatch.setEntity(entity); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPatch); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error("sendPatch: " + httpResponse.toString()); + LOGGER.error("sendPatch: " + EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug("sendPatch: " + httpResponse.toString()); + LOGGER.info("sendPatch: Patch success!"); + } + } + } + @Override public String name() { return "Azure"; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java index 3aece56c8..ba89c38d0 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -95,5 +95,5 @@ public void setThreadContext(CommentThreadContext value) { /** * Links to other related objects. */ - private ReferenceLinks links; + public ReferenceLinks _links; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java new file mode 100644 index 000000000..69a2c5b8e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java @@ -0,0 +1,5 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +public class Link { + public String href; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java index 7304bb979..0030aef33 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java @@ -1,5 +1,5 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; public class ReferenceLinks { - public ReferenceLinks(){}; + public Link self; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java index 3ac5ae6b3..88582c5c4 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -1,6 +1,9 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; +import org.sonar.api.issue.Issue; + +import static org.sonar.api.issue.Issue.*; public class CommentThreadStatusMapper { @@ -11,10 +14,18 @@ private CommentThreadStatusMapper() { public static CommentThreadStatus toCommentThreadStatus(String issueStatus) { switch (issueStatus) { - case "Open": + case STATUS_OPEN: return CommentThreadStatus.active; default: return CommentThreadStatus.fixed; } } + public static String toIssueStatus(CommentThreadStatus commentThreadStatus) { + switch (commentThreadStatus) { + case active: + return STATUS_OPEN; + default: + return STATUS_CLOSED; + } + } } From 05a522497a53f085237edf724880844e78294405 Mon Sep 17 00:00:00 2001 From: iloer Date: Thu, 12 Mar 2020 00:11:02 +0300 Subject: [PATCH 18/26] iloer@mail.ru #102 add unit tests --- ...AzureDevOpsServerPullRequestDecorator.java | 12 +- ...eDevOpsServerPullRequestDecoratorTest.java | 294 ++++++++++++++++++ 2 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index b8f723de2..294199843 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -235,11 +235,11 @@ private void setAuthorizationHeader(String apiToken) { authorizationHeader = "Basic " + encodeBytes; } - public String getIssueUrl(String projectKey, String issueKey, String pullRequestId) throws IOException { + public String getIssueUrl(String sonarProjectKey, String issueKey, String pullRequestId) throws IOException { //ISSUE http://localhost/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 return String.format(SONAR_ISSUE_URL_MASK, server.getPublicRootUrl(), - URLEncoder.encode(projectKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(sonarProjectKey, StandardCharsets.UTF_8.name()), URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), URLEncoder.encode(pullRequestId, StandardCharsets.UTF_8.name()) @@ -256,9 +256,6 @@ public String getRuleUrlWithRuleKey(String ruleKey) throws IOException { } private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails) throws UnsupportedOperationException { - return getApiUrl(apiZone, analysisDetails, null); - }; - private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails, Integer id) throws UnsupportedOperationException { StringBuilder apiUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("fail")); //instance apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("fail")); //project apiUrl.append("/_apis/git/repositories/"); @@ -280,9 +277,6 @@ private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails, Integ throw new UnsupportedOperationException("Not implemented method"); } } - if (id != null){ - apiUrl.append("/").append(id); - } apiUrl.append(AZURE_API_VERSION); return apiUrl.toString(); } @@ -290,7 +284,7 @@ private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails, Integ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { final String GIT_STATUS_CONTEXT_GENRE = "SonarQube"; final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; - final String GIT_STATUS_DESCRIPTION = "SonarQube Status"; + final String GIT_STATUS_DESCRIPTION = "SonarQube Gate"; GitPullRequestStatus status = new GitPullRequestStatus(); status.state = GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java new file mode 100644 index 000000000..9c66a251b --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -0,0 +1,294 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.issue.Issue; +import org.sonar.api.platform.Server; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; + +import java.util.Base64; +import java.util.Collections; +import java.util.Optional; + + +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AzureDevOpsServerPullRequestDecoratorTest { + @Rule + public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig()); + + @Test + public void testName() { + assertEquals("Azure", new AzureDevOpsServerPullRequestDecorator( mock(Server.class), mock(ScmInfoRepository.class) ).name()); + } + + @Test + public void decorateQualityGateStatus() { + String azureProject = "azureProject"; + String sonarProject = "sonarProject"; + String pullRequestId = "8513"; + String baseBranchName = "master"; + String branchName = "feature/some-feature"; + String azureRepository = "myRepository"; + String sonarRootUrl = "http://sonar:9000/sonar"; + String filePath = "/path/to/file"; + int lineNumber = 5; + String token = "token"; + String authorizationHeader = "Basic " + Base64.getEncoder().encodeToString((":" + token).getBytes()); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getPersonalAccessToken()).thenReturn(token); + + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))) + .thenReturn(Optional.of(wireMockRule.baseUrl())); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))) + .thenReturn(Optional.of(azureRepository)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))) + .thenReturn(Optional.of(pullRequestId)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_PROJECT_ID))) + .thenReturn(Optional.of(azureProject)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_BASE_BRANCH))) + .thenReturn(Optional.of(baseBranchName)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_BRANCH))) + .thenReturn(Optional.of(branchName)); + + when(analysisDetails.getAnalysisProjectKey()).thenReturn(sonarProject); + when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); + when(analysisDetails.getBranchName()).thenReturn(pullRequestId); + + + PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + DefaultIssue defaultIssue = mock(DefaultIssue.class); + when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); + when(defaultIssue.getLine()).thenReturn(lineNumber); + when(componentIssue.getIssue()).thenReturn(defaultIssue); + + Component component = mock(Component.class); + when(componentIssue.getComponent()).thenReturn(component); + when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); + when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + + ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); + wireMockRule.stubFor(get(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/threads?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .willReturn(okJson("{\n" + + " \"value\": [\n" + + " {\n" + + " \"pullRequestThreadContext\": {\n" + + " \"iterationContext\": {\n" + + " \"firstComparingIteration\": 1,\n" + + " \"secondComparingIteration\": 1\n" + + " },\n" + + " \"changeTrackingId\": 4\n" + + " },\n" + + " \"id\": 80450,\n" + + " \"publishedDate\": \"2020-03-10T17:40:09.603Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"comments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"parentCommentId\": 0,\n" + + " \"author\": {\n" + + " \"displayName\": \"More text\",\n" + + " \"url\": \"https://dev.azure.com/fabrikam/_apis/Identities/c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"_links\": {\n" + + " \"avatar\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " }\n" + + " },\n" + + " \"id\": \"c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"uniqueName\": \"user@mail.ru\",\n" + + " \"imageUrl\": \"https://dev.azure.com/fabrikam/_api/_common/identityImage?id=c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"descriptor\": \"win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " },\n" + + " \"publishedDate\": \"2020-03-10T17:40:09.603Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"lastContentUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"isDeleted\": true,\n" + + " \"commentType\": \"text\",\n" + + " \"usersLiked\": [],\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80450/comments/1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"threadContext\": {\n" + + " \"filePath\": \"/azureProject/myReposytory/Helpers/file.cs\",\n" + + " \"rightFileStart\": {\n" + + " \"line\": 18,\n" + + " \"offset\": 11\n" + + " },\n" + + " \"rightFileEnd\": {\n" + + " \"line\": 18,\n" + + " \"offset\": 15\n" + + " }\n" + + " },\n" + + " \"properties\": {},\n" + + " \"identities\": null,\n" + + " \"isDeleted\": true,\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80450\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"pullRequestThreadContext\": {\n" + + " \"iterationContext\": {\n" + + " \"firstComparingIteration\": 1,\n" + + " \"secondComparingIteration\": 1\n" + + " },\n" + + " \"changeTrackingId\": 13\n" + + " },\n" + + " \"id\": 80452,\n" + + " \"publishedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"comments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"parentCommentId\": 0,\n" + + " \"author\": {\n" + + " \"displayName\": \"text\",\n" + + " \"url\": \"https://dev.azure.com/fabrikam/_apis/Identities/c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"_links\": {\n" + + " \"avatar\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " }\n" + + " },\n" + + " \"id\": \"c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"uniqueName\": \"user@mail.ru\",\n" + + " \"imageUrl\": \"https://dev.azure.com/fabrikam/_api/_common/identityImage?id=c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"descriptor\": \"win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " },\n" + + " \"content\": \"Comment\",\n" + + " \"publishedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastContentUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"commentType\": \"text\",\n" + + " \"usersLiked\": [],\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80452/comments/1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"threadContext\": {\n" + + " \"filePath\": \"" + filePath + "\",\n" + + " \"rightFileStart\": {\n" + + " \"line\": 30,\n" + + " \"offset\": 57\n" + + " },\n" + + " \"rightFileEnd\": {\n" + + " \"line\": 30,\n" + + " \"offset\": 65\n" + + " }\n" + + " },\n" + + " \"properties\": {},\n" + + " \"identities\": null,\n" + + " \"isDeleted\": false,\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80452\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"count\": 2\n" + + "}"))); + + wireMockRule.stubFor(post(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/threads?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .withRequestBody(equalTo("{\n" + + "\t\"status\":\"active\",\n" + + "\t\"comments\":[\n" + + "\t\t{\n" + + "\t\t\t\"content\":\"Message\",\n" + + "\t\t\t\"commentType\":\"text\",\n" + + "\t\t\t\"parentCommentId\":0,\n" + + "\t\t\t\"id\":null,\n" + + "\t\t\t\"threadId\":null,\n" + + "\t\t\t\"author\":null,\n" + + "\t\t\t\"publishedDate\":null,\n" + + "\t\t\t\"lastUpdatedDate\":null,\n" + + "\t\t\t\"lastContentUpdatedDate\":null,\n" + + "\t\t\t\"isDeleted\":null,\n" + + "\t\t\t\"usersLiked\":null,\n" + + "\t\t\t\"links\":null\n" + + "\t\t}\n" + + "\t],\n" + + "\t\"threadContext\":{\n" + + "\t\t\"filePath\":\"" + filePath + "\",\n" + + "\t\t\"leftFileStart\":null,\n" + + "\t\t\"leftFileEnd\":null,\n" + + "\t\t\"rightFileStart\":{\n" + + "\t\t\t\"line\":3,\n" + + "\t\t\t\"offset\":6\n" + + "\t\t},\n" + + "\t\t\"rightFileEnd\":{\n" + + "\t\t\t\"line\":3,\n" + + "\t\t\t\"offset\":10}\n" + + "\t\t},\n" + + "\t\"id\":0,\n" + + "\t\"publishedDate\":null,\n" + + "\t\"lastUpdatedDate\":null,\n" + + "\t\"identities\":null,\n" + + "\t\"isDeleted\":null\n" + + "} ") + ) + .willReturn(ok())); + + + wireMockRule.stubFor(post(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/statuses?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .withRequestBody(equalTo("{" + + "\"iterationId\":null," + + "\"state\":\"Succeeded\"," + + "\"description\":\"SonarQube Gate\"," + + "\"context\":{\"name\":\"PullRequestDecoration\",\"genre\":\"SonarQube\"}," + + "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" + + "}") + ) + .willReturn(ok())); + + Server server = mock(Server.class); + when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); + AzureDevOpsServerPullRequestDecorator pullRequestDecorator = + new AzureDevOpsServerPullRequestDecorator(server, scmInfoRepository); + + pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); + } + +} From 885e1b39b21796c73ccc821ce44ee16e0c003fe1 Mon Sep 17 00:00:00 2001 From: iloer Date: Thu, 12 Mar 2020 00:43:39 +0300 Subject: [PATCH 19/26] iloer@mail.ru #102 small refactoring --- ...AzureDevOpsServerPullRequestDecorator.java | 107 +++++++++--------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index 294199843..6d0f971e1 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -5,7 +5,11 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.*; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.Comment; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThread; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusContext; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitPullRequestStatus; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThreadResponse; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers.GitStatusStateMapper; import org.apache.commons.io.IOUtils; @@ -32,7 +36,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.stream.Collectors; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,14 +44,18 @@ public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { - private enum ApiZone { - status, - thread - } private String authorizationHeader; private static final String AZURE_API_VERSION = "?api-version=5.0-preview.1"; private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); + + private String azureUrl = ""; + private String baseBranch = ""; + private String branch = ""; + private String pullRequestId = ""; + private String azureRepositoryName = ""; + private String azureProjectId = ""; + /*private static final List OPEN_ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) .collect(Collectors.toList());*/ @@ -75,34 +82,34 @@ public AzureDevOpsServerPullRequestDecorator(Server server, ScmInfoRepository sc @Override public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { LOGGER.info("starting to analyze with " + analysisDetails.toString()); - String revision = analysisDetails.getCommitSha(); try { - final String azureUrl = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( + azureUrl = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))); - final String baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElseThrow( + baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElseThrow( () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_BASE_BRANCH))); - final String branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElseThrow( + branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElseThrow( () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_BRANCH))); - final String pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElseThrow( + pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElseThrow( () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))); - final String repositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElseThrow( + azureRepositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElseThrow( () -> new IllegalStateException(String.format( - "Could not decorate AzureDevops pullRequest. '%s' has not been set in scanner properties", + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))); - - final String azureProjectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("Not found: PULLREQUEST_AZUREDEVOPS_PROJECT_ID"); - final String sonarBranch = analysisDetails.getBranchName(); + azureProjectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_PROJECT_ID))); if (almSettingDto.getPersonalAccessToken() == null) { - throw new IllegalStateException("Could not decorate AzureDevops pullRequest. Access token has not been set"); + throw new IllegalStateException("Could not decorate AzureDevOps pullRequest. Access token has not been set"); } setAuthorizationHeader(almSettingDto.getPersonalAccessToken()); @@ -111,12 +118,10 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("branch is: %s ", branch)); LOGGER.trace(String.format("pullRequestId is: %s ", pullRequestId)); LOGGER.trace(String.format("azureProjectId is: %s ", azureProjectId)); - LOGGER.trace(String.format("repositoryName is: %s ", repositoryName)); - LOGGER.trace(String.format("revision Commit/revision is: %s ", revision)); - LOGGER.trace(String.format("sonarBranch is: %s ", sonarBranch)); + LOGGER.trace(String.format("azureRepositoryName is: %s ", azureRepositoryName)); sendPost( - getApiUrl(ApiZone.status, analysisDetails), + getStatusApiUrl(), getGitPullRequestStatus(analysisDetails), "Status set successfully" ); @@ -128,7 +133,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin .collect(Collectors.toList());*/ LOGGER.trace(String.format("Analyze issue count: %s ", openIssues.size())); - ArrayList azureCommentThreads = new ArrayList(Arrays.asList(sendGet(getApiUrl(ApiZone.thread, analysisDetails), CommentThreadResponse.class).getValue())); + ArrayList azureCommentThreads = new ArrayList(Arrays.asList(sendGet(getThreadApiUrl(), CommentThreadResponse.class).getValue())); LOGGER.trace(String.format("Azure commentThreads count: %s ", azureCommentThreads.size())); azureCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted); LOGGER.trace(String.format("Azure commentThreads AFTER REMOVE count: %s ", azureCommentThreads.size())); @@ -172,7 +177,6 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin "Comment added success" ); sendPatch( - //getApiUrl(ApiZone.thread, analysisDetails, azureThread.id), azureThread._links.self.href + AZURE_API_VERSION, "{\"status\":\"closed\"}" ); @@ -216,7 +220,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin CommentThread thread = new CommentThread(filePath, locate, message); LOGGER.info(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); sendPost( - getApiUrl(ApiZone.thread, analysisDetails), + getThreadApiUrl(), new ObjectMapper().writeValueAsString(thread), "Thread created successfully" ); @@ -255,30 +259,26 @@ public String getRuleUrlWithRuleKey(String ruleKey) throws IOException { ); } - private String getApiUrl(ApiZone apiZone, AnalysisDetails analysisDetails) throws UnsupportedOperationException { - StringBuilder apiUrl = new StringBuilder(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElse("fail")); //instance - apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElse("fail")); //project - apiUrl.append("/_apis/git/repositories/"); - apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElse("fail")); // repositoryId - apiUrl.append("/pullRequests/"); - apiUrl.append(analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElse("fail")); // pullRequestId - switch (apiZone) { - case status: { - // POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.0-preview.1 - apiUrl.append("/statuses"); - break; - } - case thread: { - //POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/threads?api-version=5.0 - apiUrl.append("/threads"); - break; - } - default: { - throw new UnsupportedOperationException("Not implemented method"); - } - } - apiUrl.append(AZURE_API_VERSION); - return apiUrl.toString(); + private String getStatusApiUrl() { + // POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.0-preview.1 + return azureUrl + azureProjectId + + "/_apis/git/repositories/" + + azureRepositoryName + + "/pullRequests/" + + pullRequestId + + "/statuses" + + AZURE_API_VERSION; + } + + private String getThreadApiUrl(){ + //POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/threads?api-version=5.0 + return azureUrl + azureProjectId + + "/_apis/git/repositories/" + + azureRepositoryName + + "/pullRequests/" + + pullRequestId + + "/threads" + + AZURE_API_VERSION; } private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { @@ -300,9 +300,6 @@ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws I return new ObjectMapper().writeValueAsString(status); } - private void sendPost(String apiUrl, String body) throws IOException { - sendPost(apiUrl, body, "POST success!"); - } private void sendPost(String apiUrl, String body, String successMessage) throws IOException { LOGGER.trace(String.format("sendPost: URL: %s ", apiUrl)); LOGGER.trace(String.format("sendPost: BODY: %s ", body)); From e197ff8e6c50b58056b84cac4a080941a3f523e2 Mon Sep 17 00:00:00 2001 From: iloer Date: Fri, 13 Mar 2020 00:52:42 +0300 Subject: [PATCH 20/26] ileor@mail.ru #102 Fix unit test --- .../PullRequestPostAnalysisTask.java | 2 +- ...eDevOpsServerPullRequestDecoratorTest.java | 123 +++++++++--------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index 24f897ff0..9cf208148 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -197,7 +197,7 @@ public ScannerContext GetScannerContext(String ceTaskId) } catch (Exception ex) { - LOGGER.error("AZURE: ***ERROR***:" + ex); + LOGGER.error("GetScannerContext: " + ex); } return scannerContext; } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java index 9c66a251b..05a291d19 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -8,23 +8,21 @@ import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; import org.sonar.api.platform.Server; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; import org.sonar.core.issue.DefaultIssue; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.protobuf.DbIssues; import java.util.Base64; import java.util.Collections; import java.util.Optional; -import static com.github.tomakehurst.wiremock.client.WireMock.okJson; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.ok; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; @@ -49,7 +47,10 @@ public void decorateQualityGateStatus() { String branchName = "feature/some-feature"; String azureRepository = "myRepository"; String sonarRootUrl = "http://sonar:9000/sonar"; - String filePath = "/path/to/file"; + String filePath = "path/to/file"; + String issueMessage = "issueMessage"; + String issueKeyVal = "issueKeyVal"; + String ruleKeyVal = "ruleKeyVal"; int lineNumber = 5; String token = "token"; String authorizationHeader = "Basic " + Base64.getEncoder().encodeToString((":" + token).getBytes()); @@ -59,6 +60,11 @@ public void decorateQualityGateStatus() { when(almSettingDto.getPersonalAccessToken()).thenReturn(token); AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + DefaultIssue defaultIssue = mock(DefaultIssue.class); + Component component = mock(Component.class); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))) .thenReturn(Optional.of(wireMockRule.baseUrl())); when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))) @@ -71,31 +77,36 @@ public void decorateQualityGateStatus() { .thenReturn(Optional.of(baseBranchName)); when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_BRANCH))) .thenReturn(Optional.of(branchName)); - when(analysisDetails.getAnalysisProjectKey()).thenReturn(sonarProject); when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); when(analysisDetails.getBranchName()).thenReturn(pullRequestId); + when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); + when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); - - PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); - PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - DefaultIssue defaultIssue = mock(DefaultIssue.class); - when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); - when(defaultIssue.getLine()).thenReturn(lineNumber); + DbIssues.Locations locate = DbIssues.Locations.newBuilder().build(); + RuleType rule = RuleType.CODE_SMELL; + RuleKey ruleKey = mock(RuleKey.class); when(componentIssue.getIssue()).thenReturn(defaultIssue); - - Component component = mock(Component.class); when(componentIssue.getComponent()).thenReturn(component); - when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); + when(defaultIssue.getLine()).thenReturn(lineNumber); + when(defaultIssue.getLocations()).thenReturn(locate); + when(defaultIssue.type()).thenReturn(rule); + when(defaultIssue.getMessage()).thenReturn(issueMessage); + when(defaultIssue.getRuleKey()).thenReturn(ruleKey); + when(defaultIssue.key()).thenReturn(issueKeyVal); + when(ruleKey.toString()).thenReturn(ruleKeyVal); ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); wireMockRule.stubFor(get(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/threads?api-version=5.0-preview.1")) .withHeader("Accept", equalTo("application/json")) .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) .withHeader("Authorization", equalTo(authorizationHeader)) - .willReturn(okJson("{\n" + + .willReturn(aResponse() + .withStatus(200) + .withBody( + "{\n" + " \"value\": [\n" + " {\n" + " \"pullRequestThreadContext\": {\n" + @@ -128,7 +139,7 @@ public void decorateQualityGateStatus() { " \"publishedDate\": \"2020-03-10T17:40:09.603Z\",\n" + " \"lastUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + " \"lastContentUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + - " \"isDeleted\": true,\n" + + " \"isDeleted\": false,\n" + " \"commentType\": \"text\",\n" + " \"usersLiked\": [],\n" + " \"_links\": {\n" + @@ -152,7 +163,7 @@ public void decorateQualityGateStatus() { " },\n" + " \"properties\": {},\n" + " \"identities\": null,\n" + - " \"isDeleted\": true,\n" + + " \"isDeleted\": false,\n" + " \"_links\": {\n" + " \"self\": {\n" + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80450\"\n" + @@ -202,7 +213,7 @@ public void decorateQualityGateStatus() { " ],\n" + " \"status\": \"active\",\n" + " \"threadContext\": {\n" + - " \"filePath\": \"" + filePath + "\",\n" + + " \"filePath\": \"/azureProject/myReposytory/Helpers/file2.cs\",\n" + " \"rightFileStart\": {\n" + " \"line\": 30,\n" + " \"offset\": 57\n" + @@ -229,42 +240,36 @@ public void decorateQualityGateStatus() { .withHeader("Accept", equalTo("application/json")) .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) .withHeader("Authorization", equalTo(authorizationHeader)) - .withRequestBody(equalTo("{\n" + - "\t\"status\":\"active\",\n" + - "\t\"comments\":[\n" + - "\t\t{\n" + - "\t\t\t\"content\":\"Message\",\n" + - "\t\t\t\"commentType\":\"text\",\n" + - "\t\t\t\"parentCommentId\":0,\n" + - "\t\t\t\"id\":null,\n" + - "\t\t\t\"threadId\":null,\n" + - "\t\t\t\"author\":null,\n" + - "\t\t\t\"publishedDate\":null,\n" + - "\t\t\t\"lastUpdatedDate\":null,\n" + - "\t\t\t\"lastContentUpdatedDate\":null,\n" + - "\t\t\t\"isDeleted\":null,\n" + - "\t\t\t\"usersLiked\":null,\n" + - "\t\t\t\"links\":null\n" + - "\t\t}\n" + - "\t],\n" + - "\t\"threadContext\":{\n" + - "\t\t\"filePath\":\"" + filePath + "\",\n" + - "\t\t\"leftFileStart\":null,\n" + - "\t\t\"leftFileEnd\":null,\n" + - "\t\t\"rightFileStart\":{\n" + - "\t\t\t\"line\":3,\n" + - "\t\t\t\"offset\":6\n" + - "\t\t},\n" + - "\t\t\"rightFileEnd\":{\n" + - "\t\t\t\"line\":3,\n" + - "\t\t\t\"offset\":10}\n" + - "\t\t},\n" + - "\t\"id\":0,\n" + - "\t\"publishedDate\":null,\n" + - "\t\"lastUpdatedDate\":null,\n" + - "\t\"identities\":null,\n" + - "\t\"isDeleted\":null\n" + - "} ") + .withRequestBody(equalTo("{" + + "\"status\":\"active\"," + + "\"comments\":[{" + + "\"content\":\"CODE_SMELL: issueMessage ([rule](" + sonarRootUrl + "/coding_rules?open=" + ruleKeyVal + "&rule_key=" + ruleKeyVal + "))\\n\\n[See in SonarQube](" + sonarRootUrl + "/project/issues?id=" + sonarProject + "&issues=" + issueKeyVal + "&open=" + issueKeyVal + "&pullRequest="+ pullRequestId +")\"," + + "\"commentType\":\"text\"," + + "\"parentCommentId\":0," + + "\"id\":null," + + "\"threadId\":null," + + "\"author\":null," + + "\"publishedDate\":null," + + "\"lastUpdatedDate\":null," + + "\"lastContentUpdatedDate\":null," + + "\"isDeleted\":null," + + "\"usersLiked\":null," + + "\"links\":null}]," + + "\"threadContext\":{" + + "\"filePath\":\"/" + filePath + "\"," + + "\"leftFileStart\":null," + + "\"leftFileEnd\":null," + + "\"rightFileStart\":{\"line\":0," + + "\"offset\":1}," + + "\"rightFileEnd\":{\"line\":0," + + "\"offset\":1}}," + + "\"id\":0," + + "\"publishedDate\":null," + + "\"lastUpdatedDate\":null," + + "\"identities\":null," + + "\"isDeleted\":null," + + "\"_links\":null" + + "}") ) .willReturn(ok())); From db4f93eb0cc4002e442b0a8656197d6875f1b604 Mon Sep 17 00:00:00 2001 From: iloer Date: Fri, 13 Mar 2020 01:12:57 +0300 Subject: [PATCH 21/26] iloer@mail.ru remove unused code --- .../azuredevops/AzureDevOpsServerPullRequestDecorator.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index 6d0f971e1..b83a91518 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -148,14 +148,10 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("ISSUE: key: %s ", issue.getIssue().key())); LOGGER.trace(String.format("ISSUE: type: %s ", issue.getIssue().type().toString())); LOGGER.trace(String.format("ISSUE: severity: %s ", issue.getIssue().severity())); - LOGGER.trace(String.format("ISSUE: changes size: %s ", issue.getIssue().changes().size())); - LOGGER.trace(String.format("ISSUE: selectedAt: %s ", issue.getIssue().selectedAt())); LOGGER.trace(String.format("ISSUE: componentKey: %s ", issue.getIssue().componentKey())); LOGGER.trace(String.format("ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); LOGGER.trace(String.format("ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); LOGGER.trace(String.format("COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); - LOGGER.trace(String.format("COMPONENT: getFileAttributes: %s ", issue.getComponent().getFileAttributes().toString())); - LOGGER.trace(String.format("COMPONENT: getReportAttributes: %s ", issue.getComponent().getReportAttributes().toString())); boolean isExitsThread = false; for (CommentThread azureThread : azureCommentThreads) { @@ -226,6 +222,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin ); } catch (Exception e) { LOGGER.error(e.toString()); + //throw new IllegalStateException(e); // Uncomment to run unit tests } } } From dc9a1cc345b5f8447e27f28f9c73f54615988d04 Mon Sep 17 00:00:00 2001 From: iloer Date: Fri, 13 Mar 2020 01:23:04 +0300 Subject: [PATCH 22/26] iloer@mail.ru remove star import --- .../ce/pullrequest/PullRequestPostAnalysisTaskTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index 0cbddc6d3..df1bd5d4d 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -28,7 +28,6 @@ import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.config.Configuration; import org.sonar.api.platform.Server; -import org.sonar.ce.task.CeTask; import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; @@ -40,9 +39,13 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDao; import org.sonar.db.alm.setting.ProjectAlmSettingDto; -import org.sonar.db.ce.CeScannerContextDao; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; From 8670a7fa412c9887a6df3d089cf83d9132c5b714 Mon Sep 17 00:00:00 2001 From: iloer Date: Sun, 15 Mar 2020 03:22:56 +0300 Subject: [PATCH 23/26] iloer@mail.ru #102 comment fixes --- .../PullRequestPostAnalysisTask.java | 4 +- ...AzureDevOpsServerPullRequestDecorator.java | 36 +++--- .../azuredevops/model/Comment.java | 77 ++++++++----- .../azuredevops/model/CommentPosition.java | 16 +-- .../azuredevops/model/CommentThread.java | 62 ++++++----- .../model/CommentThreadContext.java | 39 ------- .../model/CommentThreadResponse.java | 2 +- .../model/GitPullRequestStatus.java | 44 ++++++-- .../azuredevops/model/GitStatusContext.java | 28 +++-- .../azuredevops/model/IdentityRef.java | 103 ++++++++++++++---- .../pullrequest/azuredevops/model/Link.java | 14 ++- .../model/PropertiesCollection.java | 5 - .../azuredevops/model/ReferenceLinks.java | 14 ++- .../azuredevops/model/SubjectDescriptor.java | 6 - .../mappers/CommentThreadStatusMapper.java | 6 +- .../model/mappers/GitStatusStateMapper.java | 2 +- ...eDevOpsServerPullRequestDecoratorTest.java | 18 +-- 17 files changed, 291 insertions(+), 185 deletions(-) delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index 9cf208148..e39400e5d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -154,7 +154,7 @@ public void finished(Context context) { ScannerContext scannerContext = projectAnalysis.getScannerContext().getProperties().size() > 0 ? projectAnalysis.getScannerContext() - : GetScannerContext(projectAnalysis.getCeTask().getId()); + : getScannerContext(projectAnalysis.getCeTask().getId()); AnalysisDetails analysisDetails = new AnalysisDetails(new AnalysisDetails.BranchDetails(optionalBranchName.get(), commitId), @@ -169,7 +169,7 @@ public void finished(Context context) { pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } - public ScannerContext GetScannerContext(String ceTaskId) + private ScannerContext getScannerContext(String ceTaskId) { ScannerContext scannerContext = new ScannerContext() { private final Map props = new HashMap(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index b83a91518..c25f937d3 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -135,7 +135,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin ArrayList azureCommentThreads = new ArrayList(Arrays.asList(sendGet(getThreadApiUrl(), CommentThreadResponse.class).getValue())); LOGGER.trace(String.format("Azure commentThreads count: %s ", azureCommentThreads.size())); - azureCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted); + azureCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted()); LOGGER.trace(String.format("Azure commentThreads AFTER REMOVE count: %s ", azureCommentThreads.size())); for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { @@ -152,28 +152,27 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); LOGGER.trace(String.format("ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); LOGGER.trace(String.format("COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); - + DbIssues.Locations locate = Objects.requireNonNull(issue.getIssue().getLocations()); boolean isExitsThread = false; for (CommentThread azureThread : azureCommentThreads) { LOGGER.trace(String.format("azureFilePath: %s", azureThread.getThreadContext().getFilePath())); LOGGER.trace(String.format("filePath: %s (%s)", filePath, azureThread.getThreadContext().getFilePath().equals(filePath))); LOGGER.trace(String.format("azureLine: %d", azureThread.getThreadContext().getRightFileStart().getLine())); - LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine().equals(line))); + LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine())); if (azureThread.getThreadContext().getFilePath().equals(filePath) - && azureThread.getThreadContext().getRightFileStart().getLine().equals(line)) { + && azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine()) { if(!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN) && azureThread.getStatus().equals(CommentThreadStatus.active)) { - azureThread.setStatus(CommentThreadStatus.closed); Comment comment = new Comment("Closed in SonarQube"); LOGGER.info("Issue closed in Sonar. try close in Azure"); sendPost( - azureThread._links.self.href + "/comments" + AZURE_API_VERSION, + azureThread.getLinks().getSelf().getHref() + "/comments" + AZURE_API_VERSION, new ObjectMapper().writeValueAsString(comment), "Comment added success" ); sendPatch( - azureThread._links.self.href + AZURE_API_VERSION, + azureThread.getLinks().getSelf().getHref() + AZURE_API_VERSION, "{\"status\":\"closed\"}" ); } @@ -212,7 +211,6 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin pullRequestId) ); - DbIssues.Locations locate = Objects.requireNonNull(issue.getIssue().getLocations()); CommentThread thread = new CommentThread(filePath, locate, message); LOGGER.info(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); sendPost( @@ -222,7 +220,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin ); } catch (Exception e) { LOGGER.error(e.toString()); - //throw new IllegalStateException(e); // Uncomment to run unit tests + throw new IllegalStateException(e); // Uncomment to run unit tests } } } @@ -283,17 +281,17 @@ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws I final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; final String GIT_STATUS_DESCRIPTION = "SonarQube Gate"; - GitPullRequestStatus status = new GitPullRequestStatus(); - status.state = GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()); - status.description = GIT_STATUS_DESCRIPTION; - status.context = new GitStatusContext(GIT_STATUS_CONTEXT_GENRE, GIT_STATUS_CONTEXT_NAME); // "SonarQube/PullRequestDecoration" - status.targetUrl = String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), - URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), - StandardCharsets.UTF_8.name()), - URLEncoder.encode(analysisDetails.getBranchName(), - StandardCharsets.UTF_8.name()) + GitPullRequestStatus status = new GitPullRequestStatus( + GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()), + GIT_STATUS_DESCRIPTION, + new GitStatusContext(GIT_STATUS_CONTEXT_GENRE, GIT_STATUS_CONTEXT_NAME), // "SonarQube/PullRequestDecoration", + String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), + StandardCharsets.UTF_8.name()), + URLEncoder.encode(analysisDetails.getBranchName(), + StandardCharsets.UTF_8.name()) + ) ); - return new ObjectMapper().writeValueAsString(status); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java index 63953e3d2..41ebc0136 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java @@ -1,5 +1,6 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonProperty; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentType; import java.io.Serializable; @@ -15,80 +16,98 @@ public class Comment implements Serializable { private String content; private CommentType commentType; private Integer parentCommentId; + private int id; + private int threadId; + private IdentityRef author; + private Date publishedDate; + private Date lastUpdatedDate; + private Date lastContentUpdatedDate; + @JsonProperty("isDeleted") + private Boolean deleted; + private List usersLiked; + @JsonProperty("_links") + private ReferenceLinks links; - public Comment(){} + public Comment() { + } - public Comment(String content){ + public Comment(String content) { this.content = content; this.parentCommentId = 0; this.commentType = CommentType.text; } + /** * The ID of the parent comment. This is used for replies. */ - public Integer getParentCommentId(){ + public Integer getParentCommentId() { return this.parentCommentId; - }; + } /** * The comment content. */ - public String getContent(){ + public String getContent() { return this.content; } - /** - * The ID of the parent comment. This is used for replies. - */ - public void setParentCommentId(Integer value){ - this.parentCommentId = value; - } /** * The comment type at the time of creation. */ - public CommentType getCommentType(){ + public CommentType getCommentType() { return this.commentType; - }; - /** - * The comment type at the time of creation. - */ - public void setCommentType(CommentType value){ - this.commentType = value; - }; - + } /** * The comment ID. IDs start at 1 and are unique to a pull request. */ - public Integer id; + public int getId() { + return this.id; + } /** * The parent thread ID. Used for internal server purposes only -- note * that this field is not exposed to the REST client. */ - public Integer threadId; + public int getThreadId() { + return this.threadId; + } /** * The author of the comment. */ - public IdentityRef author; + public IdentityRef getAuthor() { + return this.author; + } /** * The date the comment was first published.; */ - public Date publishedDate; + public Date getPublishedDate() { + return this.publishedDate; + } /** * The date the comment was last updated. */ - public Date lastUpdatedDate; + public Date getLastUpdatedDate() { + return this.lastUpdatedDate; + } /** * The date the comment's content was last updated. */ - public Date lastContentUpdatedDate; + public Date getLastContentUpdatedDate() { + return this.lastContentUpdatedDate; + } /** * Whether or not this comment was soft-deleted. */ - public Boolean isDeleted; + public Boolean isDeleted() { + return this.deleted; + } /** * A list of the users who have liked this comment. */ - public List usersLiked; + public List getUsersLiked() { + return this.usersLiked; + } /** * Links to other related objects. */ - public ReferenceLinks links; + public ReferenceLinks getLinks() { + return this.links; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java index 65896f6a5..ad5a4ef93 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java @@ -1,21 +1,23 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.io.Serializable; public class CommentPosition implements Serializable { - private Integer line; - private Integer offset; - - public CommentPosition() {}; + private final int line; + private final int offset; - public CommentPosition(Integer line, Integer offset){ + @JsonCreator + public CommentPosition(@JsonProperty("line") int line, @JsonProperty("offset") int offset){ this.line = line; this.offset = offset + 1; } /** *The line number of a thread's position. Starts at 1. /// */ - public Integer getLine() + public int getLine() { return this.line; }; @@ -23,7 +25,7 @@ public Integer getLine() /** *The character offset of a thread's position inside of a line. Starts at 0. */ - public Integer getOffset(){ + public int getOffset(){ return this.offset; }; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java index ba89c38d0..7e8a09a36 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -1,5 +1,6 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonProperty; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; import org.sonar.db.protobuf.DbIssues; @@ -18,6 +19,14 @@ public class CommentThread implements Serializable { private CommentThreadStatus status; private List comments; private CommentThreadContext threadContext; + private int id; + private Date publishedDate; + private Date lastUpdatedDate; + private HashMap identities; + @JsonProperty("isDeleted") + private Boolean deleted; + @JsonProperty("_links") + private ReferenceLinks links; public CommentThread(){}; public CommentThread(String filePath, DbIssues.Locations locations, String message){ @@ -37,63 +46,58 @@ public CommentThread(String filePath, DbIssues.Locations locations, String messa public List getComments(){ return this.comments; }; - /** - * A list of the comments. - */ - public void setComments(List value){ - this.comments = value; - }; /** * The status of the comment thread. */ public CommentThreadStatus getStatus(){ return this.status; }; - /** - * The status of the comment thread. - */ - public void setStatus(CommentThreadStatus value){ - this.status = value; - }; /** * Specify thread context such as position in left/right file. */ public CommentThreadContext getThreadContext() { return this.threadContext; }; - /** - * Specify thread context such as position in left/right file. - */ - public void setThreadContext(CommentThreadContext value) { - this.threadContext = value; - }; - /** * The comment thread id. */ - public int id; + public int getId() + { + return this.id; + }; /** * The time this thread was published. */ - public Date publishedDate; + public Date getPublishedDate() + { + return this.publishedDate; + }; /** * The time this thread was last updated. */ - public Date lastUpdatedDate; - /** - * Optional properties associated with the thread as a collection of key-value - */ - private PropertiesCollection properties; + public Date getLastUpdatedDate() + { + return this.lastUpdatedDate; + }; /** * Set of identities related to this thread */ - public HashMap identities; + public HashMap getIdentities() + { + return this.identities; + }; /** * Specify if the thread is deleted which happens when all comments are deleted. */ - public Boolean isDeleted; + public Boolean isDeleted() + { + return this.deleted; + }; /** * Links to other related objects. */ - public ReferenceLinks _links; + public ReferenceLinks getLinks() + { + return this.links; + }; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java index 70c9ab106..dd86626eb 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java @@ -33,67 +33,28 @@ public CommentThreadContext(String filePath, DbIssues.Locations locations){ public String getFilePath(){ return this.filePath; }; - /** - * File path relative to the root of the repository. It's up to the client to - */ - public void setFilePath(String value){ - this.filePath = value; - }; - /** * Position of first character of the thread's span in left file. /// */ public CommentPosition getLeftFileStart(){ return this.leftFileStart; }; - - /** - * Position of first character of the thread's span in left file. /// - */ - public void setLeftFileStart(CommentPosition value) - { - this.leftFileStart = value; - }; - /** * Position of last character of the thread's span in left file. /// */ public CommentPosition getLeftFileEnd(){ return this.leftFileEnd; }; - /** - * Position of last character of the thread's span in left file. /// - */ - public void setLeftFileEnd(CommentPosition value) - { - this.leftFileEnd = value; - }; - /** * Position of first character of the thread's span in right file. /// */ public CommentPosition getRightFileStart(){ return this.rightFileStart; }; - /** - * Position of first character of the thread's span in right file. /// - */ - public void setRightFileStart(CommentPosition value) - { - this.rightFileStart = value; - }; - /** * Position of last character of the thread's span in right file. /// */ public CommentPosition getRightFileEnd(){ return this.rightFileEnd; }; - /** - * Position of last character of the thread's span in right file. /// - */ - public void setRightFileEnd(CommentPosition value) - { - this.rightFileEnd = value; - }; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java index 57d0b118b..0c90ccfaf 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class CommentThreadResponse { - private CommentThread[] value; + private final CommentThread[] value; @JsonCreator public CommentThreadResponse(@JsonProperty("value") CommentThread[] value){ diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java index c7272049b..17562fb69 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java @@ -1,26 +1,54 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; public class GitPullRequestStatus { - /** - * ID of the iteration to associate status with. Minimum value is 1. - */ - public Integer iterationId = null; + final GitStatusState state; + final String description; + final GitStatusContext context; + final String targetUrl; + + @JsonCreator + public GitPullRequestStatus( + @JsonProperty("state") GitStatusState state, + @JsonProperty("description") String description, + @JsonProperty("context") GitStatusContext context, + @JsonProperty("targetUrl") String targetUrl) + { + this.state = state; + this.description = description; + this.context = context; + this.targetUrl = targetUrl; + } + /** * State of the status. */ - public GitStatusState state; + public GitStatusState getState() + { + return this.state; + } /** * Description of the status */ - public String description; + public String getDescription() + { + return this.description; + } /** * Status context that uniquely identifies the status. */ - public GitStatusContext context; + public GitStatusContext getContext() + { + return this.context; + } /** * TargetUrl of the status */ - public String targetUrl; + public String getTargetUrl() + { + return this.targetUrl; + } } \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java index ac73686b9..7be1db5b6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java @@ -1,19 +1,31 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * Status context that uniquely identifies the status. */ public class GitStatusContext { + final String name; + final String genre; + @JsonCreator + public GitStatusContext(@JsonProperty("genre") String genre, @JsonProperty("name") String name){ + this.genre = genre; + this.name = name; + } /** - * Name identifier of the status, cannot be null or empty. + * Genre of the status. Typically name of the service/tool generating the status, can be empty. */ - public final String name; + public String getGenre() + { + return this.genre; + }; /** - * Genre of the status. Typically name of the service/tool generating the status, can be empty. + * Name identifier of the status, cannot be null or empty. */ - public final String genre; - public GitStatusContext(String genre, String name){ - this.genre = genre; - this.name = name; - } + public String getName() + { + return this.name; + }; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java index 4823eacdd..541a35543 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java @@ -1,52 +1,117 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; -public class IdentityRef { - public IdentityRef(){}; - public SubjectDescriptor Descriptor; - public String DisplayName; - public String Url; - public ReferenceLinks Links; - public String Id; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +public class IdentityRef implements Serializable { + private String displayName; + private String url; + @JsonProperty("_links") + private ReferenceLinks links; + private String id; + private String uniqueName; + private String directoryAlias; + private String profileUrl; + private String imageUrl; + @JsonProperty("isContainer") + private Boolean container; + @JsonProperty("isAadIdentity") + private Boolean aadIdentity; + @JsonProperty("isInactive") + private Boolean inactive; + @JsonProperty("isDeletedInOrigin") + private Boolean deletedInOrigin; + private String displayNameForXmlSerialization; + private String urlForXmlSerialization; + + public IdentityRef() + {}; + + public String getDisplayName() + { + return this.displayName; + } + public String getUrl() + { + return this.url; + } + public ReferenceLinks getLinks() + { + return this.links; + } + public String getId() + { + return this.id; + } /** * Deprecated - use Domain+PrincipalName instead */ - public String UniqueName; + public String getUniqueName() + { + return this.uniqueName; + } /** * Deprecated - Can be retrieved by querying the Graph user referenced in the * "self" entry of the IdentityRef "_links" dictionary */ - public String DirectoryAlias; + public String getDirectoryAlias() + { + return this.directoryAlias; + } /** * Deprecated - not in use in most preexisting implementations of ToIdentityRef */ - public String ProfileUrl; + public String getProfileUrl() + { + return this.profileUrl; + } /** * Deprecated - Available in the "avatar" entry of the IdentityRef "_links" dictionary */ - public String ImageUrl; + public String getImageUrl() + { + return this.imageUrl; + } /** * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsGroupType) */ - public Boolean IsContainer; + public Boolean isContainer() + { + return this.container; + } /** * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsAadUserType/Descriptor.IsAadGroupType) */ - public Boolean IsAadIdentity; + public Boolean isAadIdentity() + { + return this.aadIdentity; + } /** * Deprecated - Can be retrieved by querying the Graph membership state referenced * in the "membershipState" entry of the GraphUser "_links" dictionary */ - public Boolean Inactive; - public Boolean IsDeletedInOrigin; - + public Boolean getInactive() + { + return this.inactive; + } + public Boolean getIsDeletedInOrigin() + { + return this.deletedInOrigin; + } /** * This property is for xml compat only. */ - public String DisplayNameForXmlSerialization; - + public String getdisplayNameForXmlSerialization() + { + return this.displayNameForXmlSerialization; + } /** * This property is for xml compat only. */ - public String UrlForXmlSerialization; + public String getUrlForXmlSerialization() + { + return this.urlForXmlSerialization; + } } \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java index 69a2c5b8e..091086c97 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java @@ -1,5 +1,17 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class Link { - public String href; + final String href; + @JsonCreator + public Link(@JsonProperty("href") String href) + { + this.href = href; + }; + public String getHref() + { + return this.href; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java deleted file mode 100644 index 544d42df4..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/PropertiesCollection.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; - -public class PropertiesCollection { - public PropertiesCollection(){}; -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java index 0030aef33..23c2c72c5 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java @@ -1,5 +1,17 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class ReferenceLinks { - public Link self; + final Link self; + @JsonCreator + public ReferenceLinks(@JsonProperty("self") Link self) + { + this.self = self; + } + public Link getSelf() + { + return this.self; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java deleted file mode 100644 index bf2d177a1..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/SubjectDescriptor.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; - -public class SubjectDescriptor { - public String SubjectType; - public String Identifier; -} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java index 88582c5c4..1c2234e21 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -1,12 +1,12 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; -import org.sonar.api.issue.Issue; -import static org.sonar.api.issue.Issue.*; +import static org.sonar.api.issue.Issue.STATUS_OPEN; +import static org.sonar.api.issue.Issue.STATUS_CLOSED; -public class CommentThreadStatusMapper { +public final class CommentThreadStatusMapper { private CommentThreadStatusMapper() { super(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java index b73dd47a2..bda46f26a 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java @@ -3,7 +3,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; import org.sonar.api.ce.posttask.QualityGate; -public class GitStatusStateMapper { +public final class GitStatusStateMapper { private GitStatusStateMapper() { super(); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java index 05a291d19..1b745ab9a 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -22,7 +22,12 @@ import java.util.Optional; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; @@ -246,15 +251,15 @@ public void decorateQualityGateStatus() { "\"content\":\"CODE_SMELL: issueMessage ([rule](" + sonarRootUrl + "/coding_rules?open=" + ruleKeyVal + "&rule_key=" + ruleKeyVal + "))\\n\\n[See in SonarQube](" + sonarRootUrl + "/project/issues?id=" + sonarProject + "&issues=" + issueKeyVal + "&open=" + issueKeyVal + "&pullRequest="+ pullRequestId +")\"," + "\"commentType\":\"text\"," + "\"parentCommentId\":0," + - "\"id\":null," + - "\"threadId\":null," + + "\"id\":0," + + "\"threadId\":0," + "\"author\":null," + "\"publishedDate\":null," + "\"lastUpdatedDate\":null," + "\"lastContentUpdatedDate\":null," + - "\"isDeleted\":null," + "\"usersLiked\":null," + - "\"links\":null}]," + + "\"isDeleted\":null," + + "\"_links\":null}]," + "\"threadContext\":{" + "\"filePath\":\"/" + filePath + "\"," + "\"leftFileStart\":null," + @@ -279,10 +284,9 @@ public void decorateQualityGateStatus() { .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) .withHeader("Authorization", equalTo(authorizationHeader)) .withRequestBody(equalTo("{" + - "\"iterationId\":null," + "\"state\":\"Succeeded\"," + "\"description\":\"SonarQube Gate\"," + - "\"context\":{\"name\":\"PullRequestDecoration\",\"genre\":\"SonarQube\"}," + + "\"context\":{\"genre\":\"SonarQube\",\"name\":\"PullRequestDecoration\"}," + "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" + "}") ) From 7c6d8a1839719be292cfe8d884469e84ea9a7734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=B2=D1=81=D0=BA=D0=B8=D0=B9?= Date: Sun, 1 Mar 2020 17:56:25 +0300 Subject: [PATCH 24/26] iloer@mail.ru --- ...munityReportAnalysisComponentProvider.java | 4 +- .../PullRequestPostAnalysisTask.java | 46 ++- ...AzureDevOpsServerPullRequestDecorator.java | 384 ++++++++++++++++++ .../azuredevops/model/Comment.java | 113 ++++++ .../azuredevops/model/CommentPosition.java | 31 ++ .../azuredevops/model/CommentThread.java | 103 +++++ .../model/CommentThreadContext.java | 60 +++ .../model/CommentThreadResponse.java | 16 + .../model/GitPullRequestStatus.java | 54 +++ .../azuredevops/model/GitStatusContext.java | 31 ++ .../azuredevops/model/IdentityRef.java | 117 ++++++ .../pullrequest/azuredevops/model/Link.java | 17 + .../azuredevops/model/ReferenceLinks.java | 17 + .../model/enums/CommentThreadStatus.java | 36 ++ .../azuredevops/model/enums/CommentType.java | 20 + .../model/enums/GitStatusState.java | 32 ++ .../mappers/CommentThreadStatusMapper.java | 31 ++ .../model/mappers/GitStatusStateMapper.java | 22 + ...tyReportAnalysisComponentProviderTest.java | 2 +- .../PullRequestPostAnalysisTaskTest.java | 13 +- ...eDevOpsServerPullRequestDecoratorTest.java | 303 ++++++++++++++ 21 files changed, 1447 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java index 104bf8b54..89632b696 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java @@ -20,6 +20,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestPostAnalysisTask; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.AzureDevOpsServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.RestApplicationAuthenticationProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4.GraphqlCheckRunProvider; @@ -40,7 +41,8 @@ public List getComponents() { return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class, PostAnalysisIssueVisitor.class, GithubPullRequestDecorator.class, GraphqlCheckRunProvider.class, RestApplicationAuthenticationProvider.class, - BitbucketServerPullRequestDecorator.class, GitlabServerPullRequestDecorator.class); + BitbucketServerPullRequestDecorator.class, GitlabServerPullRequestDecorator.class, + AzureDevOpsServerPullRequestDecorator.class); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index b559adf50..e39400e5d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -18,6 +18,7 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.api.ce.posttask.Analysis; import org.sonar.api.ce.posttask.Branch; import org.sonar.api.ce.posttask.PostProjectAnalysisTask; @@ -36,8 +37,12 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask { @@ -108,7 +113,6 @@ public void finished(Context context) { projectAlmSettingDto = optionalProjectAlmSettingDto.get(); String almSettingUuid = projectAlmSettingDto.getAlmSettingUuid(); optionalAlmSettingDto = dbClient.almSettingDao().selectByUuid(dbSession, almSettingUuid); - } if (!optionalAlmSettingDto.isPresent()) { @@ -148,19 +152,57 @@ public void finished(Context context) { String commitId = revision.get(); + ScannerContext scannerContext = projectAnalysis.getScannerContext().getProperties().size() > 0 + ? projectAnalysis.getScannerContext() + : getScannerContext(projectAnalysis.getCeTask().getId()); + AnalysisDetails analysisDetails = new AnalysisDetails(new AnalysisDetails.BranchDetails(optionalBranchName.get(), commitId), postAnalysisIssueVisitor, qualityGate, new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder), analysis, projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), - projectAnalysis.getScannerContext()); + scannerContext); PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get(); LOGGER.info("using pull request decorator" + pullRequestDecorator.name()); pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } + private ScannerContext getScannerContext(String ceTaskId) + { + ScannerContext scannerContext = new ScannerContext() { + private final Map props = new HashMap(); + @Override + public Map getProperties() { return this.props; } + }; + + try (DbSession dbSession = dbClient.openSession(false)) { + + try { + String scannerContextStr = dbClient.ceScannerContextDao().selectScannerContext(dbSession, ceTaskId).orElse(""); + + LOGGER.trace("GetScannerContext: ScannerContextStr =" + scannerContextStr); + + Pattern p = Pattern.compile("(\\w.+)=(.+)"); + Matcher m = p.matcher(scannerContextStr); + HashMap map = new HashMap(); + + while (m.find()) { + map.put(m.group(1), m.group(2)); + } + LOGGER.trace("GetScannerContext: map =" + map); + + scannerContext.getProperties().putAll(map); + + } catch (Exception ex) + { + LOGGER.error("GetScannerContext: " + ex); + } + return scannerContext; + } + } + private static Optional findCurrentPullRequestStatusDecorator( AlmSettingDto almSetting, List pullRequestDecorators) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java new file mode 100644 index 000000000..c25f937d3 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -0,0 +1,384 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.Comment; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThread; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitStatusContext; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.GitPullRequestStatus; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.CommentThreadResponse; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers.GitStatusStateMapper; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.sonar.api.issue.Issue; +import org.sonar.api.platform.Server; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; +import org.sonar.db.alm.setting.ALM; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.protobuf.DbIssues; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + + +public class AzureDevOpsServerPullRequestDecorator implements PullRequestBuildStatusDecorator { + + private String authorizationHeader; + private static final String AZURE_API_VERSION = "?api-version=5.0-preview.1"; + private static final Logger LOGGER = Loggers.get(AzureDevOpsServerPullRequestDecorator.class); + + private String azureUrl = ""; + private String baseBranch = ""; + private String branch = ""; + private String pullRequestId = ""; + private String azureRepositoryName = ""; + private String azureProjectId = ""; + + /*private static final List OPEN_ISSUE_STATUSES = + Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) + .collect(Collectors.toList());*/ + // SCANNER PROPERTY + public static final String PULLREQUEST_AZUREDEVOPS_INSTANCE_URL = "sonar.pullrequest.vsts.instanceUrl"; // sonar.pullrequest.vsts.instanceUrl=https://dev.azure.com/fabrikam/ + public static final String PULLREQUEST_AZUREDEVOPS_BASE_BRANCH = "sonar.pullrequest.base"; // sonar.pullrequest.base=master + public static final String PULLREQUEST_AZUREDEVOPS_BRANCH = "sonar.pullrequest.branch"; // sonar.pullrequest.branch=feature/some-feature + public static final String PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID = "sonar.pullrequest.key"; // sonar.pullrequest.key=222 + public static final String PULLREQUEST_AZUREDEVOPS_PROJECT_ID = "sonar.pullrequest.vsts.project"; // sonar.pullrequest.vsts.project=MyProject + public static final String PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME = "sonar.pullrequest.vsts.repository";//sonar.pullrequest.vsts.repository=MyReposytory + // SONAR URL MASK + public static final String SONAR_ISSUE_URL_MASK = "%s/project/issues?id=%s&issues=%s&open=%s&pullRequest=%s"; //http://localhost/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + public static final String SONAR_RULE_URL_MASK = "%s/coding_rules?open=%s&rule_key=%s"; //http://localhost/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 + + private final Server server; + private final ScmInfoRepository scmInfoRepository; + + public AzureDevOpsServerPullRequestDecorator(Server server, ScmInfoRepository scmInfoRepository) { + super(); + this.server = server; + this.scmInfoRepository = scmInfoRepository; + } + + @Override + public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + LOGGER.info("starting to analyze with " + analysisDetails.toString()); + + try { + azureUrl = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_INSTANCE_URL).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))); + baseBranch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BASE_BRANCH).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_BASE_BRANCH))); + branch = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_BRANCH).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_BRANCH))); + pullRequestId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))); + azureRepositoryName = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))); + azureProjectId = analysisDetails.getScannerProperty(PULLREQUEST_AZUREDEVOPS_PROJECT_ID).orElseThrow( + () -> new IllegalStateException(String.format( + "Could not decorate AzureDevOps pullRequest. '%s' has not been set in scanner properties", + PULLREQUEST_AZUREDEVOPS_PROJECT_ID))); + if (almSettingDto.getPersonalAccessToken() == null) { + throw new IllegalStateException("Could not decorate AzureDevOps pullRequest. Access token has not been set"); + } + setAuthorizationHeader(almSettingDto.getPersonalAccessToken()); + + LOGGER.trace(String.format("azureUrl is: %s ", azureUrl)); + LOGGER.trace(String.format("baseBranch is: %s ", baseBranch)); + LOGGER.trace(String.format("branch is: %s ", branch)); + LOGGER.trace(String.format("pullRequestId is: %s ", pullRequestId)); + LOGGER.trace(String.format("azureProjectId is: %s ", azureProjectId)); + LOGGER.trace(String.format("azureRepositoryName is: %s ", azureRepositoryName)); + + sendPost( + getStatusApiUrl(), + getGitPullRequestStatus(analysisDetails), + "Status set successfully" + ); + + List openIssues = analysisDetails.getPostAnalysisIssueVisitor() + .getIssues(); + /*.stream() + .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())) + .collect(Collectors.toList());*/ + LOGGER.trace(String.format("Analyze issue count: %s ", openIssues.size())); + + ArrayList azureCommentThreads = new ArrayList(Arrays.asList(sendGet(getThreadApiUrl(), CommentThreadResponse.class).getValue())); + LOGGER.trace(String.format("Azure commentThreads count: %s ", azureCommentThreads.size())); + azureCommentThreads.removeIf(x -> x.getThreadContext() == null || x.isDeleted()); + LOGGER.trace(String.format("Azure commentThreads AFTER REMOVE count: %s ", azureCommentThreads.size())); + + for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { + String filePath = analysisDetails.getSCMPathForIssue(issue).orElse(null); + Integer line = issue.getIssue().getLine(); + if (filePath != null && line != null) { + try { + filePath = "/" + filePath; + LOGGER.trace(String.format("ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); + LOGGER.trace(String.format("ISSUE: key: %s ", issue.getIssue().key())); + LOGGER.trace(String.format("ISSUE: type: %s ", issue.getIssue().type().toString())); + LOGGER.trace(String.format("ISSUE: severity: %s ", issue.getIssue().severity())); + LOGGER.trace(String.format("ISSUE: componentKey: %s ", issue.getIssue().componentKey())); + LOGGER.trace(String.format("ISSUE: getLocations: %s ", Objects.requireNonNull(issue.getIssue().getLocations()).toString())); + LOGGER.trace(String.format("ISSUE: getRuleKey: %s ", issue.getIssue().getRuleKey())); + LOGGER.trace(String.format("COMPONENT: getDescription: %s ", issue.getComponent().getDescription())); + DbIssues.Locations locate = Objects.requireNonNull(issue.getIssue().getLocations()); + boolean isExitsThread = false; + for (CommentThread azureThread : azureCommentThreads) { + LOGGER.trace(String.format("azureFilePath: %s", azureThread.getThreadContext().getFilePath())); + LOGGER.trace(String.format("filePath: %s (%s)", filePath, azureThread.getThreadContext().getFilePath().equals(filePath))); + LOGGER.trace(String.format("azureLine: %d", azureThread.getThreadContext().getRightFileStart().getLine())); + LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine())); + if (azureThread.getThreadContext().getFilePath().equals(filePath) + && azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine()) { + + if(!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN) + && azureThread.getStatus().equals(CommentThreadStatus.active)) { + Comment comment = new Comment("Closed in SonarQube"); + LOGGER.info("Issue closed in Sonar. try close in Azure"); + sendPost( + azureThread.getLinks().getSelf().getHref() + "/comments" + AZURE_API_VERSION, + new ObjectMapper().writeValueAsString(comment), + "Comment added success" + ); + sendPatch( + azureThread.getLinks().getSelf().getHref() + AZURE_API_VERSION, + "{\"status\":\"closed\"}" + ); + } + isExitsThread = true; + break; + } + } + if (!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN)) { + LOGGER.info(String.format("SKIPPED ISSUE: Issue status is %s", issue.getIssue().getStatus())); + continue; + } + + if (isExitsThread || !issue.getIssue().getStatus().equals(Issue.STATUS_OPEN)) { + LOGGER.info(String.format("SKIPPED ISSUE: %s" + + System.lineSeparator() + + "File: %s" + + System.lineSeparator() + + "Line: %d" + + System.lineSeparator() + + "Issue is already exist in azure", + issue.getIssue().getMessage(), + filePath, + line)); + continue; + } + + String message = String.format("%s: %s ([rule](%s))" + System.lineSeparator() + + System.lineSeparator() + + "[See in SonarQube](%s)", + issue.getIssue().type().name(), + issue.getIssue().getMessage(), + getRuleUrlWithRuleKey(issue.getIssue().getRuleKey().toString()), + getIssueUrl( + analysisDetails.getAnalysisProjectKey(), + issue.getIssue().key(), + pullRequestId) + ); + + CommentThread thread = new CommentThread(filePath, locate, message); + LOGGER.info(String.format("Creating thread: %s ", new ObjectMapper().writeValueAsString(thread))); + sendPost( + getThreadApiUrl(), + new ObjectMapper().writeValueAsString(thread), + "Thread created successfully" + ); + } catch (Exception e) { + LOGGER.error(e.toString()); + throw new IllegalStateException(e); // Uncomment to run unit tests + } + } + } + } catch (IOException ex) { + throw new IllegalStateException("Could not decorate Pull Request on AzureDevOps Server", ex); + } + } + + private void setAuthorizationHeader(String apiToken) { + String encodeBytes = Base64.getEncoder().encodeToString((":" + apiToken).getBytes()); + authorizationHeader = "Basic " + encodeBytes; + } + + public String getIssueUrl(String sonarProjectKey, String issueKey, String pullRequestId) throws IOException { + //ISSUE http://localhost/project/issues?id=ProjId&issues=AXCuh6CgT2BpyN1RPU03&open=AXCuh6CgT2BpyN1RPU03&pullRequest=8513 + return String.format(SONAR_ISSUE_URL_MASK, + server.getPublicRootUrl(), + URLEncoder.encode(sonarProjectKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(issueKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(pullRequestId, StandardCharsets.UTF_8.name()) + ); + } + + public String getRuleUrlWithRuleKey(String ruleKey) throws IOException { + //RULE http://localhost/coding_rules?open=csharpsquid%3AS1135&rule_key=csharpsquid%3AS1135 + return String.format(SONAR_RULE_URL_MASK, + server.getPublicRootUrl(), + URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()), + URLEncoder.encode(ruleKey, StandardCharsets.UTF_8.name()) + ); + } + + private String getStatusApiUrl() { + // POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/statuses?api-version=5.0-preview.1 + return azureUrl + azureProjectId + + "/_apis/git/repositories/" + + azureRepositoryName + + "/pullRequests/" + + pullRequestId + + "/statuses" + + AZURE_API_VERSION; + } + + private String getThreadApiUrl(){ + //POST https://{instance}/{collection}/{project}/_apis/git/repositories/{repositoryId}/pullRequests/{pullRequestId}/threads?api-version=5.0 + return azureUrl + azureProjectId + + "/_apis/git/repositories/" + + azureRepositoryName + + "/pullRequests/" + + pullRequestId + + "/threads" + + AZURE_API_VERSION; + } + + private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { + final String GIT_STATUS_CONTEXT_GENRE = "SonarQube"; + final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; + final String GIT_STATUS_DESCRIPTION = "SonarQube Gate"; + + GitPullRequestStatus status = new GitPullRequestStatus( + GitStatusStateMapper.toGitStatusState(analysisDetails.getQualityGateStatus()), + GIT_STATUS_DESCRIPTION, + new GitStatusContext(GIT_STATUS_CONTEXT_GENRE, GIT_STATUS_CONTEXT_NAME), // "SonarQube/PullRequestDecoration", + String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), + StandardCharsets.UTF_8.name()), + URLEncoder.encode(analysisDetails.getBranchName(), + StandardCharsets.UTF_8.name()) + ) + ); + return new ObjectMapper().writeValueAsString(status); + } + + private void sendPost(String apiUrl, String body, String successMessage) throws IOException { + LOGGER.trace(String.format("sendPost: URL: %s ", apiUrl)); + LOGGER.trace(String.format("sendPost: BODY: %s ", body)); + HttpPost httpPost = new HttpPost(apiUrl); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/json; charset=utf-8"); + httpPost.addHeader("Authorization", authorizationHeader); + StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8); + httpPost.setEntity(entity); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPost); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error("sendPost: " + httpResponse.toString()); + LOGGER.error("sendPost: " + EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug("sendPost: " + httpResponse.toString()); + LOGGER.info("sendPost: " + successMessage); + } + } + } + + private T sendGet(String apiUrl, Class type) throws IOException { + LOGGER.info(String.format("sendGet: URL: %s ", apiUrl)); + HttpGet httpGet = new HttpGet(apiUrl); + httpGet.addHeader("Accept", "application/json"); + httpGet.addHeader("Content-Type", "application/json; charset=utf-8"); + httpGet.addHeader("Authorization", authorizationHeader); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpGet); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error(httpResponse.toString()); + LOGGER.error(EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + //LOGGER.info(httpResponse.toString()); + HttpEntity entity = httpResponse.getEntity(); + T obj = new ObjectMapper() + .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8), type); + + LOGGER.info(type + " received"); + + return obj; + } else { + throw new IOException("No response reveived"); + } + } + } + + private void sendPatch(String apiUrl, String body) throws IOException { + LOGGER.trace(String.format("sendPatch: URL: %s ", apiUrl)); + LOGGER.trace(String.format("sendPatch: BODY: %s ", body)); + HttpPatch httpPatch = new HttpPatch(apiUrl); + httpPatch.addHeader("Accept", "application/json"); + httpPatch.addHeader("Content-Type", "application/json; charset=utf-8"); + httpPatch.addHeader("Authorization", authorizationHeader); + StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8); + httpPatch.setEntity(entity); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpResponse httpResponse = httpClient.execute(httpPatch); + if (null != httpResponse && httpResponse.getStatusLine().getStatusCode() != 200) { + LOGGER.error("sendPatch: " + httpResponse.toString()); + LOGGER.error("sendPatch: " + EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8)); + throw new IllegalStateException("An error was returned in the response from the Azure DevOps server. See the previous log messages for details"); + } else if (null != httpResponse) { + LOGGER.debug("sendPatch: " + httpResponse.toString()); + LOGGER.info("sendPatch: Patch success!"); + } + } + } + + @Override + public String name() { + return "Azure"; + } + + @Override + public ALM alm() { + return ALM.AZURE_DEVOPS; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java new file mode 100644 index 000000000..41ebc0136 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java @@ -0,0 +1,113 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentType; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + + +/** + * Represents a comment which is one of potentially many in a comment thread. + */ +public class Comment implements Serializable { + + private String content; + private CommentType commentType; + private Integer parentCommentId; + private int id; + private int threadId; + private IdentityRef author; + private Date publishedDate; + private Date lastUpdatedDate; + private Date lastContentUpdatedDate; + @JsonProperty("isDeleted") + private Boolean deleted; + private List usersLiked; + @JsonProperty("_links") + private ReferenceLinks links; + + public Comment() { + } + + public Comment(String content) { + this.content = content; + this.parentCommentId = 0; + this.commentType = CommentType.text; + } + + /** + * The ID of the parent comment. This is used for replies. + */ + public Integer getParentCommentId() { + return this.parentCommentId; + } + /** + * The comment content. + */ + public String getContent() { + return this.content; + } + /** + * The comment type at the time of creation. + */ + public CommentType getCommentType() { + return this.commentType; + } + /** + * The comment ID. IDs start at 1 and are unique to a pull request. + */ + public int getId() { + return this.id; + } + /** + * The parent thread ID. Used for internal server purposes only -- note + * that this field is not exposed to the REST client. + */ + public int getThreadId() { + return this.threadId; + } + /** + * The author of the comment. + */ + public IdentityRef getAuthor() { + return this.author; + } + /** + * The date the comment was first published.; + */ + public Date getPublishedDate() { + return this.publishedDate; + } + /** + * The date the comment was last updated. + */ + public Date getLastUpdatedDate() { + return this.lastUpdatedDate; + } + /** + * The date the comment's content was last updated. + */ + public Date getLastContentUpdatedDate() { + return this.lastContentUpdatedDate; + } + /** + * Whether or not this comment was soft-deleted. + */ + public Boolean isDeleted() { + return this.deleted; + } + /** + * A list of the users who have liked this comment. + */ + public List getUsersLiked() { + return this.usersLiked; + } + /** + * Links to other related objects. + */ + public ReferenceLinks getLinks() { + return this.links; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java new file mode 100644 index 000000000..ad5a4ef93 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentPosition.java @@ -0,0 +1,31 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +public class CommentPosition implements Serializable { + private final int line; + private final int offset; + + @JsonCreator + public CommentPosition(@JsonProperty("line") int line, @JsonProperty("offset") int offset){ + this.line = line; + this.offset = offset + 1; + } + /** + *The line number of a thread's position. Starts at 1. /// + */ + public int getLine() + { + return this.line; + }; + + /** + *The character offset of a thread's position inside of a line. Starts at 0. + */ + public int getOffset(){ + return this.offset; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java new file mode 100644 index 000000000..7e8a09a36 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -0,0 +1,103 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; +import org.sonar.db.protobuf.DbIssues; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +/** + * Represents a comment thread of a pull request. A thread contains meta data about the file + * it was left on along with one or more comments (an initial comment and the subsequent replies). + */ +public class CommentThread implements Serializable { + + private CommentThreadStatus status; + private List comments; + private CommentThreadContext threadContext; + private int id; + private Date publishedDate; + private Date lastUpdatedDate; + private HashMap identities; + @JsonProperty("isDeleted") + private Boolean deleted; + @JsonProperty("_links") + private ReferenceLinks links; + public CommentThread(){}; + + public CommentThread(String filePath, DbIssues.Locations locations, String message){ + comments = Arrays.asList( + new Comment(message) + ); + status = CommentThreadStatus.active; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); + threadContext = new CommentThreadContext( + filePath, + locations + ); + + } + /** + * A list of the comments. + */ + public List getComments(){ + return this.comments; + }; + /** + * The status of the comment thread. + */ + public CommentThreadStatus getStatus(){ + return this.status; + }; + /** + * Specify thread context such as position in left/right file. + */ + public CommentThreadContext getThreadContext() { + return this.threadContext; + }; + /** + * The comment thread id. + */ + public int getId() + { + return this.id; + }; + /** + * The time this thread was published. + */ + public Date getPublishedDate() + { + return this.publishedDate; + }; + /** + * The time this thread was last updated. + */ + public Date getLastUpdatedDate() + { + return this.lastUpdatedDate; + }; + /** + * Set of identities related to this thread + */ + public HashMap getIdentities() + { + return this.identities; + }; + /** + * Specify if the thread is deleted which happens when all comments are deleted. + */ + public Boolean isDeleted() + { + return this.deleted; + }; + /** + * Links to other related objects. + */ + public ReferenceLinks getLinks() + { + return this.links; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java new file mode 100644 index 000000000..dd86626eb --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadContext.java @@ -0,0 +1,60 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import org.sonar.db.protobuf.DbIssues; + +import java.io.Serializable; + +public class CommentThreadContext implements Serializable { + + private String filePath; + private CommentPosition leftFileStart; + private CommentPosition leftFileEnd; + private CommentPosition rightFileStart; + private CommentPosition rightFileEnd; + + public CommentThreadContext() {}; + + public CommentThreadContext(String filePath, DbIssues.Locations locations){ + this.filePath = filePath; + this.leftFileEnd = null; + this.leftFileStart = null; + this.rightFileEnd = new CommentPosition( + locations.getTextRange().getEndLine(), + locations.getTextRange().getEndOffset() + ); + this.rightFileStart = new CommentPosition( + locations.getTextRange().getStartLine(), + locations.getTextRange().getStartOffset() + ); + } + /** + * File path relative to the root of the repository. It's up to the client to + */ + public String getFilePath(){ + return this.filePath; + }; + /** + * Position of first character of the thread's span in left file. /// + */ + public CommentPosition getLeftFileStart(){ + return this.leftFileStart; + }; + /** + * Position of last character of the thread's span in left file. /// + */ + public CommentPosition getLeftFileEnd(){ + return this.leftFileEnd; + }; + /** + * Position of first character of the thread's span in right file. /// + */ + public CommentPosition getRightFileStart(){ + return this.rightFileStart; + }; + /** + * Position of last character of the thread's span in right file. /// + */ + public CommentPosition getRightFileEnd(){ + return this.rightFileEnd; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java new file mode 100644 index 000000000..0c90ccfaf --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThreadResponse.java @@ -0,0 +1,16 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CommentThreadResponse { + private final CommentThread[] value; + + @JsonCreator + public CommentThreadResponse(@JsonProperty("value") CommentThread[] value){ + this.value = value; + } + public CommentThread[] getValue(){ + return this.value; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java new file mode 100644 index 000000000..17562fb69 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitPullRequestStatus.java @@ -0,0 +1,54 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; + +public class GitPullRequestStatus { + final GitStatusState state; + final String description; + final GitStatusContext context; + final String targetUrl; + + @JsonCreator + public GitPullRequestStatus( + @JsonProperty("state") GitStatusState state, + @JsonProperty("description") String description, + @JsonProperty("context") GitStatusContext context, + @JsonProperty("targetUrl") String targetUrl) + { + this.state = state; + this.description = description; + this.context = context; + this.targetUrl = targetUrl; + } + + /** + * State of the status. + */ + public GitStatusState getState() + { + return this.state; + } + /** + * Description of the status + */ + public String getDescription() + { + return this.description; + } + /** + * Status context that uniquely identifies the status. + */ + public GitStatusContext getContext() + { + return this.context; + } + /** + * TargetUrl of the status + */ + public String getTargetUrl() + { + return this.targetUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java new file mode 100644 index 000000000..7be1db5b6 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/GitStatusContext.java @@ -0,0 +1,31 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Status context that uniquely identifies the status. + */ +public class GitStatusContext { + final String name; + final String genre; + @JsonCreator + public GitStatusContext(@JsonProperty("genre") String genre, @JsonProperty("name") String name){ + this.genre = genre; + this.name = name; + } + /** + * Genre of the status. Typically name of the service/tool generating the status, can be empty. + */ + public String getGenre() + { + return this.genre; + }; + /** + * Name identifier of the status, cannot be null or empty. + */ + public String getName() + { + return this.name; + }; +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java new file mode 100644 index 000000000..541a35543 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/IdentityRef.java @@ -0,0 +1,117 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +public class IdentityRef implements Serializable { + private String displayName; + private String url; + @JsonProperty("_links") + private ReferenceLinks links; + private String id; + private String uniqueName; + private String directoryAlias; + private String profileUrl; + private String imageUrl; + @JsonProperty("isContainer") + private Boolean container; + @JsonProperty("isAadIdentity") + private Boolean aadIdentity; + @JsonProperty("isInactive") + private Boolean inactive; + @JsonProperty("isDeletedInOrigin") + private Boolean deletedInOrigin; + private String displayNameForXmlSerialization; + private String urlForXmlSerialization; + + public IdentityRef() + {}; + + public String getDisplayName() + { + return this.displayName; + } + public String getUrl() + { + return this.url; + } + public ReferenceLinks getLinks() + { + return this.links; + } + public String getId() + { + return this.id; + } + /** + * Deprecated - use Domain+PrincipalName instead + */ + public String getUniqueName() + { + return this.uniqueName; + } + /** + * Deprecated - Can be retrieved by querying the Graph user referenced in the + * "self" entry of the IdentityRef "_links" dictionary + */ + public String getDirectoryAlias() + { + return this.directoryAlias; + } + /** + * Deprecated - not in use in most preexisting implementations of ToIdentityRef + */ + public String getProfileUrl() + { + return this.profileUrl; + } + /** + * Deprecated - Available in the "avatar" entry of the IdentityRef "_links" dictionary + */ + public String getImageUrl() + { + return this.imageUrl; + } + /** + * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsGroupType) + */ + public Boolean isContainer() + { + return this.container; + } + + /** + * Deprecated - Can be inferred from the subject type of the descriptor (Descriptor.IsAadUserType/Descriptor.IsAadGroupType) + */ + public Boolean isAadIdentity() + { + return this.aadIdentity; + } + /** + * Deprecated - Can be retrieved by querying the Graph membership state referenced + * in the "membershipState" entry of the GraphUser "_links" dictionary + */ + public Boolean getInactive() + { + return this.inactive; + } + public Boolean getIsDeletedInOrigin() + { + return this.deletedInOrigin; + } + /** + * This property is for xml compat only. + */ + public String getdisplayNameForXmlSerialization() + { + return this.displayNameForXmlSerialization; + } + /** + * This property is for xml compat only. + */ + public String getUrlForXmlSerialization() + { + return this.urlForXmlSerialization; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java new file mode 100644 index 000000000..091086c97 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Link.java @@ -0,0 +1,17 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Link { + final String href; + @JsonCreator + public Link(@JsonProperty("href") String href) + { + this.href = href; + }; + public String getHref() + { + return this.href; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java new file mode 100644 index 000000000..23c2c72c5 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/ReferenceLinks.java @@ -0,0 +1,17 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ReferenceLinks { + final Link self; + @JsonCreator + public ReferenceLinks(@JsonProperty("self") Link self) + { + this.self = self; + } + public Link getSelf() + { + return this.self; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java new file mode 100644 index 000000000..dfff1376f --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java @@ -0,0 +1,36 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; + +/** + * The status of a comment thread + */ +public enum CommentThreadStatus { + /** + * The thread status is unknown. + */ + unknown, + /** + * The thread status is active. + */ + active, + /** + * The thread status is resolved as fixed. + */ + fixed, + /** + * The thread status is resolved as won't fix. + */ + wontFix, + /** + * The thread status is closed. + */ + closed, + /** + * The thread status is resolved as by design. + */ + byDesign, + /** + * The thread status is pending. + */ + pending +} + diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java new file mode 100644 index 000000000..c2ca09569 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java @@ -0,0 +1,20 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; + +public enum CommentType { + /** + * The comment type is not known. + */ + unknown, + /** + * This is a regular user comment. + */ + text, + /** + * The comment comes as a result of a code change. + */ + codeChange, + /** + * The comment represents a system message. + */ + system +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java new file mode 100644 index 000000000..bac2566f1 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java @@ -0,0 +1,32 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums; + +/** + * State of the status. + */ +public enum GitStatusState +{ + /** + * Status state not set. Default state. + */ + NotSet, + /** + * Status pending. + */ + Pending, + /** + * Status succeeded. + */ + Succeeded, + /** + * Status failed. + */ + Failed, + /** + * Status with an error. + */ + Error, + /** + * Status is not applicable to the target object. + */ + NotApplicable +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java new file mode 100644 index 000000000..1c2234e21 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -0,0 +1,31 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.CommentThreadStatus; + +import static org.sonar.api.issue.Issue.STATUS_OPEN; +import static org.sonar.api.issue.Issue.STATUS_CLOSED; + + +public final class CommentThreadStatusMapper { + + private CommentThreadStatusMapper() { + super(); + } + + public static CommentThreadStatus toCommentThreadStatus(String issueStatus) { + switch (issueStatus) { + case STATUS_OPEN: + return CommentThreadStatus.active; + default: + return CommentThreadStatus.fixed; + } + } + public static String toIssueStatus(CommentThreadStatus commentThreadStatus) { + switch (commentThreadStatus) { + case active: + return STATUS_OPEN; + default: + return STATUS_CLOSED; + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java new file mode 100644 index 000000000..bda46f26a --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java @@ -0,0 +1,22 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.mappers; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops.model.enums.GitStatusState; +import org.sonar.api.ce.posttask.QualityGate; + +public final class GitStatusStateMapper { + + private GitStatusStateMapper() { + super(); + } + + public static GitStatusState toGitStatusState(QualityGate.Status AnnalyzeStatus) { + switch (AnnalyzeStatus) { + case OK: + return GitStatusState.Succeeded; + case ERROR: + return GitStatusState.Error; + default: + return GitStatusState.NotSet; + } + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java index 3e2802bf8..5737399a3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java @@ -32,7 +32,7 @@ public class CommunityReportAnalysisComponentProviderTest { @Test public void testGetComponents() { List result = new CommunityReportAnalysisComponentProvider().getComponents(); - assertEquals(8, result.size()); + assertEquals(9, result.size()); assertEquals(CommunityBranchLoaderDelegate.class, result.get(0)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index 9c69a530a..df1bd5d4d 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -44,6 +44,8 @@ import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -490,6 +492,10 @@ public void testFinishedAnalysisDecorationRequest() { QualityGate qualityGate = mock(QualityGate.class); doReturn(qualityGate).when(projectAnalysis).getQualityGate(); + org.sonar.api.ce.posttask.CeTask ceTask = mock(org.sonar.api.ce.posttask.CeTask.class); + doReturn(ceTask).when(projectAnalysis).getCeTask(); + when(ceTask.getId()).thenReturn(""); + PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); doReturn("decorator-name-1").when(decorator1).name(); doReturn(ALM.BITBUCKET).when(decorator1).alm(); @@ -521,7 +527,12 @@ public void testFinishedAnalysisDecorationRequest() { when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - ScannerContext scannerContext = mock(ScannerContext.class); + //ScannerContext scannerContext = mock(ScannerContext.class); + ScannerContext scannerContext = new ScannerContext() { + private final Map props = new HashMap(); + @Override + public Map getProperties() { return this.props; } + }; doReturn(scannerContext).when(projectAnalysis).getScannerContext(); PullRequestPostAnalysisTask testCase = diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java new file mode 100644 index 000000000..1b745ab9a --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -0,0 +1,303 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.issue.Issue; +import org.sonar.api.platform.Server; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.protobuf.DbIssues; + +import java.util.Base64; +import java.util.Collections; +import java.util.Optional; + + +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AzureDevOpsServerPullRequestDecoratorTest { + @Rule + public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig()); + + @Test + public void testName() { + assertEquals("Azure", new AzureDevOpsServerPullRequestDecorator( mock(Server.class), mock(ScmInfoRepository.class) ).name()); + } + + @Test + public void decorateQualityGateStatus() { + String azureProject = "azureProject"; + String sonarProject = "sonarProject"; + String pullRequestId = "8513"; + String baseBranchName = "master"; + String branchName = "feature/some-feature"; + String azureRepository = "myRepository"; + String sonarRootUrl = "http://sonar:9000/sonar"; + String filePath = "path/to/file"; + String issueMessage = "issueMessage"; + String issueKeyVal = "issueKeyVal"; + String ruleKeyVal = "ruleKeyVal"; + int lineNumber = 5; + String token = "token"; + String authorizationHeader = "Basic " + Base64.getEncoder().encodeToString((":" + token).getBytes()); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getPersonalAccessToken()).thenReturn(token); + + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + DefaultIssue defaultIssue = mock(DefaultIssue.class); + Component component = mock(Component.class); + + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_INSTANCE_URL))) + .thenReturn(Optional.of(wireMockRule.baseUrl())); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_REPOSITORY_NAME))) + .thenReturn(Optional.of(azureRepository)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_PULLREQUEST_ID))) + .thenReturn(Optional.of(pullRequestId)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_PROJECT_ID))) + .thenReturn(Optional.of(azureProject)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_BASE_BRANCH))) + .thenReturn(Optional.of(baseBranchName)); + when(analysisDetails.getScannerProperty(eq(AzureDevOpsServerPullRequestDecorator.PULLREQUEST_AZUREDEVOPS_BRANCH))) + .thenReturn(Optional.of(branchName)); + when(analysisDetails.getAnalysisProjectKey()).thenReturn(sonarProject); + when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); + when(analysisDetails.getBranchName()).thenReturn(pullRequestId); + when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); + when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + + DbIssues.Locations locate = DbIssues.Locations.newBuilder().build(); + RuleType rule = RuleType.CODE_SMELL; + RuleKey ruleKey = mock(RuleKey.class); + when(componentIssue.getIssue()).thenReturn(defaultIssue); + when(componentIssue.getComponent()).thenReturn(component); + when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); + when(defaultIssue.getLine()).thenReturn(lineNumber); + when(defaultIssue.getLocations()).thenReturn(locate); + when(defaultIssue.type()).thenReturn(rule); + when(defaultIssue.getMessage()).thenReturn(issueMessage); + when(defaultIssue.getRuleKey()).thenReturn(ruleKey); + when(defaultIssue.key()).thenReturn(issueKeyVal); + when(ruleKey.toString()).thenReturn(ruleKeyVal); + + ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); + wireMockRule.stubFor(get(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/threads?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .willReturn(aResponse() + .withStatus(200) + .withBody( + "{\n" + + " \"value\": [\n" + + " {\n" + + " \"pullRequestThreadContext\": {\n" + + " \"iterationContext\": {\n" + + " \"firstComparingIteration\": 1,\n" + + " \"secondComparingIteration\": 1\n" + + " },\n" + + " \"changeTrackingId\": 4\n" + + " },\n" + + " \"id\": 80450,\n" + + " \"publishedDate\": \"2020-03-10T17:40:09.603Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"comments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"parentCommentId\": 0,\n" + + " \"author\": {\n" + + " \"displayName\": \"More text\",\n" + + " \"url\": \"https://dev.azure.com/fabrikam/_apis/Identities/c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"_links\": {\n" + + " \"avatar\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " }\n" + + " },\n" + + " \"id\": \"c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"uniqueName\": \"user@mail.ru\",\n" + + " \"imageUrl\": \"https://dev.azure.com/fabrikam/_api/_common/identityImage?id=c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"descriptor\": \"win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " },\n" + + " \"publishedDate\": \"2020-03-10T17:40:09.603Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"lastContentUpdatedDate\": \"2020-03-10T18:05:06.99Z\",\n" + + " \"isDeleted\": false,\n" + + " \"commentType\": \"text\",\n" + + " \"usersLiked\": [],\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80450/comments/1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"threadContext\": {\n" + + " \"filePath\": \"/azureProject/myReposytory/Helpers/file.cs\",\n" + + " \"rightFileStart\": {\n" + + " \"line\": 18,\n" + + " \"offset\": 11\n" + + " },\n" + + " \"rightFileEnd\": {\n" + + " \"line\": 18,\n" + + " \"offset\": 15\n" + + " }\n" + + " },\n" + + " \"properties\": {},\n" + + " \"identities\": null,\n" + + " \"isDeleted\": false,\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80450\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"pullRequestThreadContext\": {\n" + + " \"iterationContext\": {\n" + + " \"firstComparingIteration\": 1,\n" + + " \"secondComparingIteration\": 1\n" + + " },\n" + + " \"changeTrackingId\": 13\n" + + " },\n" + + " \"id\": 80452,\n" + + " \"publishedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"comments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"parentCommentId\": 0,\n" + + " \"author\": {\n" + + " \"displayName\": \"text\",\n" + + " \"url\": \"https://dev.azure.com/fabrikam/_apis/Identities/c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"_links\": {\n" + + " \"avatar\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " }\n" + + " },\n" + + " \"id\": \"c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"uniqueName\": \"user@mail.ru\",\n" + + " \"imageUrl\": \"https://dev.azure.com/fabrikam/_api/_common/identityImage?id=c27db56f-07a0-43ac-9725-d6666e8b66b5\",\n" + + " \"descriptor\": \"win.Uy0xLTUtMjEtMzkwNzU4MjE0NC0yNDM3MzcyODg4LTE5Njg5NDAzMjgtMjIxNQ\"\n" + + " },\n" + + " \"content\": \"Comment\",\n" + + " \"publishedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"lastContentUpdatedDate\": \"2020-03-10T19:06:11.37Z\",\n" + + " \"commentType\": \"text\",\n" + + " \"usersLiked\": [],\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80452/comments/1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"threadContext\": {\n" + + " \"filePath\": \"/azureProject/myReposytory/Helpers/file2.cs\",\n" + + " \"rightFileStart\": {\n" + + " \"line\": 30,\n" + + " \"offset\": 57\n" + + " },\n" + + " \"rightFileEnd\": {\n" + + " \"line\": 30,\n" + + " \"offset\": 65\n" + + " }\n" + + " },\n" + + " \"properties\": {},\n" + + " \"identities\": null,\n" + + " \"isDeleted\": false,\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/28afee9d-4e53-46b8-8deb-99ea20202b2b/pullRequests/8513/threads/80452\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"count\": 2\n" + + "}"))); + + wireMockRule.stubFor(post(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/threads?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .withRequestBody(equalTo("{" + + "\"status\":\"active\"," + + "\"comments\":[{" + + "\"content\":\"CODE_SMELL: issueMessage ([rule](" + sonarRootUrl + "/coding_rules?open=" + ruleKeyVal + "&rule_key=" + ruleKeyVal + "))\\n\\n[See in SonarQube](" + sonarRootUrl + "/project/issues?id=" + sonarProject + "&issues=" + issueKeyVal + "&open=" + issueKeyVal + "&pullRequest="+ pullRequestId +")\"," + + "\"commentType\":\"text\"," + + "\"parentCommentId\":0," + + "\"id\":0," + + "\"threadId\":0," + + "\"author\":null," + + "\"publishedDate\":null," + + "\"lastUpdatedDate\":null," + + "\"lastContentUpdatedDate\":null," + + "\"usersLiked\":null," + + "\"isDeleted\":null," + + "\"_links\":null}]," + + "\"threadContext\":{" + + "\"filePath\":\"/" + filePath + "\"," + + "\"leftFileStart\":null," + + "\"leftFileEnd\":null," + + "\"rightFileStart\":{\"line\":0," + + "\"offset\":1}," + + "\"rightFileEnd\":{\"line\":0," + + "\"offset\":1}}," + + "\"id\":0," + + "\"publishedDate\":null," + + "\"lastUpdatedDate\":null," + + "\"identities\":null," + + "\"isDeleted\":null," + + "\"_links\":null" + + "}") + ) + .willReturn(ok())); + + + wireMockRule.stubFor(post(urlEqualTo("/_apis/git/repositories/"+ azureRepository +"/pullRequests/"+ pullRequestId +"/statuses?api-version=5.0-preview.1")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withHeader("Authorization", equalTo(authorizationHeader)) + .withRequestBody(equalTo("{" + + "\"state\":\"Succeeded\"," + + "\"description\":\"SonarQube Gate\"," + + "\"context\":{\"genre\":\"SonarQube\",\"name\":\"PullRequestDecoration\"}," + + "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" + + "}") + ) + .willReturn(ok())); + + Server server = mock(Server.class); + when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); + AzureDevOpsServerPullRequestDecorator pullRequestDecorator = + new AzureDevOpsServerPullRequestDecorator(server, scmInfoRepository); + + pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); + } + +} From 7401aa312226f297676e751dbee6894c71ca3ccc Mon Sep 17 00:00:00 2001 From: iloer Date: Mon, 16 Mar 2020 22:12:57 +0300 Subject: [PATCH 25/26] iloer@mail.ru #102 enum to UPPERCASE --- .../AzureDevOpsServerPullRequestDecorator.java | 5 ++++- .../ce/pullrequest/azuredevops/model/Comment.java | 2 +- .../azuredevops/model/CommentThread.java | 2 +- .../model/enums/CommentThreadStatus.java | 14 +++++++------- .../azuredevops/model/enums/CommentType.java | 8 ++++---- .../azuredevops/model/enums/GitStatusState.java | 12 ++++++------ .../model/mappers/CommentThreadStatusMapper.java | 6 +++--- .../model/mappers/GitStatusStateMapper.java | 6 +++--- .../AzureDevOpsServerPullRequestDecoratorTest.java | 6 +++--- 9 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index c25f937d3..a6239b2c4 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -1,6 +1,7 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.azuredevops; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; @@ -163,7 +164,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin && azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine()) { if(!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN) - && azureThread.getStatus().equals(CommentThreadStatus.active)) { + && azureThread.getStatus().equals(CommentThreadStatus.ACTIVE)) { Comment comment = new Comment("Closed in SonarQube"); LOGGER.info("Issue closed in Sonar. try close in Azure"); sendPost( @@ -335,6 +336,8 @@ private T sendGet(String apiUrl, Class type) throws IOException { //LOGGER.info(httpResponse.toString()); HttpEntity entity = httpResponse.getEntity(); T obj = new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true) .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java index 41ebc0136..a3cfa5fbb 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/Comment.java @@ -34,7 +34,7 @@ public Comment() { public Comment(String content) { this.content = content; this.parentCommentId = 0; - this.commentType = CommentType.text; + this.commentType = CommentType.TEXT; } /** diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java index 7e8a09a36..5af791d17 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/CommentThread.java @@ -33,7 +33,7 @@ public CommentThread(String filePath, DbIssues.Locations locations, String messa comments = Arrays.asList( new Comment(message) ); - status = CommentThreadStatus.active; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); + status = CommentThreadStatus.ACTIVE; //CommentThreadStatusMapper.toCommentThreadStatus(issue.status()); threadContext = new CommentThreadContext( filePath, locations diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java index dfff1376f..78e2b105e 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentThreadStatus.java @@ -7,30 +7,30 @@ public enum CommentThreadStatus { /** * The thread status is unknown. */ - unknown, + UNKNOWN, /** * The thread status is active. */ - active, + ACTIVE, /** * The thread status is resolved as fixed. */ - fixed, + FIXED, /** * The thread status is resolved as won't fix. */ - wontFix, + WONT_FIX, /** * The thread status is closed. */ - closed, + CLOSED, /** * The thread status is resolved as by design. */ - byDesign, + BY_DESIGN, /** * The thread status is pending. */ - pending + PENDING } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java index c2ca09569..50810d432 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/CommentType.java @@ -4,17 +4,17 @@ public enum CommentType { /** * The comment type is not known. */ - unknown, + UNKNOWN, /** * This is a regular user comment. */ - text, + TEXT, /** * The comment comes as a result of a code change. */ - codeChange, + CODE_CHANGE, /** * The comment represents a system message. */ - system + SYSTEM } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java index bac2566f1..2df916e07 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/enums/GitStatusState.java @@ -8,25 +8,25 @@ public enum GitStatusState /** * Status state not set. Default state. */ - NotSet, + NOT_SET, /** * Status pending. */ - Pending, + PENDING, /** * Status succeeded. */ - Succeeded, + SUCCEEDED, /** * Status failed. */ - Failed, + FAILED, /** * Status with an error. */ - Error, + ERROR, /** * Status is not applicable to the target object. */ - NotApplicable + NOT_APPLICABLE } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java index 1c2234e21..aa939f9ca 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/CommentThreadStatusMapper.java @@ -15,14 +15,14 @@ private CommentThreadStatusMapper() { public static CommentThreadStatus toCommentThreadStatus(String issueStatus) { switch (issueStatus) { case STATUS_OPEN: - return CommentThreadStatus.active; + return CommentThreadStatus.ACTIVE; default: - return CommentThreadStatus.fixed; + return CommentThreadStatus.FIXED; } } public static String toIssueStatus(CommentThreadStatus commentThreadStatus) { switch (commentThreadStatus) { - case active: + case ACTIVE: return STATUS_OPEN; default: return STATUS_CLOSED; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java index bda46f26a..1874967de 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/model/mappers/GitStatusStateMapper.java @@ -12,11 +12,11 @@ private GitStatusStateMapper() { public static GitStatusState toGitStatusState(QualityGate.Status AnnalyzeStatus) { switch (AnnalyzeStatus) { case OK: - return GitStatusState.Succeeded; + return GitStatusState.SUCCEEDED; case ERROR: - return GitStatusState.Error; + return GitStatusState.ERROR; default: - return GitStatusState.NotSet; + return GitStatusState.NOT_SET; } } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java index 1b745ab9a..b214fc7b0 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -246,10 +246,10 @@ public void decorateQualityGateStatus() { .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) .withHeader("Authorization", equalTo(authorizationHeader)) .withRequestBody(equalTo("{" + - "\"status\":\"active\"," + + "\"status\":\"ACTIVE\"," + "\"comments\":[{" + "\"content\":\"CODE_SMELL: issueMessage ([rule](" + sonarRootUrl + "/coding_rules?open=" + ruleKeyVal + "&rule_key=" + ruleKeyVal + "))\\n\\n[See in SonarQube](" + sonarRootUrl + "/project/issues?id=" + sonarProject + "&issues=" + issueKeyVal + "&open=" + issueKeyVal + "&pullRequest="+ pullRequestId +")\"," + - "\"commentType\":\"text\"," + + "\"commentType\":\"TEXT\"," + "\"parentCommentId\":0," + "\"id\":0," + "\"threadId\":0," + @@ -284,7 +284,7 @@ public void decorateQualityGateStatus() { .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) .withHeader("Authorization", equalTo(authorizationHeader)) .withRequestBody(equalTo("{" + - "\"state\":\"Succeeded\"," + + "\"state\":\"SUCCEEDED\"," + "\"description\":\"SonarQube Gate\"," + "\"context\":{\"genre\":\"SonarQube\",\"name\":\"PullRequestDecoration\"}," + "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" + From 935afffc53af478827a91d00ec80383ac244430e Mon Sep 17 00:00:00 2001 From: iloer Date: Mon, 30 Mar 2020 22:41:21 +0300 Subject: [PATCH 26/26] iloer@mail.ru #102 attempt to close comment in AzureDevOps by issue.key --- .../AzureDevOpsServerPullRequestDecorator.java | 10 +++++++--- .../AzureDevOpsServerPullRequestDecoratorTest.java | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java index a6239b2c4..f6b3cab07 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecorator.java @@ -142,7 +142,7 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin for (PostAnalysisIssueVisitor.ComponentIssue issue : openIssues) { String filePath = analysisDetails.getSCMPathForIssue(issue).orElse(null); Integer line = issue.getIssue().getLine(); - if (filePath != null && line != null) { + if (filePath != null) { try { filePath = "/" + filePath; LOGGER.trace(String.format("ISSUE: authorLogin: %s ", issue.getIssue().authorLogin())); @@ -160,8 +160,12 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettin LOGGER.trace(String.format("filePath: %s (%s)", filePath, azureThread.getThreadContext().getFilePath().equals(filePath))); LOGGER.trace(String.format("azureLine: %d", azureThread.getThreadContext().getRightFileStart().getLine())); LOGGER.trace(String.format("line: %d (%s)", line, azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine())); + if (azureThread.getThreadContext().getFilePath().equals(filePath) - && azureThread.getThreadContext().getRightFileStart().getLine() == locate.getTextRange().getEndLine()) { + && azureThread.getComments() + .stream() + .filter(c -> c.getContent().contains(issue.getIssue().key())) + .count() > 0 ) { if(!issue.getIssue().getStatus().equals(Issue.STATUS_OPEN) && azureThread.getStatus().equals(CommentThreadStatus.ACTIVE)) { @@ -279,7 +283,7 @@ private String getThreadApiUrl(){ private String getGitPullRequestStatus(AnalysisDetails analysisDetails) throws IOException { final String GIT_STATUS_CONTEXT_GENRE = "SonarQube"; - final String GIT_STATUS_CONTEXT_NAME = "PullRequestDecoration"; + final String GIT_STATUS_CONTEXT_NAME = "QualityGate"; final String GIT_STATUS_DESCRIPTION = "SonarQube Gate"; GitPullRequestStatus status = new GitPullRequestStatus( diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java index b214fc7b0..685ed1dbf 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsServerPullRequestDecoratorTest.java @@ -286,7 +286,7 @@ public void decorateQualityGateStatus() { .withRequestBody(equalTo("{" + "\"state\":\"SUCCEEDED\"," + "\"description\":\"SonarQube Gate\"," + - "\"context\":{\"genre\":\"SonarQube\",\"name\":\"PullRequestDecoration\"}," + + "\"context\":{\"genre\":\"SonarQube\",\"name\":\"QualityGate\"}," + "\"targetUrl\":\"" + sonarRootUrl + "/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId + "\"" + "}") )