Skip to content

Commit

Permalink
Adds support for Bitbucket Cloud Code Insights
Browse files Browse the repository at this point in the history
This commit provides support for the newly created bitbucket cloud code insights
API endpoints. The implementation has been done under the consideration that in
newer versions no dedicated ALM support for bitbucket cloud exists, thus this
implementation is minimal invasive.

One thing to note here:

* For local testing the link has to be set manually to a non localhost URL since
bitbuckets API doesn't support it.
  • Loading branch information
Marvin Wichmann committed Jun 7, 2020
1 parent 24bb830 commit 62c8687
Show file tree
Hide file tree
Showing 25 changed files with 935 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,6 @@ public void load(CoreExtension.Context context) {
"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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
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.bitbucket.BitbucketServerPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClientFacade;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketCloudClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketServerClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.RestApplicationAuthenticationProvider;
Expand All @@ -42,8 +44,8 @@ public List<Object> getComponents() {
return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class,
PostAnalysisIssueVisitor.class, GithubPullRequestDecorator.class,
GraphqlCheckRunProvider.class, DefaultLinkHeaderReader.class, RestApplicationAuthenticationProvider.class,
BitbucketServerPullRequestDecorator.class, BitbucketClient.class,
GitlabServerPullRequestDecorator.class);
BitbucketServerPullRequestDecorator.class, BitbucketServerClient.class, BitbucketCloudClient.class,
BitbucketClientFacade.class, GitlabServerPullRequestDecorator.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) {
projectAnalysis.getProject(), configuration, server.getPublicRootUrl());

PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get();
LOGGER.info("using pull request decorator" + pullRequestDecorator.name());
LOGGER.info("using pull request decorator " + pullRequestDecorator.name());
DecorationResult decorationResult = pullRequestDecorator.decorateQualityGateStatus(analysisDetails, unifyConfiguration);

decorationResult.getPullRequestUrl().ifPresent(pullRequestUrl -> persistPullRequestUrl(pullRequestUrl, projectAnalysis, optionalBranchName.get()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.UnifyConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClientFacade;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketException;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.Annotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateAnnotationsRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.IAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import org.sonar.api.ce.posttask.QualityGate;
Expand Down Expand Up @@ -62,8 +60,6 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat

public static final String PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG = "sonar.pullrequest.bitbucket.repositorySlug";

public static final String PULL_REQUEST_BITBUCKET_USER_SLUG = "sonar.pullrequest.bitbucket.userSlug";

private static final Logger LOGGER = Loggers.get(BitbucketServerPullRequestDecorator.class);

private static final int DEFAULT_MAX_ANNOTATIONS = 1000;
Expand All @@ -74,10 +70,10 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s))
.collect(Collectors.toList());

private final BitbucketClient client;
private final BitbucketClientFacade facade;

