├── images ├── new.png ├── rule.png ├── severity-info.png ├── severity-major.png ├── severity-minor.png ├── severity-blocker.png └── severity-critical.png ├── third-party-licenses.sh ├── NOTICE.txt ├── travis.sh ├── .gitignore ├── .travis.yml ├── README.md ├── src ├── main │ └── java │ │ └── org │ │ └── sonar │ │ └── plugins │ │ └── github │ │ ├── package-info.java │ │ ├── ReportBuilder.java │ │ ├── PullRequestProjectBuilder.java │ │ ├── IssueComparator.java │ │ ├── GitHubPlugin.java │ │ ├── MarkDownReportBuilder.java │ │ ├── MarkDownUtils.java │ │ ├── PullRequestIssuePostJob.java │ │ ├── GlobalReport.java │ │ ├── GitHubPluginConfiguration.java │ │ └── PullRequestFacade.java └── test │ └── java │ └── org │ └── sonar │ └── plugins │ └── github │ ├── GitHubPluginTest.java │ ├── PullRequestProjectBuilderTest.java │ ├── GitHubPluginConfigurationTest.java │ ├── MarkDownReportBuilderTest.java │ ├── PullRequestFacadeTest.java │ ├── PullRequestIssuePostJobTest.java │ └── GlobalReportTest.java ├── pom.xml └── LICENSE.txt /images/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/new.png -------------------------------------------------------------------------------- /images/rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/rule.png -------------------------------------------------------------------------------- /images/severity-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/severity-info.png -------------------------------------------------------------------------------- /images/severity-major.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/severity-major.png -------------------------------------------------------------------------------- /images/severity-minor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/severity-minor.png -------------------------------------------------------------------------------- /images/severity-blocker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/severity-blocker.png -------------------------------------------------------------------------------- /images/severity-critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-github/HEAD/images/severity-critical.png -------------------------------------------------------------------------------- /third-party-licenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mvn org.codehaus.mojo:license-maven-plugin:aggregate-add-third-party -Dlicense.includedScopes=compile 3 | 4 | cat target/generated-sources/license/THIRD-PARTY.txt 5 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | SonarQube :: GitHub Plugin 2 | Copyright (C) 2015-2017 SonarSource SA 3 | mailto:info AT sonarsource DOT com 4 | 5 | This product includes software developed at 6 | SonarSource (http://www.sonarsource.com/). -------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | function configureTravis { 5 | mkdir -p ~/.local 6 | curl -sSL https://github.com/SonarSource/travis-utils/tarball/v55 | tar zx --strip-components 1 -C ~/.local 7 | source ~/.local/bin/install 8 | } 9 | configureTravis 10 | 11 | 12 | export DEPLOY_PULL_REQUEST=true 13 | 14 | regular_mvn_build_deploy_analyze 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---- Maven 2 | target/ 3 | dependency-reduced-pom.xml 4 | 5 | # ---- IntelliJ IDEA 6 | *.iws 7 | *.iml 8 | *.ipr 9 | .idea/ 10 | 11 | # ---- Eclipse 12 | .classpath 13 | .project 14 | .settings 15 | .externalToolBuilders 16 | 17 | # ---- Mac OS X 18 | .DS_Store 19 | Icon? 20 | # Thumbnails 21 | ._* 22 | # Files that might appear on external disk 23 | .Spotlight-V100 24 | .Trashes 25 | 26 | # ---- Windows 27 | # Windows image file caches 28 | Thumbs.db 29 | # Folder config file 30 | Desktop.ini 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | 4 | jdk: 5 | - oraclejdk8 6 | 7 | script: 8 | - ./travis.sh 9 | 10 | cache: 11 | directories: 12 | - $HOME/.m2/repository 13 | - $HOME/.sonar 14 | 15 | notifications: 16 | email: false 17 | webhooks: 18 | - secure: "e6/3Wlr9+ArBpoy+FCwXJ3hJEnHQBe5bnfWyg60SNwqDjJj2TsSQvz2dpp/PCU4sRnf0bzlML+WZ5u7ZsW+qRFMSVu+s5flhdHCNm6cIPwTvikqdXDGquP7GMMsyYlfDyAmgpkeowg3Z2fbgAtOG1ENQIH6eM294skQvAb73yp4=" 19 | on_start: always 20 | 21 | addons: 22 | apt: 23 | packages: 24 | # upgrade java 8 as the default version 1.8.0_31 prevents from compiling sources 25 | # https://github.com/travis-ci/travis-ci/issues/4042 26 | - oracle-java8-installer 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SonarQube GitHub Plugin 2 | 3 | [![Build Status](https://travis-ci.org/SonarSource/sonar-github.svg?branch=master)](https://travis-ci.org/SonarSource/sonar-github) [![Quality Gate](https://next.sonarqube.com/sonarqube/api/project_badges/measure?project=org.sonarsource.auth.github%3Asonar-auth-github-plugin&metric=alert_status)](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.auth.github%3Asonar-auth-github-plugin) 4 | 5 | ## Deprecated 6 | This plugin is deprecated and replace since SonarQube 7.2 by the [Developer Edition](https://redirect.sonarsource.com/editions/developer.html). 7 | 8 | ### License 9 | 10 | Copyright 2015-2017 SonarSource. 11 | 12 | Licensed under the [GNU Lesser General Public License, Version 3.0](http://www.gnu.org/licenses/lgpl.txt) 13 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | @ParametersAreNonnullByDefault 21 | package org.sonar.plugins.github; 22 | 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/GitHubPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import org.junit.Test; 23 | import org.sonar.api.Plugin; 24 | import org.sonar.api.SonarQubeSide; 25 | import org.sonar.api.internal.SonarRuntimeImpl; 26 | import org.sonar.api.utils.Version; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | public class GitHubPluginTest { 31 | 32 | @Test 33 | public void uselessTest() { 34 | Plugin.Context context = new Plugin.Context(SonarRuntimeImpl.forSonarQube(Version.create(6,7), SonarQubeSide.SCANNER)); 35 | new GitHubPlugin().define(context); 36 | assertThat(context.getExtensions().size()).isGreaterThan(1); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/ReportBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.URL; 23 | import javax.annotation.Nullable; 24 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 25 | import org.sonar.api.batch.rule.Severity; 26 | 27 | public interface ReportBuilder { 28 | /** 29 | * Append an object to the report, using its toString() method. 30 | * 31 | * @param o object to append 32 | * @return a reference to this object 33 | */ 34 | ReportBuilder append(Object o); 35 | 36 | /** 37 | * Append a severity image. 38 | * 39 | * @param severity the severity to display 40 | * @return a reference to this object 41 | */ 42 | ReportBuilder append(Severity severity); 43 | 44 | /** 45 | * Register an "extra issue" (not reported on a diff), without appending. 46 | * Note that extra issues are not always included in the final rendered report. 47 | * 48 | * @param issue the extra issue to append 49 | * @param gitHubUrl GitHub URL 50 | * @return a reference to this object 51 | */ 52 | ReportBuilder registerExtraIssue(PostJobIssue issue, @Nullable URL gitHubUrl); 53 | 54 | /** 55 | * Append the registered extra issues. 56 | * 57 | * @return a reference to this object 58 | */ 59 | ReportBuilder appendExtraIssues(); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/PullRequestProjectBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import org.kohsuke.github.GHCommitState; 23 | import org.sonar.api.CoreProperties; 24 | import org.sonar.api.batch.AnalysisMode; 25 | import org.sonar.api.batch.bootstrap.ProjectBuilder; 26 | import org.sonar.api.utils.MessageException; 27 | 28 | /** 29 | * Trigger load of pull request metadata at the very beginning of SQ analysis. Also 30 | * set "in progress" status on the pull request. 31 | * 32 | */ 33 | public class PullRequestProjectBuilder extends ProjectBuilder { 34 | 35 | private final GitHubPluginConfiguration gitHubPluginConfiguration; 36 | private final PullRequestFacade pullRequestFacade; 37 | private final AnalysisMode mode; 38 | 39 | public PullRequestProjectBuilder(GitHubPluginConfiguration gitHubPluginConfiguration, PullRequestFacade pullRequestFacade, AnalysisMode mode) { 40 | this.gitHubPluginConfiguration = gitHubPluginConfiguration; 41 | this.pullRequestFacade = pullRequestFacade; 42 | this.mode = mode; 43 | } 44 | 45 | @Override 46 | public void build(Context context) { 47 | if (!gitHubPluginConfiguration.isEnabled()) { 48 | return; 49 | } 50 | checkMode(); 51 | int pullRequestNumber = gitHubPluginConfiguration.pullRequestNumber(); 52 | pullRequestFacade.init(pullRequestNumber, context.projectReactor().getRoot().getBaseDir()); 53 | 54 | pullRequestFacade.createOrUpdateSonarQubeStatus(GHCommitState.PENDING, "SonarQube analysis in progress"); 55 | } 56 | 57 | private void checkMode() { 58 | if (!mode.isIssues()) { 59 | throw MessageException.of("The GitHub plugin is only intended to be used in preview or issues mode. Please set '" + CoreProperties.ANALYSIS_MODE + "'."); 60 | } 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/IssueComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.util.Comparator; 23 | import java.util.Objects; 24 | import javax.annotation.Nullable; 25 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 26 | import org.sonar.api.batch.rule.Severity; 27 | 28 | public final class IssueComparator implements Comparator { 29 | @Override 30 | public int compare(@Nullable PostJobIssue left, @Nullable PostJobIssue right) { 31 | // Most severe issues should be displayed first. 32 | if (left == right) { 33 | return 0; 34 | } 35 | if (left == null) { 36 | return 1; 37 | } 38 | if (right == null) { 39 | return -1; 40 | } 41 | if (Objects.equals(left.severity(), right.severity())) { 42 | // When severity is the same, sort by component key to at least group issues from 43 | // the same file together. 44 | return compareComponentKeyAndLine(left, right); 45 | } 46 | return compareSeverity(left.severity(), right.severity()); 47 | } 48 | 49 | private static int compareComponentKeyAndLine(PostJobIssue left, PostJobIssue right) { 50 | if (!left.componentKey().equals(right.componentKey())) { 51 | return left.componentKey().compareTo(right.componentKey()); 52 | } 53 | return compareInt(left.line(), right.line()); 54 | } 55 | 56 | private static int compareSeverity(Severity leftSeverity, Severity rightSeverity) { 57 | if (leftSeverity.ordinal() > rightSeverity.ordinal()) { 58 | // Display higher severity first. Relies on Severity.ALL to be sorted by severity. 59 | return -1; 60 | } else { 61 | return 1; 62 | } 63 | } 64 | 65 | private static int compareInt(@Nullable Integer leftLine, @Nullable Integer rightLine) { 66 | if (Objects.equals(leftLine, rightLine)) { 67 | return 0; 68 | } else if (leftLine == null) { 69 | return -1; 70 | } else if (rightLine == null) { 71 | return 1; 72 | } else { 73 | return leftLine.compareTo(rightLine); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/GitHubPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import org.sonar.api.CoreProperties; 23 | import org.sonar.api.Plugin; 24 | import org.sonar.api.Properties; 25 | import org.sonar.api.Property; 26 | import org.sonar.api.PropertyType; 27 | 28 | @Properties({ 29 | @Property( 30 | key = GitHubPlugin.GITHUB_ENDPOINT, 31 | defaultValue = "https://api.github.com", 32 | name = "GitHub API Endpoint", 33 | description = "URL to access GitHub WS API. Default value is fine for public GitHub. Can be modified for GitHub enterprise.", 34 | global = true), 35 | @Property( 36 | key = GitHubPlugin.GITHUB_OAUTH, 37 | name = "GitHub OAuth token", 38 | description = "Authentication token", 39 | global = false, 40 | type = PropertyType.PASSWORD), 41 | @Property( 42 | key = GitHubPlugin.GITHUB_REPO, 43 | name = "GitHub repository", 44 | description = "GitHub repository for this project. Will be guessed from '" + CoreProperties.LINKS_SOURCES_DEV + "' if present", 45 | project = false, 46 | global = false), 47 | @Property( 48 | key = GitHubPlugin.GITHUB_PULL_REQUEST, 49 | name = "GitHub Pull Request", 50 | description = "Pull request number", 51 | project = false, 52 | module = false, 53 | global = false, 54 | type = PropertyType.INTEGER), 55 | @Property( 56 | key = GitHubPlugin.GITHUB_DISABLE_INLINE_COMMENTS, 57 | defaultValue = "false", 58 | name = "Disable issue reporting as inline comments", 59 | description = "Issues will not be reported as inline comments but only in the global summary comment", 60 | project = true, 61 | global = true, 62 | type = PropertyType.BOOLEAN) 63 | }) 64 | public class GitHubPlugin implements Plugin { 65 | 66 | public static final String GITHUB_ENDPOINT = "sonar.github.endpoint"; 67 | public static final String GITHUB_OAUTH = "sonar.github.oauth"; 68 | public static final String GITHUB_REPO = "sonar.github.repository"; 69 | public static final String GITHUB_PULL_REQUEST = "sonar.github.pullRequest"; 70 | public static final String GITHUB_DISABLE_INLINE_COMMENTS = "sonar.github.disableInlineComments"; 71 | 72 | @Override 73 | public void define(Context context) { 74 | context.addExtensions( 75 | PullRequestIssuePostJob.class, 76 | GitHubPluginConfiguration.class, 77 | PullRequestProjectBuilder.class, 78 | PullRequestFacade.class, 79 | MarkDownUtils.class); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/PullRequestProjectBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.io.File; 23 | import org.junit.Before; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import org.junit.rules.ExpectedException; 27 | import org.sonar.api.batch.AnalysisMode; 28 | import org.sonar.api.batch.bootstrap.ProjectBuilder; 29 | import org.sonar.api.config.PropertyDefinitions; 30 | import org.sonar.api.config.Settings; 31 | import org.sonar.api.config.internal.MapSettings; 32 | import org.sonar.api.utils.MessageException; 33 | import org.sonar.api.utils.System2; 34 | 35 | import static org.mockito.Matchers.any; 36 | import static org.mockito.Matchers.eq; 37 | import static org.mockito.Mockito.RETURNS_DEEP_STUBS; 38 | import static org.mockito.Mockito.mock; 39 | import static org.mockito.Mockito.verify; 40 | import static org.mockito.Mockito.verifyZeroInteractions; 41 | import static org.mockito.Mockito.when; 42 | import static org.mockito.Mockito.withSettings; 43 | 44 | public class PullRequestProjectBuilderTest { 45 | 46 | @Rule 47 | public ExpectedException thrown = ExpectedException.none(); 48 | 49 | private PullRequestProjectBuilder pullRequestProjectBuilder; 50 | private PullRequestFacade facade; 51 | private MapSettings settings; 52 | private AnalysisMode mode; 53 | 54 | @Before 55 | public void prepare() { 56 | settings = new MapSettings(new PropertyDefinitions(GitHubPlugin.class)); 57 | facade = mock(PullRequestFacade.class); 58 | mode = mock(AnalysisMode.class); 59 | pullRequestProjectBuilder = new PullRequestProjectBuilder(new GitHubPluginConfiguration(settings, new System2()), facade, mode); 60 | 61 | } 62 | 63 | @Test 64 | public void shouldDoNothing() { 65 | pullRequestProjectBuilder.build(null); 66 | verifyZeroInteractions(facade); 67 | } 68 | 69 | @Test 70 | public void shouldFailIfNotPreview() { 71 | settings.setProperty(GitHubPlugin.GITHUB_PULL_REQUEST, "1"); 72 | 73 | thrown.expect(MessageException.class); 74 | thrown.expectMessage("The GitHub plugin is only intended to be used in preview or issues mode. Please set 'sonar.analysis.mode'."); 75 | 76 | pullRequestProjectBuilder.build(null); 77 | } 78 | 79 | @Test 80 | public void shouldNotFailIfIssues() { 81 | settings.setProperty(GitHubPlugin.GITHUB_PULL_REQUEST, "1"); 82 | when(mode.isIssues()).thenReturn(true); 83 | 84 | pullRequestProjectBuilder.build(mock(ProjectBuilder.Context.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS))); 85 | 86 | verify(facade).init(eq(1), any(File.class)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/MarkDownReportBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.URL; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Set; 26 | import java.util.TreeSet; 27 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 28 | import org.sonar.api.batch.rule.Severity; 29 | 30 | public class MarkDownReportBuilder implements ReportBuilder { 31 | 32 | private final MarkDownUtils markDownUtils; 33 | private final StringBuilder sb = new StringBuilder(); 34 | 35 | // note: ordered implementation for consistent user experience and testability 36 | private final Set links = new TreeSet<>(); 37 | 38 | private final List extraIssues = new ArrayList<>(); 39 | 40 | private static class IssueHolder { 41 | private final PostJobIssue issue; 42 | private final URL gitHubUrl; 43 | 44 | private IssueHolder(PostJobIssue issue, URL gitHubUrl) { 45 | this.issue = issue; 46 | this.gitHubUrl = gitHubUrl; 47 | } 48 | } 49 | 50 | MarkDownReportBuilder(MarkDownUtils markDownUtils) { 51 | this.markDownUtils = markDownUtils; 52 | } 53 | 54 | @Override 55 | public ReportBuilder append(Object o) { 56 | sb.append(o); 57 | return this; 58 | } 59 | 60 | @Override 61 | public ReportBuilder append(Severity severity) { 62 | links.add(formatImageLinkDefinition(severity)); 63 | sb.append(formatImageLinkReference(severity)); 64 | return this; 65 | } 66 | 67 | private static String formatImageLinkDefinition(Severity severity) { 68 | return String.format("[%s]: %s 'Severity: %s'", severity.name(), MarkDownUtils.getImageUrl(severity), severity.name()); 69 | } 70 | 71 | private static String formatImageLinkReference(Severity severity) { 72 | return String.format("![%s][%s]", severity.name(), severity.name()); 73 | } 74 | 75 | @Override 76 | public ReportBuilder registerExtraIssue(PostJobIssue issue, URL gitHubUrl) { 77 | extraIssues.add(new IssueHolder(issue, gitHubUrl)); 78 | return this; 79 | } 80 | 81 | @Override 82 | public ReportBuilder appendExtraIssues() { 83 | // need a blank line before lists to be displayed correctly 84 | sb.append("\n"); 85 | for (IssueHolder holder : extraIssues) { 86 | PostJobIssue issue = holder.issue; 87 | links.add(formatImageLinkDefinition(issue.severity())); 88 | String image = formatImageLinkReference(issue.severity()); 89 | String text = markDownUtils.globalIssue(issue.message(), issue.ruleKey().toString(), holder.gitHubUrl, issue.componentKey()); 90 | sb.append("1. ").append(image).append(" ").append(text).append("\n"); 91 | } 92 | return this; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | StringBuilder copy = new StringBuilder(sb); 98 | for (String link : links) { 99 | copy.append("\n").append(link); 100 | } 101 | return copy.toString(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/MarkDownUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.io.UnsupportedEncodingException; 23 | import java.net.URL; 24 | import java.net.URLEncoder; 25 | import java.util.Locale; 26 | import java.util.regex.Pattern; 27 | import javax.annotation.Nullable; 28 | import org.apache.commons.lang.StringUtils; 29 | import org.sonar.api.CoreProperties; 30 | import org.sonar.api.batch.InstantiationStrategy; 31 | import org.sonar.api.batch.ScannerSide; 32 | import org.sonar.api.batch.rule.Severity; 33 | import org.sonar.api.config.Settings; 34 | 35 | @ScannerSide 36 | @InstantiationStrategy(InstantiationStrategy.PER_BATCH) 37 | public class MarkDownUtils { 38 | 39 | private static final String IMAGES_ROOT_URL = "https://sonarsource.github.io/sonar-github/"; 40 | private final String ruleUrlPrefix; 41 | 42 | public MarkDownUtils(Settings settings) { 43 | // If server base URL was not configured in SQ server then is is better to take URL configured on batch side 44 | String baseUrl = settings.hasKey(CoreProperties.SERVER_BASE_URL) ? settings.getString(CoreProperties.SERVER_BASE_URL) : settings.getString("sonar.host.url"); 45 | if (!baseUrl.endsWith("/")) { 46 | baseUrl += "/"; 47 | } 48 | this.ruleUrlPrefix = baseUrl; 49 | } 50 | 51 | public String inlineIssue(Severity severity, String message, String ruleKey) { 52 | String ruleLink = getRuleLink(ruleKey); 53 | StringBuilder sb = new StringBuilder(); 54 | sb.append(formatImageLink(severity)) 55 | .append(" ") 56 | .append(message) 57 | .append(" ") 58 | .append(ruleLink); 59 | return sb.toString(); 60 | } 61 | 62 | private static String getLocation(URL url) { 63 | String filename = Pattern.compile(".*/", Pattern.DOTALL).matcher(url.toString()).replaceAll(StringUtils.EMPTY); 64 | if (filename.length() <= 0) { 65 | filename = "Project"; 66 | } 67 | 68 | return filename; 69 | } 70 | 71 | public String globalIssue(String message, String ruleKey, @Nullable URL url, String componentKey) { 72 | StringBuilder sb = new StringBuilder(); 73 | if (url != null) { 74 | sb.append("[").append(getLocation(url)).append("]").append("(").append(url).append(")"); 75 | } else { 76 | sb.append(componentKey); 77 | } 78 | String ruleLink = getRuleLink(ruleKey); 79 | sb.append(": ").append(message).append(" ").append(ruleLink); 80 | return sb.toString(); 81 | } 82 | 83 | String getRuleLink(String ruleKey) { 84 | return "[![rule](" + IMAGES_ROOT_URL + "rule.png)](" + ruleUrlPrefix + "coding_rules#rule_key=" + encodeForUrlParam(ruleKey) + ")"; 85 | } 86 | 87 | static String encodeForUrlParam(String url) { 88 | try { 89 | return URLEncoder.encode(url, "UTF-8"); 90 | 91 | } catch (UnsupportedEncodingException e) { 92 | throw new IllegalStateException("Encoding not supported", e); 93 | } 94 | } 95 | 96 | static String getImageUrl(Severity severity) { 97 | return IMAGES_ROOT_URL + "severity-" + severity.name().toLowerCase(Locale.ENGLISH) + ".png"; 98 | } 99 | 100 | static String formatImageLink(Severity severity) { 101 | return String.format("![%s](%s 'Severity: %s')", severity.name(), getImageUrl(severity), severity.name()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.sonarsource.parent 7 | parent 8 | 45 9 | 10 | 11 | 12 | org.sonarsource.github 13 | sonar-github-plugin 14 | SonarQube :: GitHub Plugin 15 | sonar-plugin 16 | 1.5-SNAPSHOT 17 | Provide some integration between GitHub and SonarQube 18 | http://redirect.sonarsource.com/plugins/github.html 19 | 2015 20 | 21 | 22 | 6.7 23 | GitHub 24 | org.sonar.plugins.github.GitHubPlugin 25 | 26 | 27 | sonar-github 28 | 29 | ${project.groupId}:${project.artifactId}:jar 30 | 31 | 32 | 33 | SonarSource 34 | https://www.sonarsource.com 35 | 36 | 37 | 38 | 39 | GNU LGPL 3 40 | http://www.gnu.org/licenses/lgpl.txt 41 | repo 42 | 43 | 44 | 45 | 46 | 47 | henryju 48 | Julien Henry 49 | +1 50 | 51 | 52 | 53 | 54 | scm:git:git@github.com:SonarSource/sonar-github.git 55 | scm:git:git@github.com:SonarSource/sonar-github.git 56 | https://github.com/SonarSource/sonar-github 57 | HEAD 58 | 59 | 60 | 61 | jira 62 | http://jira.sonarsource.com/browse/SONARGITUB 63 | 64 | 65 | 66 | 71 | 72 | repo.jenkins-ci.org 73 | http://repo.jenkins-ci.org/public/ 74 | 75 | 76 | 77 | 78 | 79 | org.sonarsource.sonarqube 80 | sonar-plugin-api 81 | ${sonar.version} 82 | provided 83 | 84 | 85 | com.google.code.findbugs 86 | jsr305 87 | 2.0.3 88 | provided 89 | 90 | 91 | org.kohsuke 92 | github-api 93 | 1.90 94 | 95 | 96 | 97 | commons-io 98 | commons-io 99 | 2.4 100 | 101 | 102 | 103 | 104 | junit 105 | junit 106 | 4.12 107 | test 108 | 109 | 110 | org.assertj 111 | assertj-core 112 | 1.7.1 113 | test 114 | 115 | 116 | org.mockito 117 | mockito-core 118 | 1.9.5 119 | test 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/GitHubPluginConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.Proxy; 23 | import org.junit.Before; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import org.junit.rules.ExpectedException; 27 | import org.sonar.api.CoreProperties; 28 | import org.sonar.api.config.PropertyDefinitions; 29 | import org.sonar.api.config.Settings; 30 | import org.sonar.api.config.internal.MapSettings; 31 | import org.sonar.api.utils.MessageException; 32 | import org.sonar.api.utils.System2; 33 | 34 | import static org.assertj.core.api.Assertions.assertThat; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.when; 37 | 38 | public class GitHubPluginConfigurationTest { 39 | 40 | @Rule 41 | public ExpectedException thrown = ExpectedException.none(); 42 | 43 | private MapSettings settings; 44 | private GitHubPluginConfiguration config; 45 | 46 | @Before 47 | public void prepare() { 48 | settings = new MapSettings(new PropertyDefinitions(GitHubPlugin.class)); 49 | config = new GitHubPluginConfiguration(settings, new System2()); 50 | } 51 | 52 | @Test 53 | public void guessRepositoryFromScmUrl() { 54 | try { 55 | config.repository(); 56 | } catch (Exception e) { 57 | assertThat(e).isInstanceOf(MessageException.class) 58 | .hasMessage("Unable to determine GitHub repository name for this project. Please provide it using property '" + GitHubPlugin.GITHUB_REPO 59 | + "' or configure property '" + CoreProperties.LINKS_SOURCES + "'."); 60 | } 61 | 62 | settings.setProperty(CoreProperties.LINKS_SOURCES, "do_not_match_1"); 63 | try { 64 | config.repository(); 65 | } catch (Exception e) { 66 | assertThat(e).isInstanceOf(MessageException.class) 67 | .hasMessage("Unable to parse GitHub repository name for this project. Please check configuration:\n * " + CoreProperties.LINKS_SOURCES_DEV 68 | + ": null\n * " + CoreProperties.LINKS_SOURCES + ": do_not_match_1"); 69 | } 70 | settings.clear(); 71 | settings.setProperty(CoreProperties.LINKS_SOURCES_DEV, "do_not_match_2"); 72 | try { 73 | config.repository(); 74 | } catch (Exception e) { 75 | assertThat(e).isInstanceOf(MessageException.class) 76 | .hasMessage("Unable to parse GitHub repository name for this project. Please check configuration:\n * " + CoreProperties.LINKS_SOURCES_DEV 77 | + ": do_not_match_2\n * " + CoreProperties.LINKS_SOURCES + ": null"); 78 | } 79 | 80 | settings.clear(); 81 | settings.setProperty(CoreProperties.LINKS_SOURCES, "scm:git:git@github.com:SonarSource/github-integration.git"); 82 | assertThat(config.repository()).isEqualTo("SonarSource/github-integration"); 83 | 84 | settings.setProperty(CoreProperties.LINKS_SOURCES_DEV, "do_not_parse"); 85 | assertThat(config.repository()).isEqualTo("SonarSource/github-integration"); 86 | 87 | settings.setProperty(CoreProperties.LINKS_SOURCES_DEV, "scm:git:git@github.com:SonarCommunity2/github-integration.git"); 88 | assertThat(config.repository()).isEqualTo("SonarCommunity2/github-integration"); 89 | 90 | settings.removeProperty(CoreProperties.LINKS_SOURCES); 91 | assertThat(config.repository()).isEqualTo("SonarCommunity2/github-integration"); 92 | 93 | settings.setProperty(GitHubPlugin.GITHUB_REPO, "https://github.com/SonarSource/sonar-github.git"); 94 | assertThat(config.repository()).isEqualTo("SonarSource/sonar-github"); 95 | settings.setProperty(GitHubPlugin.GITHUB_REPO, "http://github.com/SonarSource/sonar-github.git"); 96 | assertThat(config.repository()).isEqualTo("SonarSource/sonar-github"); 97 | settings.setProperty(GitHubPlugin.GITHUB_REPO, "SonarCommunity3/github-integration"); 98 | assertThat(config.repository()).isEqualTo("SonarCommunity3/github-integration"); 99 | } 100 | 101 | @Test 102 | public void other() { 103 | settings.setProperty(GitHubPlugin.GITHUB_OAUTH, "oauth"); 104 | assertThat(config.oauth()).isEqualTo("oauth"); 105 | 106 | assertThat(config.isEnabled()).isFalse(); 107 | settings.setProperty(GitHubPlugin.GITHUB_PULL_REQUEST, "3"); 108 | assertThat(config.pullRequestNumber()).isEqualTo(3); 109 | assertThat(config.isEnabled()).isTrue(); 110 | 111 | assertThat(config.endpoint()).isEqualTo("https://api.github.com"); 112 | settings.setProperty(GitHubPlugin.GITHUB_ENDPOINT, "http://myprivate-endpoint"); 113 | assertThat(config.endpoint()).isEqualTo("http://myprivate-endpoint"); 114 | 115 | assertThat(config.tryReportIssuesInline()).isTrue(); 116 | settings.setProperty(GitHubPlugin.GITHUB_DISABLE_INLINE_COMMENTS, "true"); 117 | assertThat(config.tryReportIssuesInline()).isFalse(); 118 | } 119 | 120 | @Test 121 | public void testProxyConfiguration() { 122 | System2 system2 = mock(System2.class); 123 | config = new GitHubPluginConfiguration(settings, system2); 124 | assertThat(config.isProxyConnectionEnabled()).isFalse(); 125 | when(system2.property("http.proxyHost")).thenReturn("foo"); 126 | assertThat(config.isProxyConnectionEnabled()).isTrue(); 127 | when(system2.property("https.proxyHost")).thenReturn("bar"); 128 | assertThat(config.getHttpProxy()).isEqualTo(Proxy.NO_PROXY); 129 | 130 | settings.setProperty(GitHubPlugin.GITHUB_ENDPOINT, "wrong url"); 131 | thrown.expect(IllegalArgumentException.class); 132 | config.getHttpProxy(); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/MarkDownReportBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import org.junit.Test; 25 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 26 | import org.sonar.api.batch.rule.Severity; 27 | import org.sonar.api.rule.RuleKey; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | public class MarkDownReportBuilderTest { 34 | 35 | @Test 36 | public void test_empty_report() { 37 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 38 | assertThat(builder.toString()).isEmpty(); 39 | } 40 | 41 | @Test 42 | public void should_append_nothing_if_no_references() { 43 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 44 | String someText = "some text"; 45 | builder.append(someText); 46 | assertThat(builder.toString()).isEqualTo(someText); 47 | } 48 | 49 | @Test 50 | public void should_append_severity_using_reference_links() { 51 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 52 | builder.append(Severity.BLOCKER).append(" fix the leak!\n"); 53 | builder.append("Check comments too!\n"); 54 | assertThat(builder.toString()).isEqualTo("![BLOCKER][BLOCKER] fix the leak!\n" 55 | + "Check comments too!\n" 56 | + "\n" 57 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'"); 58 | } 59 | 60 | @Test 61 | public void should_append_reference_definition_only_once() { 62 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 63 | builder.append(Severity.BLOCKER).append(" fix the leak!\n"); 64 | builder.append(Severity.BLOCKER).append(" fix the leak!\n"); 65 | builder.append("Check comments too!\n"); 66 | assertThat(builder.toString()).isEqualTo("![BLOCKER][BLOCKER] fix the leak!\n" 67 | + "![BLOCKER][BLOCKER] fix the leak!\n" 68 | + "Check comments too!\n" 69 | + "\n" 70 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'"); 71 | } 72 | 73 | @Test 74 | public void should_append_reference_definition_for_all_known_severity() { 75 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 76 | for (Severity severity : Severity.values()) { 77 | builder.append(severity).append(" a ").append(severity.name()).append("-level issue\n"); 78 | } 79 | assertThat(builder.toString()).isEqualTo("![INFO][INFO] a INFO-level issue\n" 80 | + "![MINOR][MINOR] a MINOR-level issue\n" 81 | + "![MAJOR][MAJOR] a MAJOR-level issue\n" 82 | + "![CRITICAL][CRITICAL] a CRITICAL-level issue\n" 83 | + "![BLOCKER][BLOCKER] a BLOCKER-level issue\n" 84 | + "\n" 85 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 86 | + "[CRITICAL]: https://sonarsource.github.io/sonar-github/severity-critical.png 'Severity: CRITICAL'\n" 87 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'\n" 88 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'\n" 89 | + "[MINOR]: https://sonarsource.github.io/sonar-github/severity-minor.png 'Severity: MINOR'"); 90 | } 91 | 92 | @Test 93 | public void should_append_reference_definitions_for_extra_issues_too() throws MalformedURLException { 94 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 95 | builder.append(Severity.BLOCKER).append(" fix the leak!\n"); 96 | 97 | PostJobIssue postJobIssue = mock(PostJobIssue.class); 98 | when(postJobIssue.severity()).thenReturn(Severity.INFO); 99 | when(postJobIssue.ruleKey()).thenReturn(mock(RuleKey.class)); 100 | builder.registerExtraIssue(postJobIssue, new URL("http://github.com/dummy")); 101 | builder.appendExtraIssues(); 102 | 103 | builder.append("\nCheck comments too!\n"); 104 | assertThat(builder.toString()).isEqualTo("![BLOCKER][BLOCKER] fix the leak!\n" 105 | + "\n" 106 | + "1. ![INFO][INFO] null\n" 107 | + "\n" 108 | + "Check comments too!\n" 109 | + "\n" 110 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 111 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'"); 112 | } 113 | 114 | @Test 115 | public void should_append_reference_definitions_for_extra_issues_only_if_used() throws MalformedURLException { 116 | ReportBuilder builder = new MarkDownReportBuilder(mock(MarkDownUtils.class)); 117 | builder.append(Severity.BLOCKER).append(" fix the leak!\n"); 118 | 119 | PostJobIssue postJobIssue = mock(PostJobIssue.class); 120 | when(postJobIssue.severity()).thenReturn(Severity.INFO); 121 | when(postJobIssue.ruleKey()).thenReturn(mock(RuleKey.class)); 122 | builder.registerExtraIssue(postJobIssue, new URL("http://github.com/dummy")); 123 | 124 | builder.append("Check comments too!\n"); 125 | assertThat(builder.toString()).isEqualTo("![BLOCKER][BLOCKER] fix the leak!\n" 126 | + "Check comments too!\n" 127 | + "\n" 128 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'"); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/PullRequestIssuePostJob.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.util.Comparator; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.stream.StreamSupport; 26 | import org.apache.commons.lang.StringUtils; 27 | import org.kohsuke.github.GHCommitState; 28 | import org.sonar.api.batch.fs.InputComponent; 29 | import org.sonar.api.batch.fs.InputFile; 30 | import org.sonar.api.batch.postjob.PostJob; 31 | import org.sonar.api.batch.postjob.PostJobContext; 32 | import org.sonar.api.batch.postjob.PostJobDescriptor; 33 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 34 | import org.sonar.api.utils.log.Logger; 35 | import org.sonar.api.utils.log.Loggers; 36 | 37 | /** 38 | * Compute comments to be added on the pull request. 39 | */ 40 | public class PullRequestIssuePostJob implements PostJob { 41 | private static final Logger LOG = Loggers.get(PullRequestFacade.class); 42 | 43 | private static final Comparator ISSUE_COMPARATOR = new IssueComparator(); 44 | 45 | private final PullRequestFacade pullRequestFacade; 46 | private final GitHubPluginConfiguration gitHubPluginConfiguration; 47 | private final MarkDownUtils markDownUtils; 48 | 49 | public PullRequestIssuePostJob(GitHubPluginConfiguration gitHubPluginConfiguration, PullRequestFacade pullRequestFacade, MarkDownUtils markDownUtils) { 50 | this.gitHubPluginConfiguration = gitHubPluginConfiguration; 51 | this.pullRequestFacade = pullRequestFacade; 52 | this.markDownUtils = markDownUtils; 53 | } 54 | 55 | @Override 56 | public void describe(PostJobDescriptor descriptor) { 57 | descriptor 58 | .name("GitHub Pull Request Issue Publisher") 59 | .requireProperty(GitHubPlugin.GITHUB_PULL_REQUEST); 60 | } 61 | 62 | @Override 63 | public void execute(PostJobContext context) { 64 | GlobalReport report = new GlobalReport(markDownUtils, gitHubPluginConfiguration.tryReportIssuesInline()); 65 | try { 66 | Map> commentsToBeAddedByLine = processIssues(report, context.issues()); 67 | 68 | updateReviewComments(commentsToBeAddedByLine); 69 | 70 | pullRequestFacade.deleteOutdatedComments(); 71 | 72 | pullRequestFacade.createOrUpdateGlobalComments(report.hasNewIssue() ? report.formatForMarkdown() : null); 73 | 74 | pullRequestFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription()); 75 | } catch (Exception e) { 76 | LOG.error("SonarQube analysis failed to complete the review of this pull request", e); 77 | pullRequestFacade.createOrUpdateSonarQubeStatus(GHCommitState.ERROR, StringUtils.abbreviate("SonarQube analysis failed: " + e.getMessage(), 140)); 78 | } 79 | } 80 | 81 | private Map> processIssues(GlobalReport report, Iterable issues) { 82 | Map> commentToBeAddedByFileAndByLine = new HashMap<>(); 83 | 84 | StreamSupport.stream(issues.spliterator(), false) 85 | .filter(PostJobIssue::isNew) 86 | // SONARGITUB-13 Ignore issues on files not modified by the P/R 87 | .filter(i -> { 88 | InputComponent inputComponent = i.inputComponent(); 89 | return inputComponent == null || 90 | !inputComponent.isFile() || 91 | pullRequestFacade.hasFile((InputFile) inputComponent); 92 | }) 93 | .sorted(ISSUE_COMPARATOR) 94 | .forEach(i -> processIssue(report, commentToBeAddedByFileAndByLine, i)); 95 | return commentToBeAddedByFileAndByLine; 96 | 97 | } 98 | 99 | private void processIssue(GlobalReport report, Map> commentToBeAddedByFileAndByLine, PostJobIssue issue) { 100 | boolean reportedInline = false; 101 | InputComponent inputComponent = issue.inputComponent(); 102 | if (gitHubPluginConfiguration.tryReportIssuesInline() && inputComponent != null && inputComponent.isFile()) { 103 | reportedInline = tryReportInline(commentToBeAddedByFileAndByLine, issue, (InputFile) inputComponent); 104 | } 105 | report.process(issue, pullRequestFacade.getGithubUrl(inputComponent, issue.line()), reportedInline); 106 | } 107 | 108 | private boolean tryReportInline(Map> commentToBeAddedByFileAndByLine, PostJobIssue issue, InputFile inputFile) { 109 | Integer lineOrNull = issue.line(); 110 | if (lineOrNull != null) { 111 | int line = lineOrNull.intValue(); 112 | if (pullRequestFacade.hasFileLine(inputFile, line)) { 113 | String message = issue.message(); 114 | String ruleKey = issue.ruleKey().toString(); 115 | if (!commentToBeAddedByFileAndByLine.containsKey(inputFile)) { 116 | commentToBeAddedByFileAndByLine.put(inputFile, new HashMap()); 117 | } 118 | Map commentsByLine = commentToBeAddedByFileAndByLine.get(inputFile); 119 | if (!commentsByLine.containsKey(line)) { 120 | commentsByLine.put(line, new StringBuilder()); 121 | } 122 | commentsByLine.get(line).append(markDownUtils.inlineIssue(issue.severity(), message, ruleKey)).append("\n"); 123 | return true; 124 | } 125 | } 126 | return false; 127 | } 128 | 129 | private void updateReviewComments(Map> commentsToBeAddedByLine) { 130 | for (Map.Entry> entry : commentsToBeAddedByLine.entrySet()) { 131 | for (Map.Entry entryPerLine : entry.getValue().entrySet()) { 132 | String body = entryPerLine.getValue().toString(); 133 | pullRequestFacade.createOrUpdateReviewComment(entry.getKey(), entryPerLine.getKey(), body); 134 | } 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/GlobalReport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.URL; 23 | import java.util.Locale; 24 | import javax.annotation.Nullable; 25 | import org.kohsuke.github.GHCommitState; 26 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 27 | import org.sonar.api.batch.rule.Severity; 28 | 29 | public class GlobalReport { 30 | private final boolean tryReportIssuesInline; 31 | private int[] newIssuesBySeverity = new int[Severity.values().length]; 32 | private int extraIssueCount = 0; 33 | private int maxGlobalReportedIssues; 34 | private final ReportBuilder builder; 35 | 36 | public GlobalReport(MarkDownUtils markDownUtils, boolean tryReportIssuesInline) { 37 | this(markDownUtils, tryReportIssuesInline, GitHubPluginConfiguration.MAX_GLOBAL_ISSUES); 38 | } 39 | 40 | public GlobalReport(MarkDownUtils markDownUtils, boolean tryReportIssuesInline, int maxGlobalReportedIssues) { 41 | this.tryReportIssuesInline = tryReportIssuesInline; 42 | this.maxGlobalReportedIssues = maxGlobalReportedIssues; 43 | this.builder = new MarkDownReportBuilder(markDownUtils); 44 | } 45 | 46 | private void increment(Severity severity) { 47 | this.newIssuesBySeverity[severity.ordinal()]++; 48 | } 49 | 50 | public String formatForMarkdown() { 51 | int newIssues = newIssues(Severity.BLOCKER) + newIssues(Severity.CRITICAL) + newIssues(Severity.MAJOR) + newIssues(Severity.MINOR) + newIssues(Severity.INFO); 52 | if (newIssues == 0) { 53 | return "SonarQube analysis reported no issues."; 54 | } 55 | 56 | boolean hasInlineIssues = newIssues > extraIssueCount; 57 | boolean extraIssuesTruncated = extraIssueCount > maxGlobalReportedIssues; 58 | builder.append("SonarQube analysis reported ").append(newIssues).append(" issue").append(newIssues > 1 ? "s" : "").append("\n"); 59 | if (hasInlineIssues || extraIssuesTruncated) { 60 | appendSummaryBySeverity(builder); 61 | } 62 | if (tryReportIssuesInline && hasInlineIssues) { 63 | builder.append("\nWatch the comments in this conversation to review them.\n"); 64 | } 65 | 66 | if (extraIssueCount > 0) { 67 | appendExtraIssues(builder, hasInlineIssues, extraIssuesTruncated); 68 | } 69 | 70 | return builder.toString(); 71 | } 72 | 73 | private void appendExtraIssues(ReportBuilder builder, boolean hasInlineIssues, boolean extraIssuesTruncated) { 74 | if (tryReportIssuesInline) { 75 | if (hasInlineIssues || extraIssuesTruncated) { 76 | int extraCount; 77 | builder.append("\n#### "); 78 | if (extraIssueCount <= maxGlobalReportedIssues) { 79 | extraCount = extraIssueCount; 80 | } else { 81 | extraCount = maxGlobalReportedIssues; 82 | builder.append("Top "); 83 | } 84 | builder.append(extraCount).append(" extra issue").append(extraCount > 1 ? "s" : "").append("\n"); 85 | } 86 | builder.append( 87 | "\nNote: The following issues were found on lines that were not modified in the pull request. " 88 | + "Because these issues can't be reported as line comments, they are summarized here:\n"); 89 | } else if (extraIssuesTruncated) { 90 | builder.append("\n#### Top ").append(maxGlobalReportedIssues).append(" issues\n"); 91 | } 92 | builder.appendExtraIssues(); 93 | } 94 | 95 | public String getStatusDescription() { 96 | StringBuilder sb = new StringBuilder(); 97 | appendNewIssuesInline(sb); 98 | return sb.toString(); 99 | } 100 | 101 | public GHCommitState getStatus() { 102 | return (newIssues(Severity.BLOCKER) > 0 || newIssues(Severity.CRITICAL) > 0) ? GHCommitState.ERROR : GHCommitState.SUCCESS; 103 | } 104 | 105 | private int newIssues(Severity s) { 106 | return newIssuesBySeverity[s.ordinal()]; 107 | } 108 | 109 | private void appendSummaryBySeverity(ReportBuilder builder) { 110 | appendNewIssues(builder, Severity.BLOCKER); 111 | appendNewIssues(builder, Severity.CRITICAL); 112 | appendNewIssues(builder, Severity.MAJOR); 113 | appendNewIssues(builder, Severity.MINOR); 114 | appendNewIssues(builder, Severity.INFO); 115 | } 116 | 117 | private void appendNewIssuesInline(StringBuilder sb) { 118 | sb.append("SonarQube reported "); 119 | int newIssues = newIssues(Severity.BLOCKER) + newIssues(Severity.CRITICAL) + newIssues(Severity.MAJOR) + newIssues(Severity.MINOR) + newIssues(Severity.INFO); 120 | if (newIssues > 0) { 121 | sb.append(newIssues).append(" issue" + (newIssues > 1 ? "s" : "")).append(","); 122 | int newCriticalOrBlockerIssues = newIssues(Severity.BLOCKER) + newIssues(Severity.CRITICAL); 123 | if (newCriticalOrBlockerIssues > 0) { 124 | appendNewIssuesInline(sb, Severity.CRITICAL); 125 | appendNewIssuesInline(sb, Severity.BLOCKER); 126 | } else { 127 | sb.append(" no criticals or blockers"); 128 | } 129 | } else { 130 | sb.append("no issues"); 131 | } 132 | } 133 | 134 | private void appendNewIssuesInline(StringBuilder sb, Severity severity) { 135 | int issueCount = newIssues(severity); 136 | if (issueCount > 0) { 137 | if (sb.charAt(sb.length() - 1) == ',') { 138 | sb.append(" with "); 139 | } else { 140 | sb.append(" and "); 141 | } 142 | sb.append(issueCount).append(" ").append(severity.name().toLowerCase(Locale.ENGLISH)); 143 | } 144 | } 145 | 146 | private void appendNewIssues(ReportBuilder builder, Severity severity) { 147 | int issueCount = newIssues(severity); 148 | if (issueCount > 0) { 149 | builder 150 | .append("* ").append(severity) 151 | .append(" ").append(issueCount) 152 | .append(" ").append(severity.name().toLowerCase(Locale.ENGLISH)) 153 | .append("\n"); 154 | } 155 | } 156 | 157 | public void process(PostJobIssue issue, @Nullable URL gitHubUrl, boolean reportedOnDiff) { 158 | increment(issue.severity()); 159 | if (!reportedOnDiff) { 160 | if (extraIssueCount < maxGlobalReportedIssues) { 161 | builder.registerExtraIssue(issue, gitHubUrl); 162 | } 163 | extraIssueCount++; 164 | } 165 | } 166 | 167 | public boolean hasNewIssue() { 168 | return newIssues(Severity.BLOCKER) + newIssues(Severity.CRITICAL) + newIssues(Severity.MAJOR) + newIssues(Severity.MINOR) + newIssues(Severity.INFO) > 0; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/GitHubPluginConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.Authenticator; 23 | import java.net.PasswordAuthentication; 24 | import java.net.Proxy; 25 | import java.net.ProxySelector; 26 | import java.net.URI; 27 | import java.net.URISyntaxException; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | import javax.annotation.CheckForNull; 31 | import org.sonar.api.CoreProperties; 32 | import org.sonar.api.batch.BatchSide; 33 | import org.sonar.api.batch.InstantiationStrategy; 34 | import org.sonar.api.batch.ScannerSide; 35 | import org.sonar.api.config.Settings; 36 | import org.sonar.api.utils.MessageException; 37 | import org.sonar.api.utils.System2; 38 | import org.sonar.api.utils.log.Logger; 39 | import org.sonar.api.utils.log.Loggers; 40 | 41 | import static org.apache.commons.lang.StringUtils.isNotBlank; 42 | 43 | @ScannerSide 44 | @InstantiationStrategy(InstantiationStrategy.PER_BATCH) 45 | public class GitHubPluginConfiguration { 46 | 47 | public static final int MAX_GLOBAL_ISSUES = 10; 48 | private static final Logger LOG = Loggers.get(GitHubPluginConfiguration.class); 49 | public static final String HTTP_PROXY_HOSTNAME = "http.proxyHost"; 50 | public static final String HTTPS_PROXY_HOSTNAME = "https.proxyHost"; 51 | public static final String PROXY_SOCKS_HOSTNAME = "socksProxyHost"; 52 | public static final String HTTP_PROXY_PORT = "http.proxyPort"; 53 | public static final String HTTPS_PROXY_PORT = "https.proxyPort"; 54 | public static final String HTTP_PROXY_USER = "http.proxyUser"; 55 | public static final String HTTP_PROXY_PASS = "http.proxyPassword"; 56 | 57 | private final Settings settings; 58 | private final System2 system2; 59 | private final Pattern gitSshPattern; 60 | private final Pattern gitHttpPattern; 61 | 62 | public GitHubPluginConfiguration(Settings settings, System2 system2) { 63 | this.settings = settings; 64 | this.system2 = system2; 65 | this.gitSshPattern = Pattern.compile(".*@github\\.com:(.*/.*)\\.git"); 66 | this.gitHttpPattern = Pattern.compile("https?://github\\.com/(.*/.*)\\.git"); 67 | } 68 | 69 | public int pullRequestNumber() { 70 | return settings.getInt(GitHubPlugin.GITHUB_PULL_REQUEST); 71 | } 72 | 73 | public String repository() { 74 | if (settings.hasKey(GitHubPlugin.GITHUB_REPO)) { 75 | return repoFromProp(); 76 | } 77 | if (isNotBlank(settings.getString(CoreProperties.LINKS_SOURCES_DEV)) || isNotBlank(settings.getString(CoreProperties.LINKS_SOURCES))) { 78 | return repoFromScmProps(); 79 | } 80 | throw MessageException.of("Unable to determine GitHub repository name for this project. Please provide it using property '" + GitHubPlugin.GITHUB_REPO 81 | + "' or configure property '" + CoreProperties.LINKS_SOURCES + "'."); 82 | } 83 | 84 | private String repoFromScmProps() { 85 | String repo = null; 86 | if (isNotBlank(settings.getString(CoreProperties.LINKS_SOURCES_DEV))) { 87 | String url = settings.getString(CoreProperties.LINKS_SOURCES_DEV); 88 | repo = extractRepoFromGitUrl(url); 89 | } 90 | if (repo == null && isNotBlank(settings.getString(CoreProperties.LINKS_SOURCES))) { 91 | String url = settings.getString(CoreProperties.LINKS_SOURCES); 92 | repo = extractRepoFromGitUrl(url); 93 | } 94 | if (repo == null) { 95 | throw MessageException.of("Unable to parse GitHub repository name for this project. Please check configuration:\n * " + CoreProperties.LINKS_SOURCES_DEV 96 | + ": " + settings.getString(CoreProperties.LINKS_SOURCES_DEV) + "\n * " + CoreProperties.LINKS_SOURCES + ": " + settings.getString(CoreProperties.LINKS_SOURCES)); 97 | } 98 | return repo; 99 | } 100 | 101 | private String repoFromProp() { 102 | String urlOrRepo = settings.getString(GitHubPlugin.GITHUB_REPO); 103 | String repo = extractRepoFromGitUrl(urlOrRepo); 104 | if (repo == null) { 105 | return urlOrRepo; 106 | } 107 | return repo; 108 | } 109 | 110 | @CheckForNull 111 | private String extractRepoFromGitUrl(String urlOrRepo) { 112 | Matcher matcher = gitSshPattern.matcher(urlOrRepo); 113 | if (matcher.matches()) { 114 | return matcher.group(1); 115 | } 116 | matcher = gitHttpPattern.matcher(urlOrRepo); 117 | if (matcher.matches()) { 118 | return matcher.group(1); 119 | } 120 | return null; 121 | } 122 | 123 | @CheckForNull 124 | public String oauth() { 125 | return settings.getString(GitHubPlugin.GITHUB_OAUTH); 126 | } 127 | 128 | public boolean isEnabled() { 129 | return settings.hasKey(GitHubPlugin.GITHUB_PULL_REQUEST); 130 | } 131 | 132 | public String endpoint() { 133 | return settings.getString(GitHubPlugin.GITHUB_ENDPOINT); 134 | } 135 | 136 | public boolean tryReportIssuesInline() { 137 | return !settings.getBoolean(GitHubPlugin.GITHUB_DISABLE_INLINE_COMMENTS); 138 | } 139 | 140 | /** 141 | * Checks if a proxy was passed with command line parameters or configured in the system. 142 | * If only an HTTP proxy was configured then it's properties are copied to the HTTPS proxy (like SonarQube configuration) 143 | * @return True iff a proxy was configured to be used in the plugin. 144 | */ 145 | public boolean isProxyConnectionEnabled() { 146 | return system2.property(HTTP_PROXY_HOSTNAME) != null 147 | || system2.property(HTTPS_PROXY_HOSTNAME) != null 148 | || system2.property(PROXY_SOCKS_HOSTNAME) != null; 149 | } 150 | 151 | public Proxy getHttpProxy() { 152 | try { 153 | if (system2.property(HTTP_PROXY_HOSTNAME) != null && system2.property(HTTPS_PROXY_HOSTNAME) == null) { 154 | System.setProperty(HTTPS_PROXY_HOSTNAME, system2.property(HTTP_PROXY_HOSTNAME)); 155 | System.setProperty(HTTPS_PROXY_PORT, system2.property(HTTP_PROXY_PORT)); 156 | } 157 | 158 | String proxyUser = system2.property(HTTP_PROXY_USER); 159 | String proxyPass = system2.property(HTTP_PROXY_PASS); 160 | 161 | if (proxyUser != null && proxyPass != null) { 162 | Authenticator.setDefault( 163 | new Authenticator() { 164 | @Override 165 | public PasswordAuthentication getPasswordAuthentication() { 166 | return new PasswordAuthentication( 167 | proxyUser, proxyPass.toCharArray()); 168 | } 169 | }); 170 | } 171 | 172 | Proxy selectedProxy = ProxySelector.getDefault().select(new URI(endpoint())).get(0); 173 | 174 | if (selectedProxy.type() == Proxy.Type.DIRECT) { 175 | LOG.debug("There was no suitable proxy found to connect to GitHub - direct connection is used "); 176 | } 177 | 178 | LOG.info("A proxy has been configured - {}", selectedProxy.toString()); 179 | return selectedProxy; 180 | } catch (URISyntaxException e) { 181 | throw new IllegalArgumentException("Unable to perform GitHub WS operation - endpoint in wrong format: " + endpoint(), e); 182 | } 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/PullRequestFacadeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.file.Files; 26 | import java.util.ArrayList; 27 | import java.util.LinkedHashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import org.assertj.core.data.MapEntry; 31 | import org.junit.Rule; 32 | import org.junit.Test; 33 | import org.junit.rules.TemporaryFolder; 34 | import org.kohsuke.github.GHCommitStatus; 35 | import org.kohsuke.github.GHPullRequest; 36 | import org.kohsuke.github.GHRepository; 37 | import org.kohsuke.github.PagedIterable; 38 | import org.mockito.Mockito; 39 | import org.sonar.api.batch.fs.InputPath; 40 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 41 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 42 | 43 | import static org.assertj.core.api.Assertions.assertThat; 44 | import static org.mockito.Mockito.RETURNS_DEEP_STUBS; 45 | import static org.mockito.Mockito.mock; 46 | import static org.mockito.Mockito.when; 47 | import static org.mockito.Mockito.withSettings; 48 | 49 | public class PullRequestFacadeTest { 50 | 51 | @Rule 52 | public TemporaryFolder temp = new TemporaryFolder(); 53 | 54 | @Test 55 | public void testGetGithubUrl() throws Exception { 56 | 57 | File gitBasedir = temp.newFolder(); 58 | 59 | PullRequestFacade facade = new PullRequestFacade(mock(GitHubPluginConfiguration.class)); 60 | facade.setGitBaseDir(gitBasedir); 61 | GHRepository ghRepo = mock(GHRepository.class); 62 | when(ghRepo.getHtmlUrl()).thenReturn(new URL("https://github.com/SonarSource/sonar-java")); 63 | facade.setGhRepo(ghRepo); 64 | GHPullRequest pr = mock(GHPullRequest.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 65 | when(pr.getHead().getSha()).thenReturn("abc123"); 66 | facade.setPr(pr); 67 | InputPath inputPath = mock(InputPath.class); 68 | when(inputPath.file()).thenReturn(new File(gitBasedir, "src/main/with space/Foo.java")); 69 | assertThat(facade.getGithubUrl(inputPath, 10).toString()).isEqualTo("https://github.com/SonarSource/sonar-java/blob/abc123/src/main/with%20space/Foo.java#L10"); 70 | } 71 | 72 | @Test 73 | public void testPatchLineMapping_some_deleted_lines() throws IOException { 74 | Map patchLocationMapping = new LinkedHashMap(); 75 | PullRequestFacade 76 | .processPatch( 77 | patchLocationMapping, 78 | "@@ -17,9 +17,6 @@\n * along with this program; if not, write to the Free Software Foundation,\n * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n */\n-/**\n- * Deprecated in 4.5.1. JFreechart charts are replaced by Javascript charts.\n- */\n @ParametersAreNonnullByDefault\n package org.sonar.plugins.core.charts;\n "); 79 | 80 | assertThat(patchLocationMapping).containsOnly(MapEntry.entry(17, 1), MapEntry.entry(18, 2), MapEntry.entry(19, 3), MapEntry.entry(20, 7), MapEntry.entry(21, 8), 81 | MapEntry.entry(22, 9)); 82 | } 83 | 84 | @Test 85 | public void testPatchLineMapping_some_added_lines() throws IOException { 86 | Map patchLocationMapping = new LinkedHashMap(); 87 | PullRequestFacade 88 | .processPatch( 89 | patchLocationMapping, 90 | "@@ -24,9 +24,9 @@\n /**\n * A plugin is a group of extensions. See org.sonar.api.Extension interface to browse\n * available extension points.\n- *

\n *

The manifest property Plugin-Class must declare the name of the implementation class.\n * It is automatically set by sonar-packaging-maven-plugin when building plugins.

\n+ *

Implementation must declare a public constructor with no-parameters.

\n *\n * @see org.sonar.api.Extension\n * @since 1.10"); 91 | 92 | assertThat(patchLocationMapping).containsOnly(MapEntry.entry(24, 1), MapEntry.entry(25, 2), MapEntry.entry(26, 3), MapEntry.entry(27, 5), MapEntry.entry(28, 6), 93 | MapEntry.entry(29, 7), MapEntry.entry(30, 8), MapEntry.entry(31, 9), MapEntry.entry(32, 10)); 94 | } 95 | 96 | @Test 97 | public void testPatchLineMapping_no_newline_at_the_end() throws IOException { 98 | Map patchLocationMapping = new LinkedHashMap(); 99 | PullRequestFacade 100 | .processPatch( 101 | patchLocationMapping, 102 | "@@ -1 +0,0 @@\n-\n\\ No newline at end of file"); 103 | 104 | assertThat(patchLocationMapping).isEmpty(); 105 | } 106 | 107 | @Test 108 | public void testEmptyGetCommitStatusForContext() throws IOException { 109 | PullRequestFacade facade = new PullRequestFacade(mock(GitHubPluginConfiguration.class)); 110 | GHRepository ghRepo = mock(GHRepository.class); 111 | PagedIterable ghCommitStatuses = Mockito.mock(PagedIterable.class); 112 | GHPullRequest pr = mock(GHPullRequest.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 113 | when(pr.getRepository()).thenReturn(ghRepo); 114 | when(pr.getHead().getSha()).thenReturn("abc123"); 115 | when(ghRepo.listCommitStatuses(pr.getHead().getSha())).thenReturn(ghCommitStatuses); 116 | assertThat(facade.getCommitStatusForContext(pr, PullRequestFacade.COMMIT_CONTEXT)).isNull(); 117 | } 118 | 119 | @Test 120 | public void testGetCommitStatusForContextWithOneCorrectStatus() throws IOException { 121 | PullRequestFacade facade = new PullRequestFacade(mock(GitHubPluginConfiguration.class)); 122 | GHRepository ghRepo = mock(GHRepository.class); 123 | PagedIterable ghCommitStatuses = Mockito.mock(PagedIterable.class); 124 | List ghCommitStatusesList = new ArrayList<>(); 125 | GHCommitStatus ghCommitStatusGHPRHContext = Mockito.mock(GHCommitStatus.class); 126 | ghCommitStatusesList.add(ghCommitStatusGHPRHContext); 127 | GHPullRequest pr = mock(GHPullRequest.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 128 | when(pr.getRepository()).thenReturn(ghRepo); 129 | when(pr.getHead().getSha()).thenReturn("abc123"); 130 | when(ghRepo.listCommitStatuses(pr.getHead().getSha())).thenReturn(ghCommitStatuses); 131 | when(ghCommitStatuses.asList()).thenReturn(ghCommitStatusesList); 132 | when(ghCommitStatusGHPRHContext.getContext()).thenReturn(PullRequestFacade.COMMIT_CONTEXT); 133 | assertThat(facade.getCommitStatusForContext(pr, PullRequestFacade.COMMIT_CONTEXT).getContext()).isEqualTo(PullRequestFacade.COMMIT_CONTEXT); 134 | } 135 | 136 | @Test 137 | public void testInitGitBaseDirNotFound() throws Exception { 138 | PullRequestFacade facade = new PullRequestFacade(mock(GitHubPluginConfiguration.class)); 139 | File projectBaseDir = temp.newFolder(); 140 | facade.initGitBaseDir(projectBaseDir); 141 | assertThat(facade.getPath(new TestInputFileBuilder("foo", "src/main/java/Foo.java") 142 | .setModuleBaseDir(projectBaseDir.toPath()) 143 | .build())).isEqualTo("src/main/java/Foo.java"); 144 | } 145 | 146 | @Test 147 | public void testInitGitBaseDir() throws Exception { 148 | PullRequestFacade facade = new PullRequestFacade(mock(GitHubPluginConfiguration.class)); 149 | File gitBaseDir = temp.newFolder(); 150 | Files.createDirectory(gitBaseDir.toPath().resolve(".git")); 151 | File projectBaseDir = new File(gitBaseDir, "myProject"); 152 | facade.initGitBaseDir(projectBaseDir); 153 | assertThat(facade.getPath(new TestInputFileBuilder("foo", "src/main/java/Foo.java") 154 | .setModuleBaseDir(projectBaseDir.toPath()).build())).isEqualTo("myProject/src/main/java/Foo.java"); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/PullRequestIssuePostJobTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import java.util.Arrays; 25 | import javax.annotation.CheckForNull; 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | import org.kohsuke.github.GHCommitState; 29 | import org.mockito.ArgumentCaptor; 30 | import org.sonar.api.CoreProperties; 31 | import org.sonar.api.batch.fs.InputFile; 32 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 33 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 34 | import org.sonar.api.batch.postjob.PostJobContext; 35 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 36 | import org.sonar.api.batch.rule.Severity; 37 | import org.sonar.api.config.PropertyDefinition; 38 | import org.sonar.api.config.PropertyDefinitions; 39 | import org.sonar.api.config.Settings; 40 | import org.sonar.api.config.internal.MapSettings; 41 | import org.sonar.api.rule.RuleKey; 42 | import org.sonar.api.utils.System2; 43 | 44 | import static org.assertj.core.api.Assertions.assertThat; 45 | import static org.mockito.AdditionalMatchers.not; 46 | import static org.mockito.ArgumentCaptor.forClass; 47 | import static org.mockito.Matchers.any; 48 | import static org.mockito.Matchers.anyInt; 49 | import static org.mockito.Matchers.contains; 50 | import static org.mockito.Mockito.mock; 51 | import static org.mockito.Mockito.verify; 52 | import static org.mockito.Mockito.when; 53 | 54 | public class PullRequestIssuePostJobTest { 55 | 56 | private PullRequestIssuePostJob pullRequestIssuePostJob; 57 | private PullRequestFacade pullRequestFacade; 58 | private PostJobContext context; 59 | 60 | @Before 61 | public void prepare() throws Exception { 62 | pullRequestFacade = mock(PullRequestFacade.class); 63 | MapSettings settings = new MapSettings(new PropertyDefinitions(PropertyDefinition.builder(CoreProperties.SERVER_BASE_URL) 64 | .name("Server base URL") 65 | .description("HTTP URL of this SonarQube server, such as http://yourhost.yourdomain/sonar. This value is used i.e. to create links in emails.") 66 | .category(CoreProperties.CATEGORY_GENERAL) 67 | .defaultValue(CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE) 68 | .build())); 69 | GitHubPluginConfiguration config = new GitHubPluginConfiguration(settings, new System2()); 70 | context = mock(PostJobContext.class); 71 | 72 | settings.setProperty("sonar.host.url", "http://192.168.0.1"); 73 | settings.setProperty(CoreProperties.SERVER_BASE_URL, "http://myserver"); 74 | pullRequestIssuePostJob = new PullRequestIssuePostJob(config, pullRequestFacade, new MarkDownUtils(settings)); 75 | } 76 | 77 | private PostJobIssue newMockedIssue(String componentKey, @CheckForNull DefaultInputFile inputFile, @CheckForNull Integer line, Severity severity, 78 | boolean isNew, String message) { 79 | PostJobIssue issue = mock(PostJobIssue.class); 80 | when(issue.inputComponent()).thenReturn(inputFile); 81 | when(issue.componentKey()).thenReturn(componentKey); 82 | if (line != null) { 83 | when(issue.line()).thenReturn(line); 84 | } 85 | when(issue.ruleKey()).thenReturn(RuleKey.of("repo", "rule")); 86 | when(issue.severity()).thenReturn(severity); 87 | when(issue.isNew()).thenReturn(isNew); 88 | when(issue.message()).thenReturn(message); 89 | 90 | return issue; 91 | } 92 | 93 | private PostJobIssue newMockedIssue(String componentKey, Severity severity, boolean isNew, String message) { 94 | return newMockedIssue(componentKey, null, null, severity, isNew, message); 95 | } 96 | 97 | @Test 98 | public void testPullRequestAnalysisNoIssue() { 99 | when(context.issues()).thenReturn(Arrays.asList()); 100 | pullRequestIssuePostJob.execute(context); 101 | verify(pullRequestFacade).createOrUpdateGlobalComments(null); 102 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.SUCCESS, "SonarQube reported no issues"); 103 | } 104 | 105 | @Test 106 | public void testPullRequestAnalysisWithNewIssues() throws MalformedURLException { 107 | DefaultInputFile inputFile1 = new TestInputFileBuilder("foo", "src/Foo.php").build(); 108 | PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, true, "msg1"); 109 | when(pullRequestFacade.getGithubUrl(inputFile1, 1)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L1")); 110 | 111 | PostJobIssue lineNotVisible = newMockedIssue("foo:src/Foo.php", inputFile1, 2, Severity.BLOCKER, true, "msg2"); 112 | when(pullRequestFacade.getGithubUrl(inputFile1, 2)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L2")); 113 | 114 | DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", "src/Foo2.php").build(); 115 | PostJobIssue fileNotInPR = newMockedIssue("foo:src/Foo2.php", inputFile2, 1, Severity.BLOCKER, true, "msg3"); 116 | 117 | PostJobIssue notNewIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, false, "msg"); 118 | 119 | PostJobIssue issueOnDir = newMockedIssue("foo:src", Severity.BLOCKER, true, "msg4"); 120 | 121 | PostJobIssue issueOnProject = newMockedIssue("foo", Severity.BLOCKER, true, "msg"); 122 | 123 | PostJobIssue globalIssue = newMockedIssue("foo:src/Foo.php", inputFile1, null, Severity.BLOCKER, true, "msg5"); 124 | 125 | when(context.issues()).thenReturn(Arrays.asList(newIssue, globalIssue, issueOnProject, issueOnDir, fileNotInPR, lineNotVisible, notNewIssue)); 126 | when(pullRequestFacade.hasFile(inputFile1)).thenReturn(true); 127 | when(pullRequestFacade.hasFileLine(inputFile1, 1)).thenReturn(true); 128 | 129 | pullRequestIssuePostJob.execute(context); 130 | verify(pullRequestFacade).createOrUpdateGlobalComments(contains("SonarQube analysis reported 5 issues")); 131 | verify(pullRequestFacade) 132 | .createOrUpdateGlobalComments(contains("* ![BLOCKER][BLOCKER] 5 blocker")); 133 | verify(pullRequestFacade) 134 | .createOrUpdateGlobalComments( 135 | not(contains("1. [Project"))); 136 | verify(pullRequestFacade) 137 | .createOrUpdateGlobalComments( 138 | contains( 139 | "1. ![BLOCKER][BLOCKER] [Foo.php#L2](http://github/blob/abc123/src/Foo.php#L2): msg2 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule)")); 140 | 141 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.ERROR, "SonarQube reported 5 issues, with 5 blocker"); 142 | } 143 | 144 | @Test 145 | public void testSortIssues() throws MalformedURLException { 146 | ArgumentCaptor commentCaptor = forClass(String.class); 147 | DefaultInputFile inputFile1 = new TestInputFileBuilder("foo", "src/Foo.php").build(); 148 | DefaultInputFile inputFile2 = new TestInputFileBuilder("foo", "src/Foo2.php").build(); 149 | 150 | // Blocker and 8th line => Should be displayed in 3rd position 151 | PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 8, Severity.BLOCKER, true, "msg1"); 152 | when(pullRequestFacade.getGithubUrl(inputFile1, 1)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L1")); 153 | 154 | // Blocker and 2nd line (Foo2.php) => Should be displayed in 4th position 155 | PostJobIssue issueInSecondFile = newMockedIssue("foo:src/Foo2.php", inputFile2, 2, Severity.BLOCKER, true, "msg2"); 156 | when(pullRequestFacade.getGithubUrl(inputFile1, 2)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L2")); 157 | 158 | // Major => Should be displayed in 6th position 159 | PostJobIssue newIssue2 = newMockedIssue("foo:src/Foo.php", inputFile1, 4, Severity.MAJOR, true, "msg3"); 160 | 161 | // Critical => Should be displayed in 5th position 162 | PostJobIssue newIssue3 = newMockedIssue("foo:src/Foo.php", inputFile1, 3, Severity.CRITICAL, true, "msg4"); 163 | 164 | // Critical => Should be displayed in 7th position 165 | PostJobIssue newIssue4 = newMockedIssue("foo:src/Foo.php", inputFile1, 13, Severity.INFO, true, "msg5"); 166 | 167 | // Blocker on project => Should be displayed 1st position 168 | PostJobIssue issueOnProject = newMockedIssue("foo", Severity.BLOCKER, true, "msg6"); 169 | 170 | // Blocker and no line => Should be displayed in 2nd position 171 | PostJobIssue globalIssue = newMockedIssue("foo:src/Foo.php", inputFile1, null, Severity.BLOCKER, true, "msg7"); 172 | 173 | when(context.issues()).thenReturn(Arrays.asList(newIssue, globalIssue, issueOnProject, newIssue4, newIssue2, issueInSecondFile, newIssue3)); 174 | when(pullRequestFacade.hasFile(any(InputFile.class))).thenReturn(true); 175 | when(pullRequestFacade.hasFileLine(any(InputFile.class), anyInt())).thenReturn(false); 176 | 177 | pullRequestIssuePostJob.execute(context); 178 | 179 | verify(pullRequestFacade).createOrUpdateGlobalComments(commentCaptor.capture()); 180 | 181 | String comment = commentCaptor.getValue(); 182 | assertThat(comment).containsSequence("msg6", "msg7", "msg1", "msg2", "msg4", "msg3", "msg5"); 183 | } 184 | 185 | @Test 186 | public void testPullRequestAnalysisWithNewCriticalIssues() throws MalformedURLException { 187 | DefaultInputFile inputFile1 = new TestInputFileBuilder("foo", "src/Foo.php").build(); 188 | PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.CRITICAL, true, "msg1"); 189 | when(pullRequestFacade.getGithubUrl(inputFile1, 1)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L1")); 190 | 191 | when(context.issues()).thenReturn(Arrays.asList(newIssue)); 192 | when(pullRequestFacade.hasFile(inputFile1)).thenReturn(true); 193 | when(pullRequestFacade.hasFileLine(inputFile1, 1)).thenReturn(true); 194 | 195 | pullRequestIssuePostJob.execute(context); 196 | 197 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.ERROR, "SonarQube reported 1 issue, with 1 critical"); 198 | } 199 | 200 | @Test 201 | public void testPullRequestAnalysisWithNewIssuesNoBlockerNorCritical() throws MalformedURLException { 202 | DefaultInputFile inputFile1 = new TestInputFileBuilder("foo", "src/Foo.php").build(); 203 | PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.MAJOR, true, "msg1"); 204 | when(pullRequestFacade.getGithubUrl(inputFile1, 1)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L1")); 205 | 206 | when(context.issues()).thenReturn(Arrays.asList(newIssue)); 207 | when(pullRequestFacade.hasFile(inputFile1)).thenReturn(true); 208 | when(pullRequestFacade.hasFileLine(inputFile1, 1)).thenReturn(true); 209 | 210 | pullRequestIssuePostJob.execute(context); 211 | 212 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.SUCCESS, "SonarQube reported 1 issue, no criticals or blockers"); 213 | } 214 | 215 | @Test 216 | public void testPullRequestAnalysisWithNewBlockerAndCriticalIssues() throws MalformedURLException { 217 | DefaultInputFile inputFile1 = new TestInputFileBuilder("foo", "src/Foo.php").build(); 218 | PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.CRITICAL, true, "msg1"); 219 | when(pullRequestFacade.getGithubUrl(inputFile1, 1)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L1")); 220 | 221 | PostJobIssue lineNotVisible = newMockedIssue("foo:src/Foo.php", inputFile1, 2, Severity.BLOCKER, true, "msg2"); 222 | when(pullRequestFacade.getGithubUrl(inputFile1, 2)).thenReturn(new URL("http://github/blob/abc123/src/Foo.php#L2")); 223 | 224 | when(context.issues()).thenReturn(Arrays.asList(newIssue, lineNotVisible)); 225 | when(pullRequestFacade.hasFile(inputFile1)).thenReturn(true); 226 | when(pullRequestFacade.hasFileLine(inputFile1, 1)).thenReturn(true); 227 | 228 | pullRequestIssuePostJob.execute(context); 229 | 230 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.ERROR, "SonarQube reported 2 issues, with 1 critical and 1 blocker"); 231 | } 232 | 233 | @Test 234 | public void should_update_sonarqube_status_even_if_unexpected_errors_were_raised() { 235 | String innerMsg = "Failed to get issues"; 236 | // not really realistic unexpected error, but good enough for this test 237 | when(context.issues()).thenThrow(new IllegalStateException(innerMsg)); 238 | pullRequestIssuePostJob.execute(context); 239 | 240 | String msg = "SonarQube analysis failed: " + innerMsg; 241 | verify(pullRequestFacade).createOrUpdateSonarQubeStatus(GHCommitState.ERROR, msg); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/github/PullRequestFacade.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.io.BufferedReader; 23 | import java.io.File; 24 | import java.io.FileNotFoundException; 25 | import java.io.IOException; 26 | import java.io.StringReader; 27 | import java.net.MalformedURLException; 28 | import java.net.URI; 29 | import java.net.URISyntaxException; 30 | import java.net.URL; 31 | import java.util.HashMap; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.regex.Matcher; 35 | import java.util.regex.Pattern; 36 | import javax.annotation.CheckForNull; 37 | import javax.annotation.Nullable; 38 | import org.kohsuke.github.GHCommitState; 39 | import org.kohsuke.github.GHCommitStatus; 40 | import org.kohsuke.github.GHIssueComment; 41 | import org.kohsuke.github.GHPullRequest; 42 | import org.kohsuke.github.GHPullRequestFileDetail; 43 | import org.kohsuke.github.GHPullRequestReviewComment; 44 | import org.kohsuke.github.GHRepository; 45 | import org.kohsuke.github.GitHub; 46 | import org.kohsuke.github.GitHubBuilder; 47 | import org.sonar.api.batch.BatchSide; 48 | import org.sonar.api.batch.InstantiationStrategy; 49 | import org.sonar.api.batch.ScannerSide; 50 | import org.sonar.api.batch.fs.InputComponent; 51 | import org.sonar.api.batch.fs.InputFile; 52 | import org.sonar.api.batch.fs.InputPath; 53 | import org.sonar.api.scan.filesystem.PathResolver; 54 | import org.sonar.api.utils.MessageException; 55 | import org.sonar.api.utils.log.Logger; 56 | import org.sonar.api.utils.log.Loggers; 57 | 58 | /** 59 | * Facade for all WS interaction with GitHub. 60 | */ 61 | @ScannerSide 62 | @InstantiationStrategy(InstantiationStrategy.PER_BATCH) 63 | public class PullRequestFacade { 64 | 65 | private static final Logger LOG = Loggers.get(PullRequestFacade.class); 66 | 67 | static final String COMMIT_CONTEXT = "sonarqube"; 68 | 69 | private final GitHubPluginConfiguration config; 70 | private Map> patchPositionMappingByFile; 71 | private Map> existingReviewCommentsByLocationByFile = new HashMap<>(); 72 | private GHRepository ghRepo; 73 | private GHPullRequest pr; 74 | private Map reviewCommentToBeDeletedById = new HashMap<>(); 75 | private File gitBaseDir; 76 | private String myself; 77 | 78 | public PullRequestFacade(GitHubPluginConfiguration config) { 79 | this.config = config; 80 | } 81 | 82 | public void init(int pullRequestNumber, File projectBaseDir) { 83 | initGitBaseDir(projectBaseDir); 84 | try { 85 | GitHub github; 86 | if (config.isProxyConnectionEnabled()) { 87 | github = new GitHubBuilder().withProxy(config.getHttpProxy()).withEndpoint(config.endpoint()).withOAuthToken(config.oauth()).build(); 88 | } else { 89 | github = new GitHubBuilder().withEndpoint(config.endpoint()).withOAuthToken(config.oauth()).build(); 90 | } 91 | setGhRepo(github.getRepository(config.repository())); 92 | setPr(ghRepo.getPullRequest(pullRequestNumber)); 93 | LOG.info("Starting analysis of pull request: " + pr.getHtmlUrl()); 94 | myself = github.getMyself().getLogin(); 95 | loadExistingReviewComments(); 96 | patchPositionMappingByFile = mapPatchPositionsToLines(pr); 97 | } catch (IOException e) { 98 | LOG.debug("Unable to perform GitHub WS operation", e); 99 | throw MessageException.of("Unable to perform GitHub WS operation: " + e.getMessage()); 100 | } 101 | } 102 | 103 | void initGitBaseDir(File projectBaseDir) { 104 | File detectedGitBaseDir = findGitBaseDir(projectBaseDir); 105 | if (detectedGitBaseDir == null) { 106 | LOG.debug("Unable to find Git root directory. Is " + projectBaseDir + " part of a Git repository?"); 107 | setGitBaseDir(projectBaseDir); 108 | } else { 109 | setGitBaseDir(detectedGitBaseDir); 110 | } 111 | } 112 | 113 | void setGhRepo(GHRepository ghRepo) { 114 | this.ghRepo = ghRepo; 115 | } 116 | 117 | void setPr(GHPullRequest pr) { 118 | this.pr = pr; 119 | } 120 | 121 | public File findGitBaseDir(@Nullable File baseDir) { 122 | if (baseDir == null) { 123 | return null; 124 | } 125 | if (new File(baseDir, ".git").exists()) { 126 | return baseDir; 127 | } 128 | return findGitBaseDir(baseDir.getParentFile()); 129 | } 130 | 131 | void setGitBaseDir(File gitBaseDir) { 132 | this.gitBaseDir = gitBaseDir; 133 | } 134 | 135 | /** 136 | * Load all previous comments made by provided github account. 137 | */ 138 | private void loadExistingReviewComments() throws IOException { 139 | for (GHPullRequestReviewComment comment : pr.listReviewComments()) { 140 | if (!myself.equals(comment.getUser().getLogin())) { 141 | // Ignore comments from other users 142 | continue; 143 | } 144 | if (!existingReviewCommentsByLocationByFile.containsKey(comment.getPath())) { 145 | existingReviewCommentsByLocationByFile.put(comment.getPath(), new HashMap()); 146 | } 147 | // By default all previous comments will be marked for deletion 148 | reviewCommentToBeDeletedById.put(comment.getId(), comment); 149 | existingReviewCommentsByLocationByFile.get(comment.getPath()).put(comment.getPosition(), comment); 150 | } 151 | } 152 | 153 | /** 154 | * GitHub expect review comments to be added on "patch lines" (aka position) but not on file lines. 155 | * So we have to iterate over each patch and compute corresponding file line in order to later map issues to the correct position. 156 | * @return Map File path -> Line -> Position 157 | */ 158 | private Map> mapPatchPositionsToLines(GHPullRequest pr) throws IOException { 159 | Map> result = new HashMap<>(); 160 | for (GHPullRequestFileDetail file : pr.listFiles()) { 161 | Map patchLocationMapping = new HashMap<>(); 162 | result.put(file.getFilename(), patchLocationMapping); 163 | if (config.tryReportIssuesInline()) { 164 | String patch = file.getPatch(); 165 | if (patch == null) { 166 | continue; 167 | } 168 | processPatch(patchLocationMapping, patch); 169 | } 170 | } 171 | return result; 172 | } 173 | 174 | static void processPatch(Map patchLocationMapping, String patch) throws IOException { 175 | int currentLine = -1; 176 | int patchLocation = 0; 177 | BufferedReader reader = new BufferedReader(new StringReader(patch)); 178 | String line; 179 | while ((line = reader.readLine()) != null) { 180 | if (line.startsWith("@")) { 181 | // http://en.wikipedia.org/wiki/Diff_utility#Unified_format 182 | Matcher matcher = Pattern.compile("@@\\p{IsWhite_Space}-[0-9]+(?:,[0-9]+)?\\p{IsWhite_Space}\\+([0-9]+)(?:,[0-9]+)?\\p{IsWhite_Space}@@.*").matcher(line); 183 | if (!matcher.matches()) { 184 | throw new IllegalStateException("Unable to parse patch line " + line + "\nFull patch: \n" + patch); 185 | } 186 | currentLine = Integer.parseInt(matcher.group(1)); 187 | } else if (line.startsWith("-")) { 188 | // Skip removed lines 189 | } else if (line.startsWith("+") || line.startsWith(" ")) { 190 | // Count added and unmodified lines 191 | patchLocationMapping.put(currentLine, patchLocation); 192 | currentLine++; 193 | } else if (line.startsWith("\\")) { 194 | // I'm only aware of \ No newline at end of file 195 | // Ignore 196 | } 197 | patchLocation++; 198 | } 199 | } 200 | 201 | String getPath(InputPath inputPath) { 202 | return new PathResolver().relativePath(gitBaseDir, inputPath.file()); 203 | } 204 | 205 | /** 206 | * Test if the P/R contains the provided file path (ie this file was added/modified/updated) 207 | */ 208 | public boolean hasFile(InputFile inputFile) { 209 | return patchPositionMappingByFile.containsKey(getPath(inputFile)); 210 | } 211 | 212 | /** 213 | * Test if the P/R contains the provided line for the file path (ie this line is "visible" in diff) 214 | */ 215 | public boolean hasFileLine(InputFile inputFile, int line) { 216 | return patchPositionMappingByFile.get(getPath(inputFile)).containsKey(line); 217 | } 218 | 219 | public void createOrUpdateReviewComment(InputFile inputFile, Integer line, String body) { 220 | String fullpath = getPath(inputFile); 221 | Integer lineInPatch = patchPositionMappingByFile.get(fullpath).get(line); 222 | try { 223 | if (existingReviewCommentsByLocationByFile.containsKey(fullpath) && existingReviewCommentsByLocationByFile.get(fullpath).containsKey(lineInPatch)) { 224 | GHPullRequestReviewComment existingReview = existingReviewCommentsByLocationByFile.get(fullpath).get(lineInPatch); 225 | if (!existingReview.getBody().equals(body)) { 226 | existingReview.update(body); 227 | } 228 | reviewCommentToBeDeletedById.remove(existingReview.getId()); 229 | } else { 230 | pr.createReviewComment(body, pr.getHead().getSha(), fullpath, lineInPatch); 231 | } 232 | } catch (IOException e) { 233 | throw new IllegalStateException("Unable to create or update review comment in file " + fullpath + " at line " + line, e); 234 | } 235 | 236 | } 237 | 238 | public void deleteOutdatedComments() { 239 | for (GHPullRequestReviewComment reviewToDelete : reviewCommentToBeDeletedById.values()) { 240 | try { 241 | reviewToDelete.delete(); 242 | } catch (IOException e) { 243 | throw new IllegalStateException("Unable to delete review comment with id " + reviewToDelete.getId(), e); 244 | } 245 | } 246 | } 247 | 248 | public void createOrUpdateGlobalComments(@Nullable String markup) { 249 | try { 250 | boolean found = findAndDeleteOthers(markup); 251 | if (markup != null && !found) { 252 | pr.comment(markup); 253 | } 254 | } catch (IOException e) { 255 | throw new IllegalStateException("Unable to read the pull request comments", e); 256 | } 257 | } 258 | 259 | private boolean findAndDeleteOthers(@Nullable String markup) throws IOException { 260 | boolean found = false; 261 | for (GHIssueComment comment : pr.listComments()) { 262 | if (myself.equals(comment.getUser().getLogin())) { 263 | if (markup == null || found || !markup.equals(comment.getBody())) { 264 | comment.delete(); 265 | continue; 266 | } 267 | if (markup.equals(comment.getBody())) { 268 | found = true; 269 | } 270 | } 271 | } 272 | return found; 273 | } 274 | 275 | public void createOrUpdateSonarQubeStatus(GHCommitState status, String statusDescription) { 276 | try { 277 | // Copy previous targetUrl in case it was set by an external system (like the CI job). 278 | String targetUrl = null; 279 | GHCommitStatus lastStatus = getCommitStatusForContext(pr, COMMIT_CONTEXT); 280 | if (lastStatus != null) { 281 | targetUrl = lastStatus.getTargetUrl(); 282 | } 283 | ghRepo.createCommitStatus(pr.getHead().getSha(), status, targetUrl, statusDescription, COMMIT_CONTEXT); 284 | } catch (FileNotFoundException e) { 285 | String msg = "Unable to set pull request status. GitHub account probably miss push permission on the repository."; 286 | if (LOG.isDebugEnabled()) { 287 | LOG.warn(msg, e); 288 | } else { 289 | LOG.warn(msg); 290 | } 291 | } catch (IOException e) { 292 | throw new IllegalStateException("Unable to update commit status", e); 293 | } 294 | } 295 | 296 | @CheckForNull 297 | public URL getGithubUrl(@Nullable InputComponent inputComponent, @Nullable Integer issueLine) { 298 | if (inputComponent instanceof InputPath) { 299 | String path = getPath((InputPath) inputComponent); 300 | URL url1 = ghRepo.getHtmlUrl(); 301 | try { 302 | return new URI(url1.getProtocol(), null, url1.getHost(), url1.getPort(), 303 | url1.getFile() + "/blob/" + pr.getHead().getSha() + "/" + path, null, issueLine != null ? ("L" + issueLine) : "").toURL(); 304 | } catch (MalformedURLException | URISyntaxException e) { 305 | LOG.error("Invalid URL", e); 306 | } 307 | } 308 | return null; 309 | } 310 | 311 | @CheckForNull 312 | GHCommitStatus getCommitStatusForContext(GHPullRequest pr, String context) { 313 | List statuses; 314 | try { 315 | statuses = pr.getRepository().listCommitStatuses(pr.getHead().getSha()).asList(); 316 | } catch (IOException e) { 317 | throw new IllegalStateException("Unable to retrieve commit statuses.", e); 318 | } 319 | for (GHCommitStatus status : statuses) { 320 | if (context.equals(status.getContext())) { 321 | return status; 322 | } 323 | } 324 | return null; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/github/GlobalReportTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube :: GitHub Plugin 3 | * Copyright (C) 2015-2018 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.github; 21 | 22 | import java.net.MalformedURLException; 23 | import java.net.URI; 24 | import java.net.URISyntaxException; 25 | import java.net.URL; 26 | import javax.annotation.CheckForNull; 27 | import org.junit.Before; 28 | import org.junit.Test; 29 | import org.sonar.api.CoreProperties; 30 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 31 | import org.sonar.api.batch.postjob.issue.PostJobIssue; 32 | import org.sonar.api.batch.rule.Severity; 33 | import org.sonar.api.config.PropertyDefinition; 34 | import org.sonar.api.config.PropertyDefinitions; 35 | import org.sonar.api.config.Settings; 36 | import org.sonar.api.config.internal.MapSettings; 37 | import org.sonar.api.rule.RuleKey; 38 | 39 | import static org.assertj.core.api.Assertions.assertThat; 40 | import static org.mockito.Mockito.mock; 41 | import static org.mockito.Mockito.when; 42 | 43 | public class GlobalReportTest { 44 | 45 | private static final URL GITHUB_URL = parse("https://github.com/SonarSource/sonar-github"); 46 | 47 | private MapSettings settings; 48 | 49 | @Before 50 | public void setup() { 51 | settings = new MapSettings(new PropertyDefinitions(PropertyDefinition.builder(CoreProperties.SERVER_BASE_URL) 52 | .name("Server base URL") 53 | .description("HTTP URL of this SonarQube server, such as http://yourhost.yourdomain/sonar. This value is used i.e. to create links in emails.") 54 | .category(CoreProperties.CATEGORY_GENERAL) 55 | .defaultValue(CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE) 56 | .build())); 57 | 58 | settings.setProperty("sonar.host.url", "http://myserver"); 59 | } 60 | 61 | private static URL parse(String url) { 62 | try { 63 | return new URL(url); 64 | } catch (MalformedURLException e) { 65 | throw new IllegalStateException(e); 66 | } 67 | } 68 | 69 | private PostJobIssue newMockedIssue(String componentKey, @CheckForNull DefaultInputFile inputFile, @CheckForNull Integer line, Severity severity, boolean isNew, String message, 70 | String rule) { 71 | PostJobIssue issue = mock(PostJobIssue.class); 72 | when(issue.inputComponent()).thenReturn(inputFile); 73 | when(issue.componentKey()).thenReturn(componentKey); 74 | if (line != null) { 75 | when(issue.line()).thenReturn(line); 76 | } 77 | when(issue.ruleKey()).thenReturn(RuleKey.of("repo", rule)); 78 | when(issue.severity()).thenReturn(severity); 79 | when(issue.isNew()).thenReturn(isNew); 80 | when(issue.message()).thenReturn(message); 81 | 82 | return issue; 83 | } 84 | 85 | @Test 86 | public void noIssues() { 87 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 88 | 89 | String desiredMarkdown = "SonarQube analysis reported no issues."; 90 | 91 | String formattedGlobalReport = globalReport.formatForMarkdown(); 92 | 93 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 94 | } 95 | 96 | @Test 97 | public void oneIssue() { 98 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 99 | globalReport.process(newMockedIssue("component", null, null, Severity.INFO, true, "Issue", "rule"), GITHUB_URL, true); 100 | 101 | String desiredMarkdown = "SonarQube analysis reported 1 issue\n" + 102 | "* ![INFO][INFO] 1 info\n" + 103 | "\nWatch the comments in this conversation to review them.\n" + 104 | "\n[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'"; 105 | 106 | String formattedGlobalReport = globalReport.formatForMarkdown(); 107 | 108 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 109 | } 110 | 111 | @Test 112 | public void oneIssueOnDir() { 113 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 114 | globalReport.process(newMockedIssue("component0", null, null, Severity.INFO, true, "Issue0", "rule0"), null, false); 115 | 116 | String desiredMarkdown = "SonarQube analysis reported 1 issue\n\n" + 117 | "Note: The following issues were found on lines that were not modified in the pull request. Because these issues can't be reported as line comments, they are summarized here:\n\n" 118 | + 119 | "1. ![INFO][INFO] component0: Issue0 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule0)\n" + 120 | "\n[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'"; 121 | 122 | String formattedGlobalReport = globalReport.formatForMarkdown(); 123 | 124 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 125 | } 126 | 127 | @Test 128 | public void shouldFormatIssuesForMarkdownNoInline() { 129 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 130 | globalReport.process(newMockedIssue("component", null, null, Severity.INFO, true, "Issue", "rule"), GITHUB_URL, true); 131 | globalReport.process(newMockedIssue("component", null, null, Severity.MINOR, true, "Issue", "rule"), GITHUB_URL, true); 132 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue", "rule"), GITHUB_URL, true); 133 | globalReport.process(newMockedIssue("component", null, null, Severity.CRITICAL, true, "Issue", "rule"), GITHUB_URL, true); 134 | globalReport.process(newMockedIssue("component", null, null, Severity.BLOCKER, true, "Issue", "rule"), GITHUB_URL, true); 135 | 136 | String desiredMarkdown = "SonarQube analysis reported 5 issues\n" + 137 | "* ![BLOCKER][BLOCKER] 1 blocker\n" + 138 | "* ![CRITICAL][CRITICAL] 1 critical\n" + 139 | "* ![MAJOR][MAJOR] 1 major\n" + 140 | "* ![MINOR][MINOR] 1 minor\n" + 141 | "* ![INFO][INFO] 1 info\n" + 142 | "\nWatch the comments in this conversation to review them.\n" 143 | + "\n" 144 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 145 | + "[CRITICAL]: https://sonarsource.github.io/sonar-github/severity-critical.png 'Severity: CRITICAL'\n" 146 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'\n" 147 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'\n" 148 | + "[MINOR]: https://sonarsource.github.io/sonar-github/severity-minor.png 'Severity: MINOR'"; 149 | 150 | String formattedGlobalReport = globalReport.formatForMarkdown(); 151 | 152 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 153 | } 154 | 155 | @Test 156 | public void shouldFormatIssuesForMarkdownMixInlineGlobal() { 157 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 158 | globalReport.process(newMockedIssue("component", null, null, Severity.INFO, true, "Issue 0", "rule0"), GITHUB_URL, true); 159 | globalReport.process(newMockedIssue("component", null, null, Severity.MINOR, true, "Issue 1", "rule1"), GITHUB_URL, false); 160 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue 2", "rule2"), GITHUB_URL, true); 161 | globalReport.process(newMockedIssue("component", null, null, Severity.CRITICAL, true, "Issue 3", "rule3"), GITHUB_URL, false); 162 | globalReport.process(newMockedIssue("component", null, null, Severity.BLOCKER, true, "Issue 4", "rule4"), GITHUB_URL, true); 163 | 164 | String desiredMarkdown = "SonarQube analysis reported 5 issues\n" + 165 | "* ![BLOCKER][BLOCKER] 1 blocker\n" + 166 | "* ![CRITICAL][CRITICAL] 1 critical\n" + 167 | "* ![MAJOR][MAJOR] 1 major\n" + 168 | "* ![MINOR][MINOR] 1 minor\n" + 169 | "* ![INFO][INFO] 1 info\n" + 170 | "\nWatch the comments in this conversation to review them.\n" + 171 | "\n#### 2 extra issues\n" + 172 | "\nNote: The following issues were found on lines that were not modified in the pull request. Because these issues can't be reported as line comments, they are summarized here:\n\n" 173 | + 174 | "1. ![MINOR][MINOR] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 1 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule1)\n" 175 | + 176 | "1. ![CRITICAL][CRITICAL] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 3 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule3)\n" 177 | + "\n" 178 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 179 | + "[CRITICAL]: https://sonarsource.github.io/sonar-github/severity-critical.png 'Severity: CRITICAL'\n" 180 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'\n" 181 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'\n" 182 | + "[MINOR]: https://sonarsource.github.io/sonar-github/severity-minor.png 'Severity: MINOR'"; 183 | 184 | String formattedGlobalReport = globalReport.formatForMarkdown(); 185 | 186 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 187 | } 188 | 189 | @Test 190 | public void shouldFormatIssuesForMarkdownWhenInlineCommentsDisabled() { 191 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), false); 192 | globalReport.process(newMockedIssue("component", null, null, Severity.INFO, true, "Issue 0", "rule0"), GITHUB_URL, false); 193 | globalReport.process(newMockedIssue("component", null, null, Severity.MINOR, true, "Issue 1", "rule1"), GITHUB_URL, false); 194 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue 2", "rule2"), GITHUB_URL, false); 195 | globalReport.process(newMockedIssue("component", null, null, Severity.CRITICAL, true, "Issue 3", "rule3"), GITHUB_URL, false); 196 | globalReport.process(newMockedIssue("component", null, null, Severity.BLOCKER, true, "Issue 4", "rule4"), GITHUB_URL, false); 197 | 198 | String desiredMarkdown = "SonarQube analysis reported 5 issues\n\n" + 199 | "1. ![INFO][INFO] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 0 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule0)\n" 200 | + 201 | "1. ![MINOR][MINOR] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 1 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule1)\n" 202 | + 203 | "1. ![MAJOR][MAJOR] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 2 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule2)\n" 204 | + 205 | "1. ![CRITICAL][CRITICAL] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 3 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule3)\n" 206 | + 207 | "1. ![BLOCKER][BLOCKER] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 4 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule4)\n" 208 | + "\n" 209 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 210 | + "[CRITICAL]: https://sonarsource.github.io/sonar-github/severity-critical.png 'Severity: CRITICAL'\n" 211 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'\n" 212 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'\n" 213 | + "[MINOR]: https://sonarsource.github.io/sonar-github/severity-minor.png 'Severity: MINOR'"; 214 | 215 | String formattedGlobalReport = globalReport.formatForMarkdown(); 216 | 217 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 218 | } 219 | 220 | @Test 221 | public void shouldFormatIssuesForMarkdownWhenInlineCommentsDisabledAndLimitReached() { 222 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), false, 4); 223 | globalReport.process(newMockedIssue("component", null, null, Severity.INFO, true, "Issue 0", "rule0"), GITHUB_URL, false); 224 | globalReport.process(newMockedIssue("component", null, null, Severity.MINOR, true, "Issue 1", "rule1"), GITHUB_URL, false); 225 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue 2", "rule2"), GITHUB_URL, false); 226 | globalReport.process(newMockedIssue("component", null, null, Severity.CRITICAL, true, "Issue 3", "rule3"), GITHUB_URL, false); 227 | globalReport.process(newMockedIssue("component", null, null, Severity.BLOCKER, true, "Issue 4", "rule4"), GITHUB_URL, false); 228 | 229 | String desiredMarkdown = "SonarQube analysis reported 5 issues\n" + 230 | "* ![BLOCKER][BLOCKER] 1 blocker\n" + 231 | "* ![CRITICAL][CRITICAL] 1 critical\n" + 232 | "* ![MAJOR][MAJOR] 1 major\n" + 233 | "* ![MINOR][MINOR] 1 minor\n" + 234 | "* ![INFO][INFO] 1 info\n" + 235 | "\n#### Top 4 issues\n\n" + 236 | "1. ![INFO][INFO] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 0 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule0)\n" 237 | + 238 | "1. ![MINOR][MINOR] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 1 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule1)\n" 239 | + 240 | "1. ![MAJOR][MAJOR] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 2 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule2)\n" 241 | + 242 | "1. ![CRITICAL][CRITICAL] [sonar-github](https://github.com/SonarSource/sonar-github): Issue 3 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule3)\n" 243 | + "\n" 244 | + "[BLOCKER]: https://sonarsource.github.io/sonar-github/severity-blocker.png 'Severity: BLOCKER'\n" 245 | + "[CRITICAL]: https://sonarsource.github.io/sonar-github/severity-critical.png 'Severity: CRITICAL'\n" 246 | + "[INFO]: https://sonarsource.github.io/sonar-github/severity-info.png 'Severity: INFO'\n" 247 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'\n" 248 | + "[MINOR]: https://sonarsource.github.io/sonar-github/severity-minor.png 'Severity: MINOR'"; 249 | 250 | String formattedGlobalReport = globalReport.formatForMarkdown(); 251 | 252 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 253 | } 254 | 255 | @Test 256 | public void shouldLimitGlobalIssues() throws MalformedURLException, URISyntaxException { 257 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), true); 258 | for (int i = 0; i < 17; i++) { 259 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue number:" + i, "rule" + i), 260 | new URI(GITHUB_URL.getProtocol(), null, GITHUB_URL.getHost(), GITHUB_URL.getPort(), 261 | GITHUB_URL.getFile() + "/with space/File.java", null, "L" + i).toURL(), 262 | false); 263 | } 264 | 265 | String desiredMarkdown = "SonarQube analysis reported 17 issues\n" + 266 | "* ![MAJOR][MAJOR] 17 major\n" + 267 | "\n#### Top 10 extra issues\n" + 268 | "\nNote: The following issues were found on lines that were not modified in the pull request. Because these issues can't be reported as line comments, they are summarized here:\n\n" 269 | + 270 | "1. ![MAJOR][MAJOR] [File.java#L0](https://github.com/SonarSource/sonar-github/with%20space/File.java#L0): Issue number:0 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule0)\n" 271 | + 272 | "1. ![MAJOR][MAJOR] [File.java#L1](https://github.com/SonarSource/sonar-github/with%20space/File.java#L1): Issue number:1 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule1)\n" 273 | + 274 | "1. ![MAJOR][MAJOR] [File.java#L2](https://github.com/SonarSource/sonar-github/with%20space/File.java#L2): Issue number:2 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule2)\n" 275 | + 276 | "1. ![MAJOR][MAJOR] [File.java#L3](https://github.com/SonarSource/sonar-github/with%20space/File.java#L3): Issue number:3 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule3)\n" 277 | + 278 | "1. ![MAJOR][MAJOR] [File.java#L4](https://github.com/SonarSource/sonar-github/with%20space/File.java#L4): Issue number:4 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule4)\n" 279 | + 280 | "1. ![MAJOR][MAJOR] [File.java#L5](https://github.com/SonarSource/sonar-github/with%20space/File.java#L5): Issue number:5 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule5)\n" 281 | + 282 | "1. ![MAJOR][MAJOR] [File.java#L6](https://github.com/SonarSource/sonar-github/with%20space/File.java#L6): Issue number:6 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule6)\n" 283 | + 284 | "1. ![MAJOR][MAJOR] [File.java#L7](https://github.com/SonarSource/sonar-github/with%20space/File.java#L7): Issue number:7 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule7)\n" 285 | + 286 | "1. ![MAJOR][MAJOR] [File.java#L8](https://github.com/SonarSource/sonar-github/with%20space/File.java#L8): Issue number:8 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule8)\n" 287 | + 288 | "1. ![MAJOR][MAJOR] [File.java#L9](https://github.com/SonarSource/sonar-github/with%20space/File.java#L9): Issue number:9 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule9)\n" 289 | + "\n" 290 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'"; 291 | 292 | String formattedGlobalReport = globalReport.formatForMarkdown(); 293 | 294 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 295 | } 296 | 297 | @Test 298 | public void shouldLimitGlobalIssuesWhenInlineCommentsDisabled() throws MalformedURLException, URISyntaxException { 299 | GlobalReport globalReport = new GlobalReport(new MarkDownUtils(settings), false); 300 | for (int i = 0; i < 17; i++) { 301 | globalReport.process(newMockedIssue("component", null, null, Severity.MAJOR, true, "Issue number:" + i, "rule" + i), 302 | new URI(GITHUB_URL.getProtocol(), null, GITHUB_URL.getHost(), GITHUB_URL.getPort(), 303 | GITHUB_URL.getFile() + "/File.java", null, "L" + i).toURL(), 304 | false); 305 | } 306 | 307 | String desiredMarkdown = "SonarQube analysis reported 17 issues\n" + 308 | "* ![MAJOR][MAJOR] 17 major\n" + 309 | "\n#### Top 10 issues\n\n" + 310 | "1. ![MAJOR][MAJOR] [File.java#L0](https://github.com/SonarSource/sonar-github/File.java#L0): Issue number:0 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule0)\n" 311 | + 312 | "1. ![MAJOR][MAJOR] [File.java#L1](https://github.com/SonarSource/sonar-github/File.java#L1): Issue number:1 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule1)\n" 313 | + 314 | "1. ![MAJOR][MAJOR] [File.java#L2](https://github.com/SonarSource/sonar-github/File.java#L2): Issue number:2 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule2)\n" 315 | + 316 | "1. ![MAJOR][MAJOR] [File.java#L3](https://github.com/SonarSource/sonar-github/File.java#L3): Issue number:3 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule3)\n" 317 | + 318 | "1. ![MAJOR][MAJOR] [File.java#L4](https://github.com/SonarSource/sonar-github/File.java#L4): Issue number:4 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule4)\n" 319 | + 320 | "1. ![MAJOR][MAJOR] [File.java#L5](https://github.com/SonarSource/sonar-github/File.java#L5): Issue number:5 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule5)\n" 321 | + 322 | "1. ![MAJOR][MAJOR] [File.java#L6](https://github.com/SonarSource/sonar-github/File.java#L6): Issue number:6 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule6)\n" 323 | + 324 | "1. ![MAJOR][MAJOR] [File.java#L7](https://github.com/SonarSource/sonar-github/File.java#L7): Issue number:7 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule7)\n" 325 | + 326 | "1. ![MAJOR][MAJOR] [File.java#L8](https://github.com/SonarSource/sonar-github/File.java#L8): Issue number:8 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule8)\n" 327 | + 328 | "1. ![MAJOR][MAJOR] [File.java#L9](https://github.com/SonarSource/sonar-github/File.java#L9): Issue number:9 [![rule](https://sonarsource.github.io/sonar-github/rule.png)](http://myserver/coding_rules#rule_key=repo%3Arule9)\n" 329 | + "\n" 330 | + "[MAJOR]: https://sonarsource.github.io/sonar-github/severity-major.png 'Severity: MAJOR'"; 331 | 332 | String formattedGlobalReport = globalReport.formatForMarkdown(); 333 | 334 | assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown); 335 | } 336 | } 337 | --------------------------------------------------------------------------------