Skip to content

Commit

Permalink
#67: Link back to the Pull Request from Sonarqube
Browse files Browse the repository at this point in the history
Sonarqube provides the ability to link back to the original Pull Request or Merge Request from the Pull Request overview screen if the Pull Request URL is persisted alongside the branch details. To allow this, decorators are now required to return a `DecorationResult` that contains an optional URL for the Pull Request's overview/summary page. The Gitlab and Github decorators have been updated to provide the new details, however the Bitbucket decorator defaults to returning no URL since the details are not readily available, so Bitbucket Pull Requests will continue to show no link back the the Pull Request from Sonarqube.
  • Loading branch information
mc1arke committed May 31, 2020
1 parent 2bf096a commit f09ac56
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2020 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest;

import java.util.Optional;

public final class DecorationResult {

private final String pullRequestUrl;

private DecorationResult(Builder builder) {
super();
this.pullRequestUrl = builder.pullRequestUrl;
}

public Optional<String> getPullRequestUrl() {
return Optional.ofNullable(pullRequestUrl);
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {

private String pullRequestUrl;

private Builder() {
super();
}

public Builder withPullRequestUrl(String pullRequestUrl) {
this.pullRequestUrl = pullRequestUrl;
return this;
}

public DecorationResult build() {
return new DecorationResult(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ public interface PullRequestBuildStatusDecorator {

String name();

void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration);
DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDao;
import org.sonar.db.component.BranchDto;
import org.sonar.db.protobuf.DbProjectBranches;

import java.util.List;
import java.util.Optional;
Expand All @@ -47,13 +52,14 @@ public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask,
private final MetricRepository metricRepository;
private final MeasureRepository measureRepository;
private final TreeRootHolder treeRootHolder;
private final DbClient dbClient;

public PullRequestPostAnalysisTask(Server server,
ConfigurationRepository configurationRepository,
List<PullRequestBuildStatusDecorator> pullRequestDecorators,
PostAnalysisIssueVisitor postAnalysisIssueVisitor,
MetricRepository metricRepository, MeasureRepository measureRepository,
TreeRootHolder treeRootHolder) {
TreeRootHolder treeRootHolder, DbClient dbClient) {
super();
this.server = server;
this.configurationRepository = configurationRepository;
Expand All @@ -62,6 +68,7 @@ public PullRequestPostAnalysisTask(Server server,
this.metricRepository = metricRepository;
this.measureRepository = measureRepository;
this.treeRootHolder = treeRootHolder;
this.dbClient = dbClient;
}

@Override
Expand Down Expand Up @@ -129,7 +136,9 @@ public void finished(PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) {

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

decorationResult.getPullRequestUrl().ifPresent(pullRequestUrl -> persistPullRequestUrl(pullRequestUrl, projectAnalysis, optionalBranchName.get()));
}

private static Optional<PullRequestBuildStatusDecorator> findCurrentPullRequestStatusDecorator(
Expand All @@ -153,4 +162,20 @@ private static Optional<PullRequestBuildStatusDecorator> findCurrentPullRequestS
LOGGER.warn("No decorator could be found matching " + implementationName);
return Optional.empty();
}

private void persistPullRequestUrl(String pullRequestUrl, ProjectAnalysis projectAnalysis, String branchName) {
try (DbSession dbSession = dbClient.openSession(false)) {
BranchDao branchDao = dbClient.branchDao();
Optional<BranchDto> optionalBranchDto = branchDao
.selectByPullRequestKey(dbSession, projectAnalysis.getProject().getUuid(), branchName);
if (optionalBranchDto.isPresent()) {
BranchDto branchDto = optionalBranchDto.get();
DbProjectBranches.PullRequestData.Builder pullRequestDataBuilder = DbProjectBranches.PullRequestData.newBuilder(branchDto.getPullRequestData());
pullRequestDataBuilder.setUrl(pullRequestUrl);
branchDto.setPullRequestData(pullRequestDataBuilder.build());
branchDao.upsert(dbSession, branchDto);
dbSession.commit();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.UnifyConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
Expand Down Expand Up @@ -67,6 +68,8 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat

private static final int DEFAULT_MAX_ANNOTATIONS = 1000;

private static final DecorationResult DEFAULT_DECORATION_RESULT = DecorationResult.builder().build();

private static final List<String> OPEN_ISSUE_STATUSES =
Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s))
.collect(Collectors.toList());
Expand All @@ -83,11 +86,11 @@ public String name() {
}

@Override
public void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration) {
public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration configuration) {
try {
if(!client.supportsCodeInsights()) {
LOGGER.warn("Your Bitbucket instances does not support the Code Insights API.");
return;
return DEFAULT_DECORATION_RESULT;
}
String project = configuration.getRequiredProperty(PULL_REQUEST_BITBUCKET_PROJECT_KEY);

Expand All @@ -100,6 +103,8 @@ public void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConf
} catch (IOException e) {
LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e);
}

return DEFAULT_DECORATION_RESULT;
}

private CreateReportRequest toReport(AnalysisDetails analysisDetails) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github;

import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.UnifyConfiguration;

import java.io.IOException;
import java.security.GeneralSecurityException;

public interface CheckRunProvider {
void createCheckRun(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) throws IOException, GeneralSecurityException;
DecorationResult createCheckRun(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) throws IOException, GeneralSecurityException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github;

import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.UnifyConfiguration;

Expand All @@ -31,9 +32,9 @@ public GithubPullRequestDecorator(CheckRunProvider checkRunProvider) {
}

@Override
public void decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) {
public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) {
try {
checkRunProvider.createCheckRun(analysisDetails, unifyConfiguration);
return checkRunProvider.createCheckRun(analysisDetails, unifyConfiguration);
} catch (Exception ex) {
throw new IllegalStateException("Could not decorate Pull Request on Github", ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ public class RepositoryAuthenticationToken {

private final String repositoryId;
private final String authenticationToken;
private final String repositoryUrl;

public RepositoryAuthenticationToken(String repositoryId, String authenticationToken) {
public RepositoryAuthenticationToken(String repositoryId, String authenticationToken, String repositoryUrl) {
super();
this.repositoryId = repositoryId;
this.authenticationToken = authenticationToken;
this.repositoryUrl = repositoryUrl;
}

public String getRepositoryId() {
Expand All @@ -36,4 +38,8 @@ public String getRepositoryId() {
public String getAuthenticationToken() {
return authenticationToken;
}

public String getRepositoryUrl() {
return repositoryUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private Optional<RepositoryAuthenticationToken> findRepositoryAuthenticationToke
objectMapper.readerFor(InstallationRepositories.class).readValue(installationRepositoriesReader);
for (Repository repository : installationRepositories.getRepositories()) {
if (projectPath.equals(repository.getFullName())) {
return Optional.of(new RepositoryAuthenticationToken(repository.getNodeId(), appToken.getToken()));
return Optional.of(new RepositoryAuthenticationToken(repository.getNodeId(), appToken.getToken(), repository.getHtmlUrl()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ public class Repository {

private final String nodeId;
private final String fullName;
private final String htmlUrl;

@JsonCreator
public Repository(@JsonProperty("node_id") String nodeId, @JsonProperty("full_name") String fullName) {
public Repository(@JsonProperty("node_id") String nodeId, @JsonProperty("full_name") String fullName, @JsonProperty("html_url") String htmlUrl) {
this.nodeId = nodeId;
this.fullName = fullName;
this.htmlUrl = htmlUrl;
}

public String getFullName() {
Expand All @@ -39,4 +41,8 @@ public String getFullName() {
public String getNodeId() {
return nodeId;
}

public String getHtmlUrl() {
return htmlUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4;

import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
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.github.CheckRunProvider;
Expand Down Expand Up @@ -94,7 +95,7 @@ public GraphqlCheckRunProvider(Clock clock,
}

@Override
public void createCheckRun(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) throws IOException, GeneralSecurityException {
public DecorationResult createCheckRun(AnalysisDetails analysisDetails, UnifyConfiguration unifyConfiguration) throws IOException, GeneralSecurityException {
String apiUrl = unifyConfiguration.getRequiredServerProperty(PULL_REQUEST_GITHUB_URL);
String apiPrivateKey = unifyConfiguration.getRequiredServerProperty(PULL_REQUEST_GITHUB_TOKEN);
String projectPath = unifyConfiguration.getRequiredProperty(PULL_REQUEST_GITHUB_REPOSITORY);
Expand Down Expand Up @@ -165,6 +166,9 @@ public void createCheckRun(AnalysisDetails analysisDetails, UnifyConfiguration u
inputObjectArguments, checkRunOutputContentBuilder, graphQLRequestEntityBuilder);


return DecorationResult.builder()
.withPullRequestUrl(repositoryAuthenticationToken.getRepositoryUrl() + "/pull/" + analysisDetails.getBranchName())
.build();

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
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.UnifyConfiguration;
Expand Down Expand Up @@ -87,7 +88,7 @@ public GitlabServerPullRequestDecorator(Server server, ScmInfoRepository scmInfo
}

@Override
public void decorateQualityGateStatus(AnalysisDetails analysis, UnifyConfiguration unifyConfiguration) {
public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, UnifyConfiguration unifyConfiguration) {
LOGGER.info("starting to analyze with " + analysis.toString());
String revision = analysis.getCommitSha();

Expand All @@ -110,6 +111,8 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, UnifyConfigurati
final String prCommitsURL = mergeRequestURl + "/commits";
final String mergeRequestDiscussionURL = mergeRequestURl + "/discussions";

final String prHtmlUrl = String.format("%s/%s/merge_requests/%s", hostURL, repositorySlug, pullRequestId);

LOGGER.info(String.format("Status url is: %s ", statusUrl));
LOGGER.info(String.format("PR commits url is: %s ", prCommitsURL));
LOGGER.info(String.format("MR discussion url is: %s ", mergeRequestDiscussionURL));
Expand Down Expand Up @@ -188,6 +191,8 @@ public void decorateQualityGateStatus(AnalysisDetails analysis, UnifyConfigurati
}
}
}

return DecorationResult.builder().withPullRequestUrl(prHtmlUrl).build();
} catch (IOException ex) {
throw new IllegalStateException("Could not decorate Pull Request on Gitlab Server", ex);
}
Expand Down
Loading

0 comments on commit f09ac56

Please sign in to comment.