public BitbucketServerPullRequestDecorator(BitbucketClient client) {
this.client = client;
public BitbucketServerPullRequestDecorator(BitbucketClientFacade facade) {
this.facade = facade;
}

@Override
Expand All @@ -88,17 +84,25 @@ public String name() {
@Override
public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration) {
try {
if(!client.supportsCodeInsights()) {
LOGGER.warn("Your Bitbucket instances does not support the Code Insights API.");
facade.withConfiguration(configuration);
if (!facade.supportsCodeInsights()) {
LOGGER.warn("Your Bitbucket instance does not support the Code Insights API.");
return DEFAULT_DECORATION_RESULT;
}
String project = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_PROJECT_KEY);

String repo = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG);
client.createReport(project, repo,

facade.createReport(project, repo,
analysisDetails.getCommitSha(),
toReport(analysisDetails)
toReport(analysisDetails),
reportDescription(analysisDetails),
analysisDetails.getAnalysisDate().toInstant(),
analysisDetails.getDashboardUrl(),
format("%s/common/icon.png", analysisDetails.getBaseImageUrl()),
analysisDetails.getQualityGateStatus()
);

updateAnnotations(project, repo, analysisDetails);
} catch (IOException e) {
LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e);
Expand All @@ -107,7 +111,7 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetail
return DEFAULT_DECORATION_RESULT;
}

private CreateReportRequest toReport(AnalysisDetails analysisDetails) {
private List<ReportData> toReport(AnalysisDetails analysisDetails) {
Map<RuleType, Long> rules = analysisDetails.countRuleByType();

List<ReportData> reportData = new ArrayList<>();
Expand All @@ -116,31 +120,24 @@ private CreateReportRequest toReport(AnalysisDetails analysisDetails) {
reportData.add(securityReport(rules.get(RuleType.VULNERABILITY), rules.get(RuleType.SECURITY_HOTSPOT)));
reportData.add(new ReportData("Duplication", new DataValue.Percentage(newDuplication(analysisDetails))));
reportData.add(maintainabilityReport(rules.get(RuleType.CODE_SMELL)));
reportData.add(new ReportData("Analysis details", new DataValue.Link("Go to SonarQube", analysisDetails.getDashboardUrl())));

return new CreateReportRequest(reportData,
reportDescription(analysisDetails),
"SonarQube",
"SonarQube",
analysisDetails.getAnalysisDate().toInstant(),
analysisDetails.getDashboardUrl(),
format("%s/common/icon.png", analysisDetails.getBaseImageUrl()),
asInsightStatus(analysisDetails.getQualityGateStatus()));
reportData.add(new ReportData("Analysis details", facade.createLinkDataValue(analysisDetails.getDashboardUrl())));

return reportData;
}

private void updateAnnotations(String project, String repo, AnalysisDetails analysisDetails) throws IOException {
final AtomicInteger chunkCounter = new AtomicInteger(0);

client.deleteAnnotations(project, repo, analysisDetails.getCommitSha());
facade.deleteAnnotations(project, repo, analysisDetails.getCommitSha());

Map<Object, Set<Annotation>> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream()
Map<Object, Set<IAnnotation>> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream()
.filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent())
.filter(i -> i.getComponent().getType() == Component.Type.FILE)
.filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status()))
.sorted(Comparator.comparing(a -> Severity.ALL.indexOf(a.getIssue().severity())))
.map(componentIssue -> {
String path = componentIssue.getComponent().getReportAttributes().getScmPath().get();
return new Annotation(componentIssue.getIssue().key(),
return facade.createAnnotation(componentIssue.getIssue().key(),
Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0),
analysisDetails.getIssueUrl(componentIssue.getIssue().key()),
componentIssue.getIssue().getMessage(),
Expand All @@ -149,9 +146,9 @@ private void updateAnnotations(String project, String repo, AnalysisDetails anal
toBitbucketType(componentIssue.getIssue().type()));
}).collect(Collectors.groupingBy(s -> chunkCounter.getAndIncrement() / DEFAULT_MAX_ANNOTATIONS, toSet()));

for (Set<Annotation> annotations : annotationChunks.values()) {
for (Set<IAnnotation> annotations : annotationChunks.values()) {
try {
client.createAnnotations(project, repo, analysisDetails.getCommitSha(), new CreateAnnotationsRequest(annotations));
facade.createAnnotations(project, repo, analysisDetails.getCommitSha(), annotations);
} catch (BitbucketException e) {
if (e.isError(BitbucketException.PAYLOAD_TOO_LARGE)) {
LOGGER.warn("The annotations will be truncated since the maximum number of annotations for this report has been reached.");
Expand All @@ -163,10 +160,6 @@ private void updateAnnotations(String project, String repo, AnalysisDetails anal
}
}

private String asInsightStatus(QualityGate.Status status) {
return QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS";
}

private String toBitbucketSeverity(String severity) {
if (severity == null) {
return "LOW";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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.client;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.UnifyConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketServerPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ErrorResponse;
import okhttp3.Response;

import java.io.IOException;

abstract class AbstractBitbucketClient {
private UnifyConfiguration configuration;

ObjectMapper getObjectMapper() {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

String baseUrl() {
return configuration.getRequiredProperty(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_URL);
}

String getToken() {
return configuration.getRequiredProperty(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_TOKEN);
}

void validate(Response response) throws IOException {
if (!response.isSuccessful()) {
ErrorResponse errors = null;
if (response.body() != null) {
errors = getObjectMapper().reader().forType(ErrorResponse.class)
.readValue(response.body().string());
}
throw new BitbucketException(response.code(), errors);
}
}

public void setConfiguration(UnifyConfiguration configuration) {
this.configuration = configuration;
}
}

0 comments on commit 62c8687

Please sign in to comment.