From 4adf6a57f12103e952078a1d87610f3ecdb526c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20=C3=85hsberg?= Date: Mon, 6 Apr 2020 12:07:24 +0000 Subject: [PATCH] Add support for Bitbucket Code Insights This change will add support for Bitbucket Code Insights in favor of regular comments when available. It will fall back on the comments strategy when the Code Insights is not available (it is supported in version 5.15 and later). --- .../plugin/CommunityBranchPlugin.java | 6 +- ...munityReportAnalysisComponentProvider.java | 6 +- .../ce/pullrequest/AnalysisDetails.java | 21 +- .../BitbucketServerPullRequestDecorator.java | 235 +++++++++++++ .../ce/pullrequest/bitbucket/FileComment.java | 44 --- .../bitbucket/client/BitbucketClient.java | 172 ++++++++++ .../bitbucket/client/BitbucketException.java | 50 +++ .../model/Annotation.java} | 51 ++- .../model/CreateAnnotationsRequest.java} | 23 +- .../client/model/CreateReportRequest.java | 88 +++++ .../bitbucket/client/model/DataValue.java | 77 +++++ .../model/ErrorResponse.java} | 36 +- .../model/ReportData.java} | 44 +-- .../client/model/ServerProperties.java | 66 ++++ .../bitbucket/response/activity/Activity.java | 51 --- .../response/activity/ActivityPage.java | 72 ---- .../bitbucket/response/activity/Comment.java | 58 ---- .../bitbucket/response/diff/Diff.java | 66 ---- .../bitbucket/response/diff/DiffLine.java | 66 ---- .../bitbucket/response/diff/DiffPage.java | 59 ---- .../bitbucket/response/diff/File.java | 66 ---- .../bitbucket/response/diff/Hunk.java | 73 ---- .../BitbucketServerPullRequestDecorator.java | 319 ------------------ .../ScannerConfigurationLoaderSensor.java | 2 +- src/main/resources/static/common/icon.png | Bin 0 -> 4601 bytes .../plugin/CommunityBranchPluginTest.java | 2 +- ...tyReportAnalysisComponentProviderTest.java | 2 +- .../BitbucketPullRequestDecoratorTest.java | 153 +++++++++ ...tbucketServerPullRequestDecoratorTest.java | 231 ------------- 29 files changed, 953 insertions(+), 1186 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketServerPullRequestDecorator.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/FileComment.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketClient.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketException.java rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/{Anchor.java => client/model/Annotation.java} (51%) rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/{SummaryComment.java => client/model/CreateAnnotationsRequest.java} (65%) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateReportRequest.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/DataValue.java rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/{response/activity/User.java => client/model/ErrorResponse.java} (59%) rename src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/{response/diff/Segment.java => client/model/ReportData.java} (58%) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ServerProperties.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/diff/Diff.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffLine.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffPage.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/File.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Hunk.java delete mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java create mode 100644 src/main/resources/static/common/icon.png create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java delete mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java 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 80f375312..8595f99b5 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -21,7 +21,7 @@ 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.bitbucket.BitbucketServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4.GraphqlCheckRunProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; @@ -142,10 +142,6 @@ public void load(CoreExtension.Context context) { .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") 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 95a98e654..3210268ce 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,7 +20,8 @@ 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.server.BitbucketServerPullRequestDecorator; +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.github.GithubPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.DefaultLinkHeaderReader; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.RestApplicationAuthenticationProvider; @@ -41,7 +42,8 @@ public List getComponents() { return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class, PostAnalysisIssueVisitor.class, GithubPullRequestDecorator.class, GraphqlCheckRunProvider.class, DefaultLinkHeaderReader.class, RestApplicationAuthenticationProvider.class, - BitbucketServerPullRequestDecorator.class, GitlabServerPullRequestDecorator.class); + BitbucketServerPullRequestDecorator.class, BitbucketClient.class, + GitlabServerPullRequestDecorator.class); } } 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 3f55b9b02..7f2b1c693 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 @@ -105,6 +105,14 @@ public String getCommitSha() { return branchDetails.getCommitId(); } + public String getDashboardUrl() { + return publicRootURL + "/dashboard?id=" + URLEncoder.encode(project.getKey()) + "&pullRequest=" + branchDetails.getBranchName(); + } + + public String getIssueUrl(String issueKey) { + return publicRootURL + "/project/issues?id=" + URLEncoder.encode(project.getKey()) + "&pullRequest=" + branchDetails.getBranchName() + "&issues=" + issueKey + "&open=" + issueKey; + } + public QualityGate.Status getQualityGateStatus() { return qualityGate.getStatus(); } @@ -183,7 +191,7 @@ public String createAnalysisSummary(FormatterFactory formatterFactory) { .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"))); + new Link(getDashboardUrl(), new Text("View in SonarQube"))); return formatterFactory.documentFormatter().format(document, formatterFactory); } @@ -205,7 +213,7 @@ public String createAnalysisIssueSummary(PostAnalysisIssueVisitor.ComponentIssue 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")) + new Link(getIssueUrl(issue.key()), new Text("View in SonarQube")) ); return formatterFactory.documentFormatter().format(document, formatterFactory); } @@ -273,13 +281,12 @@ public String getAnalysisProjectKey() { return project.getKey(); } - - private List findFailedConditions() { + public List findFailedConditions() { return qualityGate.getConditions().stream().filter(c -> c.getStatus() == QualityGate.EvaluationStatus.ERROR) .collect(Collectors.toList()); } - private Optional findMeasure(String metricKey) { + public Optional findMeasure(String metricKey) { return measuresHolder.getMeasureRepository().getRawMeasure(measuresHolder.getTreeRootHolder().getRoot(), measuresHolder.getMetricRepository() .getByKey(metricKey)) @@ -290,7 +297,7 @@ public Optional findQualityGateCondition(String metricKey return qualityGate.getConditions().stream().filter(c -> metricKey.equals(c.getMetricKey())).findFirst(); } - private Map countRuleByType() { + public Map countRuleByType() { return Arrays.stream(RuleType.values()).collect(Collectors.toMap(k -> k, k -> postAnalysisIssueVisitor.getIssues() .stream() @@ -305,7 +312,7 @@ private static String pluralOf(long value, String singleLabel, String multiLabel } - private static String format(QualityGate.Condition condition) { + public static String format(QualityGate.Condition condition) { Metric metric = CoreMetrics.getMetric(condition.getMetricKey()); if (metric.getType() == Metric.ValueType.RATING) { return String diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketServerPullRequestDecorator.java new file mode 100644 index 000000000..24834b1bb --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketServerPullRequestDecorator.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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; + +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.UnifyConfiguration; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient; +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.DataValue; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.ce.task.projectanalysis.component.Component; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toSet; + +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 = "sonar.pullrequest.bitbucket.projectKey"; + + 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; + + 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 final BitbucketClient client; + + public BitbucketServerPullRequestDecorator(BitbucketClient client) { + this.client = client; + } + + @Override + public String name() { + return "BitbucketServer"; + } + + @Override + public void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration) { + try { + if(!client.supportsCodeInsights()) { + LOGGER.warn("Your Bitbucket instances does not support the Code Insights API."); + return; + } + String project = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_PROJECT_KEY); + + String repo = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG); + client.createReport(project, repo, + analysisDetails.getCommitSha(), + toReport(analysisDetails) + ); + updateAnnotations(project, repo, analysisDetails); + } catch (IOException e) { + LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e); + } + } + + private CreateReportRequest toReport(AnalysisDetails analysisDetails) { + Map rules = analysisDetails.countRuleByType(); + + List reportData = new ArrayList<>(); + reportData.add(reliabilityReport(rules.get(RuleType.BUG))); + reportData.add(new ReportData("Code coverage", new DataValue.Percentage(newCoverage(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())); + } + + private void updateAnnotations(String project, String repo, AnalysisDetails analysisDetails) throws IOException { + final AtomicInteger chunkCounter = new AtomicInteger(0); + + client.deleteAnnotations(project, repo, analysisDetails.getCommitSha()); + + Map> 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(), + Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0), + analysisDetails.getIssueUrl(componentIssue.getIssue().key()), + componentIssue.getIssue().getMessage(), + path, + toBitbucketSeverity(componentIssue.getIssue().severity()), + toBitbucketType(componentIssue.getIssue().type())); + }).collect(Collectors.groupingBy(s -> chunkCounter.getAndIncrement() / DEFAULT_MAX_ANNOTATIONS, toSet())); + + for (Set annotations : annotationChunks.values()) { + try { + client.createAnnotations(project, repo, analysisDetails.getCommitSha(), new CreateAnnotationsRequest(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."); + } else { + throw e; + } + + } + } + } + + private String asInsightStatus(QualityGate.Status status) { + return QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS"; + } + + private String toBitbucketSeverity(String severity) { + if (severity == null) { + return "LOW"; + } + switch (severity) { + case Severity.BLOCKER: + case Severity.CRITICAL: + return "HIGH"; + case Severity.MAJOR: + return "MEDIUM"; + default: + return "LOW"; + } + } + + private String toBitbucketType(RuleType sonarqubeType) { + switch (sonarqubeType) { + case SECURITY_HOTSPOT: + case VULNERABILITY: + return "VULNERABILITY"; + case CODE_SMELL: + return "CODE_SMELL"; + case BUG: + return "BUG"; + default: + throw new IllegalStateException(format("%s is not a valid ruleType.", sonarqubeType)); + } + } + + private ReportData securityReport(Long vulnerabilities, Long hotspots) { + String vulnerabilityDescription = vulnerabilities == 1 ? "Vulnerability" : "Vulnerabilities"; + String hotspotDescription = hotspots == 1 ? "Hotspot" : "Hotspots"; + String security = format("%d %s (and %d %s)", vulnerabilities, vulnerabilityDescription, hotspots, hotspotDescription); + return new ReportData("Security", new DataValue.Text(security)); + } + + private ReportData reliabilityReport(Long bugs) { + String description = bugs == 1 ? "Bug" : "Bugs"; + return new ReportData("Reliability", new DataValue.Text(format("%d %s", bugs, description))); + } + + private ReportData maintainabilityReport(Long codeSmells) { + String description = codeSmells == 1 ? "Code Smell" : "Code Smells"; + return new ReportData("Maintainability", new DataValue.Text(format("%d %s", codeSmells, description))); + } + + private String reportDescription(AnalysisDetails details) { + String header = details.getQualityGateStatus() == QualityGate.Status.OK ? "Quality Gate passed" : "Quality Gate failed"; + String body = details.findFailedConditions().stream() + .map(AnalysisDetails::format) + .map(s -> format("- %s", s)) + .collect(Collectors.joining(System.lineSeparator())); + return format("%s%n%s", header, body); + } + + private BigDecimal newCoverage(AnalysisDetails details) { + return details.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) + .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) + .map(QualityGate.Condition::getValue) + .map(BigDecimal::new) + .orElse(BigDecimal.ZERO); + } + + private BigDecimal newDuplication(AnalysisDetails details) { + return details.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY) + .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) + .map(QualityGate.Condition::getValue) + .map(BigDecimal::new) + .orElse(BigDecimal.ZERO); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/FileComment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/FileComment.java deleted file mode 100644 index b3988e747..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/FileComment.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; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class FileComment implements Serializable { - private final String text; - - private final Anchor anchor; - - @JsonCreator - public FileComment(@JsonProperty("text") String text, @JsonProperty("anchor") Anchor anchor) { - this.text = text; - this.anchor = anchor; - } - - public String getText() { - return text; - } - - public Anchor getAnchor() { - return anchor; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketClient.java new file mode 100644 index 000000000..209768a38 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketClient.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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.Include; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +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.ErrorResponse; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ServerProperties; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketServerPullRequestDecorator; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import java.io.IOException; +import java.util.Optional; + +import static java.lang.String.format; + +@ComputeEngineSide +public class BitbucketClient { + private static final Logger LOGGER = Loggers.get(BitbucketClient.class); + private static final String REPORT_KEY = "com.github.mc1arke.sonarqube"; + private final Configuration configuration; + + private OkHttpClient client; + private ObjectMapper objectMapper; + + public BitbucketClient(Configuration configuration) { + this.configuration = configuration; + } + + public boolean isConfigured() { + return configuration.hasKey(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_URL) && + configuration.hasKey(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_TOKEN); + } + + public ServerProperties getServerProperties() throws IOException { + Request req = new Request.Builder() + .get() + .url(format("%s/rest/api/1.0/application-properties", baseUrl())) + .build(); + try (Response response = getClient().newCall(req).execute()) { + validate(response); + + return getObjectMapper().reader().forType(ServerProperties.class) + .readValue(response.body().string()); + } + } + + public void createReport(String project, String repository, String commit, CreateReportRequest request) throws IOException { + String body = getObjectMapper().writeValueAsString(request); + Request req = new Request.Builder() + .put(RequestBody.create(MediaType.parse("application/json"), body)) + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", baseUrl(), project, repository, commit, REPORT_KEY)) + .build(); + + try (Response response = getClient().newCall(req).execute()) { + validate(response); + } + } + + public void createAnnotations(String project, String repository, String commit, CreateAnnotationsRequest request) throws IOException { + if (request.getAnnotations().isEmpty()) { + return; + } + Request req = new Request.Builder() + .post(RequestBody.create(MediaType.parse("application/json"), getObjectMapper().writeValueAsString(request))) + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", baseUrl(), project, repository, commit, REPORT_KEY)) + .build(); + try (Response response = getClient().newCall(req).execute()) { + validate(response); + } + } + + public void deleteAnnotations(String project, String repository, String commit) throws IOException { + Request req = new Request.Builder() + .delete() + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", baseUrl(), project, repository, commit, REPORT_KEY)) + .build(); + try (Response response = getClient().newCall(req).execute()) { + validate(response); + } + } + + public boolean supportsCodeInsights() { + try { + ServerProperties server = getServerProperties(); + LOGGER.debug(format("Your Bitbucket Server installation is version %s", server.getVersion())); + if (server.hasCodeInsightsApi()) { + return true; + } else { + LOGGER.info("Bitbucket Server version is to old. %s is the minimum version that supports Code Insights", + ServerProperties.CODE_INSIGHT_VERSION); + } + } catch (IOException e) { + LOGGER.error("Could not determine Bitbucket Server version", e); + return false; + } + return false; + } + + private 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); + } + } + + private OkHttpClient getClient() { + client = Optional.ofNullable(client).orElseGet(() -> + new OkHttpClient.Builder() + .authenticator(((route, response) -> + response.request() + .newBuilder() + .header("Authorization", format("Bearer %s", getToken())) + .header("Accept", "application/json") + .build() + )) + .build() + ); + return client; + } + + private ObjectMapper getObjectMapper() { + objectMapper = Optional.ofNullable(objectMapper).orElseGet(() -> new ObjectMapper() + .setSerializationInclusion(Include.NON_NULL) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + ); + return objectMapper; + } + + private String baseUrl() { + return configuration.get(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_URL) + .orElseThrow(() -> + new IllegalArgumentException(format("Missing required property %s", BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_URL)) + ); + } + + private String getToken() { + return configuration.get(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_TOKEN) + .orElseThrow(() -> new IllegalArgumentException("Personal Access Token for Bitbucket Server is missing")); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketException.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketException.java new file mode 100644 index 000000000..392a480d8 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/BitbucketException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ErrorResponse; + +import java.util.Optional; +import java.util.stream.Collectors; + +public class BitbucketException extends RuntimeException { + public static final int PAYLOAD_TOO_LARGE = 413; + + private final int code; + private final ErrorResponse errors; + + BitbucketException(int code, ErrorResponse errors) { + this.code = code; + this.errors = errors; + } + + public boolean isError(int code) { + return this.code == code; + } + + @Override + public String getMessage() { + return Optional.ofNullable(errors) + .map(ErrorResponse::getErrors) + .map(e -> e.stream() + .map(ErrorResponse.Error::getMessage) + .collect(Collectors.joining("\n"))) + .orElse(String.format("Bitbucket responded with an error status (%d)", code)); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/Anchor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/Annotation.java similarity index 51% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/Anchor.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/Annotation.java index eaed06360..255fe16ca 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/Anchor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/Annotation.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Oliver Jedinger + * Copyright (C) 2020 Mathias Åhsberg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,43 +16,64 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; -public class Anchor implements Serializable { +public class Annotation implements Serializable { + private final String externalId; private final int line; - - private final String lineType; - + private final String link; + private final String message; private final String path; - - private final String fileType; + private final String severity; + private final String type; @JsonCreator - public Anchor(@JsonProperty("line") int line, @JsonProperty("lineType") String lineType, @JsonProperty("path") String path, @JsonProperty("fileType") String fileType) { + public Annotation(@JsonProperty("externalId") String externalId, + @JsonProperty("line") int line, + @JsonProperty("link") String link, + @JsonProperty("message") String message, + @JsonProperty("path") String path, + @JsonProperty("severity") String severity, + @JsonProperty("type") String type) { + this.externalId = externalId; this.line = line; - this.lineType = lineType; + this.link = link; + this.message = message; this.path = path; - this.fileType = fileType; + this.severity = severity; + this.type = type; + } + + public String getExternalId() { + return externalId; } public int getLine() { return line; } - public String getLineType() { - return lineType; + public String getLink() { + return link; + } + + public String getMessage() { + return message; } public String getPath() { return path; } - public String getFileType() { - return fileType; + public String getSeverity() { + return severity; + } + + public String getType() { + return type; } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/SummaryComment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateAnnotationsRequest.java similarity index 65% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/SummaryComment.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateAnnotationsRequest.java index 70c0b9435..d115bea5d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/SummaryComment.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateAnnotationsRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Oliver Jedinger + * Copyright (C) 2020 Mathias Åhsberg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,23 +16,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model; import java.io.Serializable; +import java.util.Collections; +import java.util.Set; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +public class CreateAnnotationsRequest implements Serializable { + private final Set annotations; -public class SummaryComment implements Serializable { - private final String text; - - @JsonCreator - public SummaryComment(@JsonProperty("text") final String text) { - super(); - this.text = text; + public CreateAnnotationsRequest(Set annotations) { + this.annotations = annotations == null ? Collections.emptySet() : annotations; } - public String getText() { - return text; + public Set getAnnotations() { + return annotations; } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateReportRequest.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateReportRequest.java new file mode 100644 index 000000000..c39e0259c --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/CreateReportRequest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; +import java.util.List; + +public class CreateReportRequest { + private final List data; + private final String details; + private final String title; + private final String reporter; + private final Instant createdDate; + private final String link; + private final String logoUrl; + private final String result; + + @JsonCreator + public CreateReportRequest( + @JsonProperty("data") List data, + @JsonProperty("details") String details, + @JsonProperty("title") String title, + @JsonProperty("reporter") String reporter, + @JsonProperty("createdDate") Instant createdDate, + @JsonProperty("link") String link, + @JsonProperty("logoUrl") String logoUrl, + @JsonProperty("result") String result) { + this.data = data; + this.details = details; + this.title = title; + this.reporter = reporter; + this.createdDate = createdDate; + this.link = link; + this.logoUrl = logoUrl; + this.result = result; + } + + public List getData() { + return data; + } + + public String getDetails() { + return details; + } + + public String getTitle() { + return title; + } + + public String getReporter() { + return reporter; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public String getLink() { + return link; + } + + public String getLogoUrl() { + return logoUrl; + } + + public String getResult() { + return result; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/DataValue.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/DataValue.java new file mode 100644 index 000000000..e7ebd1edc --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/DataValue.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.io.Serializable; +import java.math.BigDecimal; + +public abstract class DataValue implements Serializable { + + public static class Link extends DataValue { + private final String linktext; + private final String href; + + @JsonCreator + public Link(@JsonProperty("linktext") String linktext, @JsonProperty("href") String href) { + this.linktext = linktext; + this.href = href; + } + + public String getLinktext() { + return linktext; + } + + public String getHref() { + return href; + } + } + + public static class Text extends DataValue { + private final String value; + + @JsonCreator + public Text(@JsonProperty("value") String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + public static class Percentage extends DataValue { + + private final BigDecimal value; + + @JsonCreator + public Percentage(@JsonProperty("value") BigDecimal value) { + this.value = value; + } + + @JsonValue + public BigDecimal getValue() { + return value; + } + } +} 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/client/model/ErrorResponse.java similarity index 59% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/activity/User.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ErrorResponse.java index e0ef00055..bbe230b45 100644 --- 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/client/model/ErrorResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Oliver Jedinger + * Copyright (C) 2020 Mathias Åhsberg * * 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,31 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.activity; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model; -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; +import java.util.Collections; +import java.util.Set; - private final String slug; +public class ErrorResponse { + private final Set errors; - @JsonCreator - public User(@JsonProperty("name") final String name, @JsonProperty("slug") final String slug) { - this.name = name; - this.slug = slug; + ErrorResponse(@JsonProperty("errors") Set errors) { + this.errors = errors; } - - public String getName() { - return name; + public Set getErrors() { + return Collections.unmodifiableSet(errors); } + public static class Error { + private String message; + Error(@JsonProperty("message") String message) { + this.message = message; + } - public String getSlug() { - return slug; + public String getMessage() { + return this.message; + } } } + diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Segment.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ReportData.java similarity index 58% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Segment.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ReportData.java index 829b81bd3..3039f029f 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Segment.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ReportData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Oliver Jedinger + * Copyright (C) 2020 Mathias Åhsberg * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,37 +16,43 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff; - -import java.io.Serializable; -import java.util.List; +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class Segment implements Serializable { +public class ReportData { + private final String title; + private final DataValue value; + @JsonProperty("type") private final String type; - private final List lines; + @JsonCreator + public ReportData(@JsonProperty("title") String title, @JsonProperty("value") DataValue value) { + this.title = title; + this.value = value; + this.type = typeFrom(value); + } - private final boolean truncated; + public String getTitle() { + return title; + } - @JsonCreator - public Segment(@JsonProperty("type") final String type, @JsonProperty("lines") final List lines, @JsonProperty("truncated") final boolean truncated) { - this.type = type; - this.lines = lines; - this.truncated = truncated; + public DataValue getValue() { + return value; } public String getType() { return type; } - public List getLines() { - return lines; - } - - public boolean isTruncated() { - return truncated; + private String typeFrom(DataValue value) { + if (value instanceof DataValue.Link) { + return "LINK"; + } else if (value instanceof DataValue.Percentage) { + return "PERCENTAGE"; + } else { + return "TEXT"; + } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ServerProperties.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ServerProperties.java new file mode 100644 index 000000000..1d5ef8b48 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/client/model/ServerProperties.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Mathias Åhsberg + * + * 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.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +public class ServerProperties implements Serializable { + + public static final String CODE_INSIGHT_VERSION = "5.15"; + + private final String version; + + @JsonCreator + public ServerProperties(@JsonProperty("version") String version) { + this.version = version; + } + + public String getVersion() { + return version; + } + + public boolean hasCodeInsightsApi() { + return compareTo(CODE_INSIGHT_VERSION) >= 0; + } + + private int compareTo(String other) { + String[] current = semver(version); + String[] minimum = semver(other); + + int length = Math.max(current.length, minimum.length); + for (int i = 0; i < length; i++) { + int thisPart = i < current.length ? + Integer.parseInt(current[i]) : 0; + int thatPart = i < minimum.length ? + Integer.parseInt(minimum[i]) : 0; + if (thisPart < thatPart) + return -1; + if (thisPart > thatPart) + return 1; + } + return 0; + } + + private String[] semver(String v) { + return v.split("\\."); + } +} 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/diff/Diff.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Diff.java deleted file mode 100644 index c7299c948..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Diff.java +++ /dev/null @@ -1,66 +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.diff; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Diff implements Serializable { - private final String fromHash; - - private final String toHash; - - private final List hunks; - - private final File source; - - private final File destination; - - @JsonCreator - public Diff(@JsonProperty("fromHash") String fromHash, @JsonProperty("toHash") String toHash, @JsonProperty("hunks") List hunks, @JsonProperty("source") File source, @JsonProperty("destination") File destination) { - this.fromHash = fromHash; - this.toHash = toHash; - this.hunks = hunks; - this.source = source; - this.destination = destination; - } - - public String getFromHash() { - return fromHash; - } - - public String getToHash() { - return toHash; - } - - public List getHunks() { - return hunks; - } - - public File getSource() { - return source; - } - - public File getDestination() { - return destination; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffLine.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffLine.java deleted file mode 100644 index 4e0fd6ff6..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffLine.java +++ /dev/null @@ -1,66 +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.diff; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class DiffLine implements Serializable { - private final int source; - - private final int destination; - - private final String line; - - private final boolean truncated; - - private final List commentIds; - - @JsonCreator - public DiffLine(@JsonProperty("source") int source, @JsonProperty("destination") int destination, @JsonProperty("line") String line, @JsonProperty("truncated") boolean truncated, @JsonProperty("commentIds") List commentIds) { - this.source = source; - this.destination = destination; - this.line = line; - this.truncated = truncated; - this.commentIds = commentIds; - } - - public int getSource() { - return source; - } - - public int getDestination() { - return destination; - } - - public String getLine() { - return line; - } - - public boolean isTruncated() { - return truncated; - } - - public List getCommentIds() { - return commentIds; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffPage.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffPage.java deleted file mode 100644 index 55fba904c..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/DiffPage.java +++ /dev/null @@ -1,59 +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.diff; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class DiffPage implements Serializable { - private final String fromHash; - - private final String toHash; - - private final boolean truncated; - - private final List diffs; - - @JsonCreator - public DiffPage(@JsonProperty("fromHash") final String fromHash, @JsonProperty("toHash") final String toHash, @JsonProperty("truncated") final boolean truncated, @JsonProperty("diffs") final List diffs) { - this.fromHash = fromHash; - this.toHash = toHash; - this.truncated = truncated; - this.diffs = diffs; - } - - public String getFromHash() { - return fromHash; - } - - public String getToHash() { - return toHash; - } - - public boolean isTruncated() { - return truncated; - } - - public List getDiffs() { - return diffs; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/File.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/File.java deleted file mode 100644 index 71487d2a7..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/File.java +++ /dev/null @@ -1,66 +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.diff; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class File implements Serializable { - private final String parent; - - private final String name; - - private final String extension; - - private final String toString; - - private final List components; - - @JsonCreator - public File(@JsonProperty("parent") final String parent, @JsonProperty("name") final String name, @JsonProperty("extension") final String extension, @JsonProperty("toString") final String toString, @JsonProperty("components") final List components) { - this.parent = parent; - this.name = name; - this.extension = extension; - this.toString = toString; - this.components = components; - } - - public String getParent() { - return parent; - } - - public String getName() { - return name; - } - - public String getExtension() { - return extension; - } - - public String getToString() { - return toString; - } - - public List getComponents() { - return components; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Hunk.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Hunk.java deleted file mode 100644 index 9f64faa4c..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/response/diff/Hunk.java +++ /dev/null @@ -1,73 +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.diff; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Hunk implements Serializable { - private final String context; - - private final int sourceLine; - - private final int sourceSpan; - - private final int destinationLine; - - private final int destinationSpan; - - private final List segments; - - @JsonCreator - public Hunk(@JsonProperty("context") final String context, @JsonProperty("sourceLine") final int sourceLine, @JsonProperty("sourceSpan") final int sourceSpan, @JsonProperty("destinationLine") final int destinationLine, @JsonProperty("destinationSpan") final int destinationSpan, @JsonProperty("segments") final List segments) { - this.context = context; - this.sourceLine = sourceLine; - this.sourceSpan = sourceSpan; - this.destinationLine = destinationLine; - this.destinationSpan = destinationSpan; - this.segments = segments; - } - - public String getContext() { - return context; - } - - public int getSourceLine() { - return sourceLine; - } - - public int getSourceSpan() { - return sourceSpan; - } - - public int getDestinationLine() { - return destinationLine; - } - - public int getDestinationSpan() { - return destinationSpan; - } - - public List getSegments() { - return segments; - } -} 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 deleted file mode 100644 index 48cc29cb4..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecorator.java +++ /dev/null @@ -1,319 +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.server; - -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.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; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.Hunk; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.response.diff.Segment; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; -import org.apache.commons.io.IOUtils; -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; -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.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; -import org.sonar.core.issue.DefaultIssue; - -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; -import java.util.Optional; -import java.util.stream.Collectors; - -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 = "sonar.pullrequest.bitbucket.projectKey"; - - public static final String PULL_REQUEST_BITBUCKET_USER_SLUG = "sonar.pullrequest.bitbucket.userSlug"; - - public static final String PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG = "sonar.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; - - @Override - public void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration) { - LOGGER.info("starting to analyze with " + analysisDetails.toString()); - - try { - final String hostURL = configuration.getRequiredServerProperty(PULL_REQUEST_BITBUCKET_URL); - final String apiToken = configuration.getRequiredServerProperty(PULL_REQUEST_BITBUCKET_TOKEN); - final String repositorySlug = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG); - final String pullRequestId = analysisDetails.getBranchName(); - final String userSlug = configuration.getProperty(PULL_REQUEST_BITBUCKET_USER_SLUG).orElse(StringUtils.EMPTY); - final String projectKey = configuration.getProperty(PULL_REQUEST_BITBUCKET_PROJECT_KEY).orElse(StringUtils.EMPTY); - final String commentUserSlug = configuration.getServerProperty(PULL_REQUEST_BITBUCKET_COMMENT_USER_SLUG).orElse(StringUtils.EMPTY); - - final boolean summaryCommentEnabled = Boolean.parseBoolean(configuration.getRequiredServerProperty(PULL_REQUEST_COMMENT_SUMMARY_ENABLED)); - final boolean fileCommentEnabled = Boolean.parseBoolean(configuration.getRequiredServerProperty(PULL_REQUEST_FILE_COMMENT_ENABLED)); - final boolean deleteCommentsEnabled = Boolean.parseBoolean(configuration.getRequiredServerProperty(PULL_REQUEST_DELETE_COMMENTS_ENABLED)); - - 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); - - 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()); - for (PostAnalysisIssueVisitor.ComponentIssue componentIssue : componentIssues) { - final DefaultIssue issue = componentIssue.getIssue(); - String analysisIssueSummary = analysisDetails.createAnalysisIssueSummary(componentIssue, new MarkdownFormatterFactory()); - String issuePath = analysisDetails.getSCMPathForIssue(componentIssue).orElse(StringUtils.EMPTY); - int issueLine = issue.getLine() != null ? issue.getLine() : 0; - String issueType = getIssueType(diffPage, issuePath, issueLine); - String fileType = "TO"; - if (issueType.equals("CONTEXT")) { - fileType = "FROM"; - } - StringEntity fileCommentEntity = new StringEntity( - new ObjectMapper().writeValueAsString(new FileComment(analysisIssueSummary, new Anchor(issueLine, issueType, issuePath, fileType))), ContentType.APPLICATION_JSON - ); - postComment(commentUrl, headers, fileCommentEntity, fileCommentEnabled); - } - } catch (IOException ex) { - throw new IllegalStateException("Could not decorate Pull Request on Bitbucket Server", ex); - } - - } - - protected String getIssueType(DiffPage diffPage, String issuePath, int issueLine) { - String issueType = "CONTEXT"; - List diffs = diffPage.getDiffs().stream() - .filter(diff -> diff.getDestination() != null) - .filter(diff -> issuePath.equals(diff.getDestination().getToString())) - .collect(Collectors.toList()); - - if (!diffs.isEmpty()) { - for (Diff diff : diffs) { - List hunks = diff.getHunks(); - if (!hunks.isEmpty()) { - issueType = getExtractIssueType(issueLine, issueType, hunks); - } - } - } - return issueType; - } - - private String getExtractIssueType(int issueLine, String issueType, List hunks) { - for (Hunk hunk : hunks) { - List segments = hunk.getSegments(); - for (Segment segment : segments) { - Optional optionalLine = segment.getLines().stream().filter(diffLine -> diffLine.getDestination() == issueLine).findFirst(); - if (optionalLine.isPresent()) { - issueType = segment.getType(); - break; - } - } - } - 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()) { - LOGGER.debug(String.format("Getting page %s", type)); - HttpGet httpGet = new HttpGet(diffUrl); - for (Map.Entry entry : headers.entrySet()) { - httpGet.addHeader(entry.getKey(), entry.getValue()); - } - HttpResponse httpResponse = closeableHttpClient.execute(httpGet); - if (null == httpResponse) { - LOGGER.error(String.format("HttpResponse for getting page %s was null", type)); - } else if (httpResponse.getStatusLine().getStatusCode() != 200) { - HttpEntity entity = httpResponse.getEntity(); - LOGGER.error("Error response from Bitbucket: " + IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name())); - throw new IllegalStateException(String.format("Error response returned from Bitbucket Server. Expected HTTP Status 200 but got %s", httpResponse.getStatusLine().getStatusCode()) ); - } else { - HttpEntity entity = httpResponse.getEntity(); - page = 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.name()), type); - LOGGER.debug(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(page)); - } - } catch (IOException ex) { - LOGGER.error(String.format("Could not get %s from Bitbucket Server", type.getName()), ex); - } - return type.cast(page); - } - - protected boolean postComment(String commentUrl, Map headers, StringEntity requestEntity, boolean sendRequest) throws IOException { - boolean commentPosted = false; - HttpPost httpPost = new HttpPost(commentUrl); - for (Map.Entry entry : headers.entrySet()) { - httpPost.addHeader(entry.getKey(), entry.getValue()); - } - 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; - } - } - } - return commentPosted; - } - - @Override - public String name() { - return "BitbucketServer"; - } -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerConfigurationLoaderSensor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerConfigurationLoaderSensor.java index 0896fae45..96f241073 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerConfigurationLoaderSensor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/ScannerConfigurationLoaderSensor.java @@ -19,7 +19,7 @@ package com.github.mc1arke.sonarqube.plugin.scanner; import com.github.mc1arke.sonarqube.plugin.CommunityBranchPlugin; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.server.BitbucketServerPullRequestDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4.GraphqlCheckRunProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabServerPullRequestDecorator; import com.google.common.collect.Sets; diff --git a/src/main/resources/static/common/icon.png b/src/main/resources/static/common/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bc84c16605bd3ebca1d316f09c33cc111f43d8ef GIT binary patch literal 4601 zcmX|F2{cr1*dL5#Xc&7@WX~>Z*~(IuFpFbX$R4$(sE{^l+e8 zp@;8FqU#km0CZiQ@LDISyooIV-YNTV{It|9HBH6+ z+6X__kgr_$t04fc@YKW1G3lf6^#roG$>nMIu3!X~DCY)@eP&Fd+ zu;|{knoB&FR13|gy_o2+SGr8kY;RC*f|;q&TM=`c@hCDD{VqJ>yVfdZgIiqev){$Q(Xouc zP=}m}RE(*faFc6NK@8h;ax*_FQVD*9GJi1q1h0D@YFh`Fj794j7H?wF1UELH8mK_? zJ@ol>R7=7k8mo;G_l4M}r8FXdYW~w#I>5EW>%6KR7HN>P^jGwA1UKIIxIB6F_mMK{ zrPh}V;192iAS)k5-M(a1uBJ!b!=gn@K0PLhvQDA`2@CEMgL0u#_uUlTiCIxFL^Q3&PF|(N}k}_H=@s6 zpBSvc^4;$cU2>ZwW^9aCo1&oXQ?wphIUZU@y=kbl;@XMevGkp$h&*%lnK=ayvs2?dzQ;>ZNekd1C$;~)6blCL`s7GxsTAp~q9^Z! zHjVQ6nvAkzXRoBi_g^Ab{{0;}*rnoRG|En`Swr=n4`GSKN}S-?G(kOG0P7Yx>&fdO z)K0u`;l6n6lW9OX7F`twmle$5k9V2X7Uo(8T*(QR47qX2usrfSzQp5ugsJ2#NyKK5 zs=G=FC|3R!`1@5;`f7!E#&e~>*?|O5Pb}O_a6{OjKGYVj?vVD?7N=1?<2>_Ed@E>*1!Jex&W z^oHSN&x`EM%XMb4@Ro-clzY6pO-d(2$nPiSi9w2oA4j;ITE{*%|8W478oc=1kbo&A zaxw<3zD*X;&a_cjI^^7*@lXgZJYyuXjnaz-cQf^2RsQ@%&R=}zR<^m-xcxwuA{pxL z8!SF!F6o%pZLJBH>!sP+lldCDaUv(^jSV-2VjGQVN-|PaiCb+HAEaTojHMLXlzw_r zVzQp*ex^;4%|EHy&%kOk0eFTT!;Fhf=5v&FH_PS*L2f4O7uEi@dWwQAIldnGIYacgI9{zbSBRVBK_!FLcnw_Zs6 zB)wm>!{6bq5ci|hNSY-qT6n#yw9i5^p7SKk;~%+sb1DXFcGB3&fK>2VPv#z1?c`ZZ zzZErv7=AuEcJ|oInCG7`_5MOQHNBzw^&?Pwz8X-xJ4Hv9<;C14TO$c-I|jH6Ec*7J z_@E(4Pt6F2djUB~-rGmZ_gb-`%<$hL_xUi%sla#$5P54nObsOJZMxF3VG+8v9x(3xFr}ebW zdoj-mY6KkS-4*@M<_^or6}sB@Ehwv zD%dAo`eKD19an@7n6xTk&sB_1L1`JsPXbDYwqV{>K^qT2RJrQ$T7Eml_~o|rD9#^{E({$|JWe*qXTUWV84Ww zq1(M^ZBjwkqAbW0=++|Tek_Utad7=p*t zvSaP!#NgRO;otLz@y&|SW&=*s(;9`1LkG%thQzP5U ziemV@tG6Llsb?*s@)~^Z{a&%e;gTw9gM7r9bzUJ+g6L2SFL{wzfm=nN>QCkBWriru zT#HuaQW34yZw$To`5agLqL)l`+MUR9p@5>_fI#3B+$95D2fD3aB$*NrWJ-}7+-^g}4Z#G73 z0EZOI-^mY}sfO%`_fkV^*&ap{gIArSn72(^Z*=&u=@v%94W;8VTu0#Z^> zi+%Y=!#c79>vRC+r#=d5du=@UPrl^ZWhSs{17Pt&}VZpwC)Q!chL3XI`>hNV zoBZGDl&izBHGeR15xzxEQ>l6SA4}UPi0;bHud95@_KER;s7MebZJJ27>6umZP&^GINomdreu2a7E;SL4s^$D@GRgCU`OQy5NHVe5bfm6pW=* zXTYx;b(pl_*aBozUGa}lm%e0o_>(_uN0l{xBv84Ovu?7rSr9VR{pS~vCe6h)xmhEV z^U!y-zrk8cWJz0Ulm##S!i}hlQoN@NfF&JUCA0+4w&@5f5%dTExh6Xt4q_@0F z?22ut2%FcgA(<1LNV4RGBOb}5Po|L;?9_wxKDO!kjdL)K0+(pP?McVtW+H#ja4xS# zeO1t9_Q2`F$j@~ddJb|8x&v!v=0UzrNQiB)7Se2+$gjyPR;RA>UCm-hRP!VkU|D7K zmL~{g$*dO>dh%;0lIM;W$8RS!s^jpS7c+bX#e3VJpV25DHu^r03c7xzm)e$t!#qDh76fNB&Irr-g zWAS%hGYEjyjkBB(7X5*WM#Pv;hl+0WYfG(^YJHaL>wsAJZE9D)j!d=H1IqS>9Qg)) zhz%Knlzl4l07xkQLW4eY3TlGU{j(;rf`W1a9Y6Ggq+G9c^8&h`b0$kv`wK$G+awOY znuMa-)wK}&EFIecm3fywC*0l3w*LnSLeWyJJ&?hN_& z3^}n4RxRYfSK^Vh1sj6M^^-ee;Xn9d)#x)%#T30v0?U$^bXDGeCH$GMcE#0zMdHNL zvIfxNNfBNJZIL}KIA~NPpLV{h>Xre{b==cQfMFhZTk3>S2ZkEF=y{g@E+<6JQzdqy zu|c-u?kl-wX5Pl8_`+>c?;p%wa!fg}(dd3=Bkivz@cn*)w0O?MVe&gb$A(k;#+=%{ zhalTmZjG+mrZj(?DlxZt0;^74D+GApFfqnHs)2R97jwq6$=y9Q|VYj_A_+*j&9x*y;Fj?Gcg!tPsI4>20a$`v8TBXL4^C* z&y=t4w}iZqVgy|6dFG*B!4G3)`xHnid1AzF#&j1zO%9j1r8^KY@mOT9j^(t4SFL8f z;V4dB&$41G7U;AkL>SCRbuyW+^x6aNM~q9}Kf^gd_7VE*e7q`JuN4E^MUxE_HK*(+ zsWJ5qE2y#dbRQwix#Oj7RZ^Nty?c&yRd(M5do8=SIbh$ic+@r2*Sz6~^9;|P_vKUm0JQvh-*}FGPexNN_HWW(wNq^&a zcI)84a-TyJxewu5(Xo3+7Zqq>3ZP(5|5GW%_Sofy1#o-l3aV07O6*Qhkk8#;)-RSH zT-YasWDP5upYMt5X>}RRcbZN_S!h0Xl9k&oTmah*D{#skL;m_x*YyV}45krgYWGc1T6Rt=DZYG{-NPyN43@f-gGFM|?9&;}3@=BcLd*TFr?J>|l zNQkIDgJ`U=)R^-|kOD`dM4*BPvutM`LI-=WtMWKD2H}cY;Kgp!&-LpCsrRnmYd`w% z9n9%GmI$_I4pm7n>|_Fuc6-Coym z4mJwF3expTC%edg)&h(-zL-dptYXW1u##D=5^;nSoQi#vrM4GLvnL1J>^BXuxcj?E&iy9{oi57{~Lnp b$G=<_pz{hrn|;B+Aq~jkjI(WxHHz>*ydk}g literal 0 HcmV?d00001 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 3af011a47..b77039317 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -119,7 +119,7 @@ public void testServerSideLoad() { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); - assertEquals(23, argumentCaptor.getAllValues().size()); + assertEquals(22, argumentCaptor.getAllValues().size()); assertEquals(Arrays.asList(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class), argumentCaptor.getAllValues().subList(0, 2)); 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 5737399a3..16a2b7622 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(9, result.size()); + assertEquals(10, result.size()); assertEquals(CommunityBranchLoaderDelegate.class, result.get(0)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java new file mode 100644 index 000000000..fce8a3990 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java @@ -0,0 +1,153 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket; + +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.UnifyConfiguration; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient; +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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportAttributes; +import org.sonar.core.issue.DefaultIssue; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BitbucketPullRequestDecoratorTest { + + private static final String PROJECT = "project"; + private static final String REPO = "repo"; + private static final String COMMIT = "commit"; + + private static final String ISSUE_KEY = "issue-key"; + private static final int ISSUE_LINE = 1; + private static final String ISSUE_LINK = "https://issue-link"; + private static final String ISSUE_MESSAGE = "issue message"; + private static final String ISSUE_PATH = "/issue/path"; + private static final String DASHBOARD_URL = "https://dashboard-url"; + private static final String IMAGE_URL = "https://image-url"; + + private AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + private UnifyConfiguration unifyConfiguration = mock(UnifyConfiguration.class); + + private BitbucketClient client = mock(BitbucketClient.class); + + private BitbucketServerPullRequestDecorator underTest = new BitbucketServerPullRequestDecorator(client); + + @Before + public void setUp() { + when(unifyConfiguration.getRequiredProperty(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_PROJECT_KEY)).thenReturn(PROJECT); + when(unifyConfiguration.getRequiredProperty(BitbucketServerPullRequestDecorator.PULL_REQUEST_BITBUCKET_REPOSITORY_SLUG)).thenReturn(REPO); + } + + @Test + public void testValidAnalysis() throws IOException { + when(client.supportsCodeInsights()).thenReturn(true); + + mockValidAnalysis(); + final ArgumentCaptor reportCaptor = ArgumentCaptor.forClass(CreateReportRequest.class); + final ArgumentCaptor annotationsCaptor = ArgumentCaptor.forClass(CreateAnnotationsRequest.class); + + underTest.decorateQualityGateStatus(analysisDetails, unifyConfiguration); + + verify(client).createReport(eq(PROJECT), eq(REPO), eq(COMMIT), reportCaptor.capture()); + verifyExpectedReport(reportCaptor.getValue()); + + verify(client).deleteAnnotations(PROJECT, REPO, COMMIT); + verify(client).createAnnotations(eq(PROJECT), eq(REPO), eq(COMMIT), annotationsCaptor.capture()); + + CreateAnnotationsRequest actualAnnotations = annotationsCaptor.getValue(); + assertThat(actualAnnotations.getAnnotations()).size().isEqualTo(1); + verifyExpectedAnnotation(actualAnnotations.getAnnotations().iterator().next()); + } + + private void mockValidAnalysis() { + when(analysisDetails.getCommitSha()).thenReturn(COMMIT); + when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); + + Map ruleCount = new HashMap<>(); + ruleCount.put(RuleType.CODE_SMELL, 1L); + ruleCount.put(RuleType.VULNERABILITY, 2L); + ruleCount.put(RuleType.SECURITY_HOTSPOT, 3L); + ruleCount.put(RuleType.BUG, 4L); + + when(analysisDetails.countRuleByType()).thenReturn(ruleCount); + when(analysisDetails.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY)).thenReturn(Optional.empty()); + when(analysisDetails.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY)).thenReturn(Optional.empty()); + when(analysisDetails.getAnalysisDate()).thenReturn(Date.from(Instant.now())); + when(analysisDetails.getDashboardUrl()).thenReturn(DASHBOARD_URL); + + ReportAttributes reportAttributes = mock(ReportAttributes.class); + when(reportAttributes.getScmPath()).thenReturn(Optional.of(ISSUE_PATH)); + + Component component = mock(Component.class); + when(component.getType()).thenReturn(Component.Type.FILE); + when(component.getReportAttributes()).thenReturn(reportAttributes); + + DefaultIssue defaultIssue = mock(DefaultIssue.class); + when(defaultIssue.status()).thenReturn(Issue.STATUS_OPEN); + when(defaultIssue.severity()).thenReturn(Severity.CRITICAL); + when(defaultIssue.getLine()).thenReturn(ISSUE_LINE); + when(defaultIssue.key()).thenReturn(ISSUE_KEY); + when(defaultIssue.type()).thenReturn(RuleType.BUG); + when(defaultIssue.getMessage()).thenReturn(ISSUE_MESSAGE); + when(analysisDetails.getIssueUrl(ISSUE_KEY)).thenReturn(ISSUE_LINK); + when(analysisDetails.getBaseImageUrl()).thenReturn(IMAGE_URL); + + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue.getIssue()).thenReturn(defaultIssue); + when(componentIssue.getComponent()).thenReturn(component); + + PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + + when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); + } + + private void verifyExpectedReport(CreateReportRequest actual) { + assertThat(actual.getTitle()).isEqualTo("SonarQube"); + assertThat(actual.getResult()).isEqualTo("PASS"); + assertThat(actual.getReporter()).isEqualTo("SonarQube"); + assertThat(actual.getCreatedDate()).isBetween(Instant.now().minus(1, ChronoUnit.MINUTES), Instant.now()); + assertThat(actual.getDetails()).isEqualTo("Quality Gate passed" + System.lineSeparator()); + assertThat(actual.getLink()).isEqualTo(DASHBOARD_URL); + assertThat(actual.getLogoUrl()).isEqualTo(String.format("%s/common/icon.png", IMAGE_URL)); + + assertThat(actual.getData()).size().isEqualTo(6); + } + + private void verifyExpectedAnnotation(Annotation actual) { + assertThat(actual.getExternalId()).isEqualTo(ISSUE_KEY); + assertThat(actual.getLine()).isEqualTo(ISSUE_LINE); + assertThat(actual.getLink()).isEqualTo(ISSUE_LINK); + assertThat(actual.getMessage()).isEqualTo(ISSUE_MESSAGE); + assertThat(actual.getPath()).isEqualTo(ISSUE_PATH); + assertThat(actual.getSeverity()).isEqualTo("HIGH"); + assertThat(actual.getType()).isEqualTo("BUG"); + } +} 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 deleted file mode 100644 index 52156ec4f..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/server/BitbucketServerPullRequestDecoratorTest.java +++ /dev/null @@ -1,231 +0,0 @@ -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; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.InjectMocks; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -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; - -public class BitbucketServerPullRequestDecoratorTest { - - @Rule - public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8089)); - - @InjectMocks - private BitbucketServerPullRequestDecorator bitbucketServerPullRequestDecorator; - - private Map headers; - - /** - * 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(); - - 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"))) - ) - ); - 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"))) - ) - ); - DiffPage diffPage = bitbucketServerPullRequestDecorator.getPage(DIFFURL, headers, DiffPage.class); - - // wrong file - String issueType = bitbucketServerPullRequestDecorator.getIssueType(diffPage, "src/DoesNotExist.java", 15); - assertThat(issueType, is("CONTEXT")); - - // line not within diff - issueType = bitbucketServerPullRequestDecorator.getIssueType(diffPage, "src/com/sonar/sample/classes/ClassWithInvalidMethodName.java", 0); - assertThat(issueType, is("CONTEXT")); - - issueType = bitbucketServerPullRequestDecorator.getIssueType(diffPage, "src/com/sonar/sample/classes/ClassWithInvalidMethodName.java", 15); - assertThat(issueType, is("ADDED")); - } - - @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)); - } -}