From d76df7dc5f2d0bf75bfbcb5097aedb26dbdd2276 Mon Sep 17 00:00:00 2001 From: Guillaume Jambet Date: Tue, 29 May 2018 14:53:21 +0200 Subject: [PATCH] SONAR-10467 scanner report link support branches and pull requests --- .../sonar/scanner/report/ReportPublisher.java | 103 +++++++++++++++--- .../scanner/report/ReportPublisherTest.java | 75 ++++++++++++- 2 files changed, 160 insertions(+), 18 deletions(-) diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java index d9f36877aac..714199de09c 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java @@ -56,10 +56,14 @@ import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsResponse; import static java.net.URLEncoder.encode; +import static org.apache.commons.lang.StringUtils.EMPTY; +import static org.apache.commons.lang.StringUtils.isBlank; import static org.sonar.core.config.ScannerProperties.BRANCH_NAME; import static org.sonar.core.config.ScannerProperties.ORGANIZATION; import static org.sonar.core.util.FileUtils.deleteQuietly; +import static org.sonar.scanner.scan.branch.BranchType.LONG; import static org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST; +import static org.sonar.scanner.scan.branch.BranchType.SHORT; @ScannerSide public class ReportPublisher implements Startable { @@ -71,6 +75,11 @@ public class ReportPublisher implements Startable { public static final String METADATA_DUMP_FILENAME = "report-task.txt"; private static final String CHARACTERISTIC = "characteristic"; + private static final String DASHBOARD = "dashboard"; + private static final String BRANCH = "branch"; + private static final String ID = "id"; + private static final String RESOLVED = "resolved"; + private final Configuration settings; private final ScannerWsClient wsClient; private final AnalysisContextReportPublisher contextPublisher; @@ -213,32 +222,21 @@ public class ReportPublisher implements Startable { if (taskId == null) { LOG.info("ANALYSIS SUCCESSFUL"); } else { - String publicUrl = server.getPublicRootUrl(); - HttpUrl httpUrl = HttpUrl.parse(publicUrl); Map metadata = new LinkedHashMap<>(); String effectiveKey = moduleHierarchy.root().getKeyWithBranch(); settings.get(ORGANIZATION).ifPresent(org -> metadata.put("organization", org)); metadata.put("projectKey", effectiveKey); - metadata.put("serverUrl", publicUrl); + metadata.put("serverUrl", server.getPublicRootUrl()); metadata.put("serverVersion", server.getVersion()); - settings.get(BRANCH_NAME).ifPresent(branch -> metadata.put("branch", branch)); - - URL dashboardUrl; - try { - dashboardUrl = httpUrl.newBuilder() - .addPathSegment("dashboard") - .addEncodedQueryParameter("id", encode(effectiveKey, "UTF-8")) - .build() - .url(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("Unable to urlencode " + effectiveKey, e); - } + settings.get(BRANCH_NAME).ifPresent(branch -> metadata.put(BRANCH, branch)); + + URL dashboardUrl = buildDashboardUrl(server.getPublicRootUrl(), effectiveKey); metadata.put("dashboardUrl", dashboardUrl.toExternalForm()); - URL taskUrl = HttpUrl.parse(publicUrl).newBuilder() + URL taskUrl = HttpUrl.parse(server.getPublicRootUrl()).newBuilder() .addPathSegment("api").addPathSegment("ce").addPathSegment("task") - .addQueryParameter("id", taskId) + .addQueryParameter(ID, taskId) .build() .url(); metadata.put("ceTaskId", taskId); @@ -252,6 +250,77 @@ public class ReportPublisher implements Startable { } } + private URL buildDashboardUrl(String publicUrl, String effectiveKey) { + HttpUrl httpUrl = HttpUrl.parse(publicUrl); + + if (onPullRequest(branchConfiguration)) { + return httpUrl.newBuilder() + .addPathSegment("project") + .addPathSegment("issues") + .addEncodedQueryParameter(ID, encoded(effectiveKey)) + .addEncodedQueryParameter("pullRequest", encoded(branchConfiguration.pullRequestKey())) + .addQueryParameter(RESOLVED, "false") + .build() + .url(); + } + + if (onLongLivingBranch(branchConfiguration)) { + return httpUrl.newBuilder() + .addPathSegment(DASHBOARD) + .addEncodedQueryParameter(ID, encoded(effectiveKey)) + .addEncodedQueryParameter(BRANCH, encoded(branchConfiguration.branchName())) + .build() + .url(); + } + + if (onShortLivingBranch(branchConfiguration)) { + return httpUrl.newBuilder() + .addPathSegment(DASHBOARD) + .addEncodedQueryParameter(ID, encoded(effectiveKey)) + .addEncodedQueryParameter(BRANCH, encoded(branchConfiguration.branchName())) + .addQueryParameter(RESOLVED, "false") + .build() + .url(); + } + + if (onMainBranch(branchConfiguration)) { + return httpUrl.newBuilder() + .addPathSegment(DASHBOARD) + .addEncodedQueryParameter(ID, encoded(effectiveKey)) + .build() + .url(); + } + + return httpUrl.newBuilder().build().url(); + } + + private static boolean onPullRequest(BranchConfiguration branchConfiguration) { + return branchConfiguration.branchName() != null && (branchConfiguration.branchType() == PULL_REQUEST); + } + + private static boolean onShortLivingBranch(BranchConfiguration branchConfiguration) { + return branchConfiguration.branchName() != null && (branchConfiguration.branchType() == SHORT); + } + + private static boolean onLongLivingBranch(BranchConfiguration branchConfiguration) { + return branchConfiguration.branchName() != null && (branchConfiguration.branchType() == LONG); + } + + private static boolean onMainBranch(BranchConfiguration branchConfiguration) { + return branchConfiguration.branchName() == null; + } + + private static String encoded(@Nullable String queryParameter) { + if (isBlank(queryParameter)){ + return EMPTY; + } + try { + return encode(queryParameter, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Unable to urlencode " + queryParameter, e); + } + } + private void dumpMetadata(Map metadata) { Path file = moduleHierarchy.root().getWorkDir().resolve(METADATA_DUMP_FILENAME); try (Writer output = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java index ccc9d46b1ce..9213c5d0146 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java @@ -59,6 +59,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.scanner.scan.branch.BranchType.LONG; import static org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST; import static org.sonar.scanner.scan.branch.BranchType.SHORT; @@ -130,7 +131,7 @@ public class ReportPublisherTest { } @Test - public void log_public_url_if_defined() throws IOException { + public void log_public_url_if_defined_for_main_branch() throws IOException { when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); ReportPublisher underTest = new ReportPublisher(settings.asConfig(), wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), new ReportPublisherStep[0], branchConfiguration); @@ -151,6 +152,78 @@ public class ReportPublisherTest { "ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123\n"); } + @Test + public void log_public_url_if_defined_for_long_living_branches() throws IOException { + when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); + when(branchConfiguration.branchType()).thenReturn(LONG); + when(branchConfiguration.branchName()).thenReturn("branch-6.7"); + ReportPublisher underTest = new ReportPublisher(settings.asConfig(), wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), + new ReportPublisherStep[0], branchConfiguration); + + underTest.logSuccess("TASK-123"); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("ANALYSIS SUCCESSFUL, you can browse https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&branch=branch-6.7") + .contains("More about the report processing at https://publicserver/sonarqube/api/ce/task?id=TASK-123"); + + File detailsFile = new File(temp.getRoot(), "report-task.txt"); + assertThat(readFileToString(detailsFile)).isEqualTo( + "projectKey=org.sonarsource.sonarqube:sonarqube\n" + + "serverUrl=https://publicserver/sonarqube\n" + + "serverVersion=6.4\n" + + "dashboardUrl=https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&branch=branch-6.7\n" + + "ceTaskId=TASK-123\n" + + "ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123\n"); + } + + @Test + public void log_public_url_if_defined_for_short_living_branches() throws IOException { + when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); + when(branchConfiguration.branchType()).thenReturn(SHORT); + when(branchConfiguration.branchName()).thenReturn("branch-6.7"); + ReportPublisher underTest = new ReportPublisher(settings.asConfig(), wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), + new ReportPublisherStep[0], branchConfiguration); + + underTest.logSuccess("TASK-123"); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("ANALYSIS SUCCESSFUL, you can browse https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&branch=branch-6.7&resolved=false") + .contains("More about the report processing at https://publicserver/sonarqube/api/ce/task?id=TASK-123"); + + File detailsFile = new File(temp.getRoot(), "report-task.txt"); + assertThat(readFileToString(detailsFile)).isEqualTo( + "projectKey=org.sonarsource.sonarqube:sonarqube\n" + + "serverUrl=https://publicserver/sonarqube\n" + + "serverVersion=6.4\n" + + "dashboardUrl=https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&branch=branch-6.7&resolved=false\n" + + "ceTaskId=TASK-123\n" + + "ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123\n"); + } + + @Test + public void log_public_url_if_defined_for_pull_request() throws IOException { + when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); + when(branchConfiguration.branchName()).thenReturn("Bitbucket cloud Widget"); + when(branchConfiguration.branchType()).thenReturn(PULL_REQUEST); + when(branchConfiguration.pullRequestKey()).thenReturn("105"); + + ReportPublisher underTest = new ReportPublisher(settings.asConfig(), wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), + new ReportPublisherStep[0], branchConfiguration); + + underTest.logSuccess("TASK-123"); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("ANALYSIS SUCCESSFUL, you can browse https://publicserver/sonarqube/project/issues?id=org.sonarsource.sonarqube%3Asonarqube&pullRequest=105&resolved=false") + .contains("More about the report processing at https://publicserver/sonarqube/api/ce/task?id=TASK-123"); + + File detailsFile = new File(temp.getRoot(), "report-task.txt"); + assertThat(readFileToString(detailsFile)).isEqualTo( + "projectKey=org.sonarsource.sonarqube:sonarqube\n" + + "serverUrl=https://publicserver/sonarqube\n" + + "serverVersion=6.4\n" + + "dashboardUrl=https://publicserver/sonarqube/project/issues?id=org.sonarsource.sonarqube%3Asonarqube&pullRequest=105&resolved=false\n" + + "ceTaskId=TASK-123\n" + + "ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123\n"); + } + @Test public void fail_if_public_url_malformed() { when(server.getPublicRootUrl()).thenReturn("invalid"); -- 2.39.5