diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64f97e8..df59df1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,6 @@ on: # yamllint disable-line rule:truthy jobs: build: name: "build" - permissions: - checks: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -42,7 +40,7 @@ jobs: java-version: '17' cache: 'gradle' - name: Cache SonarQube packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -90,7 +88,8 @@ jobs: - name: Qualityannotate if: success() && steps.findPr.outputs.number env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} + # ${{ secrets.TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PULL_REQUEST: ${{ steps.findPr.outputs.pr }} GITHUB_PROJECT: balrok/qualityannotate # ${{env.GITHUB_ACTION_REPOSITORY}} SONARQUBE_PROJECT: quyt_qualityannotate diff --git a/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java b/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java index cfc7af5..e55246f 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java @@ -2,8 +2,11 @@ import java.util.List; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + public interface CodeRepository { - void createOrUpdateAnnotations(Comment globalComment, List fileComments) throws Exception; + void createOrUpdateAnnotations(Comment globalComment, List fileComments, + MetricsAndIssues metricsAndIssues) throws Exception; String printConfigWithoutSecrets(); } diff --git a/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java b/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java new file mode 100644 index 0000000..da26cd3 --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java @@ -0,0 +1,14 @@ +package org.qualityannotate.api.coderepository; + +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + +public final class CodeStructuredExecutor { + private CodeStructuredExecutor() { + + } + + public static void run(CodeStructuredApi codeStructuredApi, MetricsAndIssues metricsAndIssues) throws Exception { + codeStructuredApi.update(metricsAndIssues); + } +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java b/src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java similarity index 67% rename from src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java rename to src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java index 984ec9c..17c2af7 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java @@ -4,41 +4,25 @@ import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.api.coderepository.api.CodeFileComment; -import org.qualityannotate.api.coderepository.api.CodeMainComment; +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.coderepository.api.CodeTextApi; +import org.qualityannotate.api.coderepository.api.CodeTextFileComment; +import org.qualityannotate.api.coderepository.api.CodeTextMainComment; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; -public abstract class AbstractCodeRepository implements CodeRepository { +public final class CodeTextExecutor { + private CodeTextExecutor() { - protected static Map, String> convertFileCommentsToMap(List fileComments) { - Map, List> fileLineToCommentList = new HashMap<>(); - for (FileComment fileComment : fileComments) { - List comments = fileLineToCommentList - .computeIfAbsent(Pair.of(fileComment.fileName(), fileComment.linenumber()), k -> new ArrayList<>()); - comments.add(fileComment.comment()); - } - return fileLineToCommentList.entrySet() - .stream() - .map(e -> Map.entry(e.getKey(), e.getValue().stream().reduce((c1, c2) -> c1).orElse(Comment.EMPTY))) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().markdown())); } - protected abstract CodeApi getCodeApi(); - - @Override - public void createOrUpdateAnnotations(Comment globalComment, List fileComments) { - CodeApi codeApi = getCodeApi(); - Optional mainComment = Optional.empty(); + public static void run(CodeTextApi codeTextApi, Comment globalComment, List fileComments) + throws Exception { + Optional mainComment = Optional.empty(); try { - mainComment = codeApi.getMainComment(); + mainComment = codeTextApi.getMainComment(); } catch (IOException e) { Log.warn("Could not retrieve main comment"); } @@ -50,20 +34,20 @@ public void createOrUpdateAnnotations(Comment globalComment, List f } }, () -> { try { - codeApi.createMainComment(globalComment.markdown()); + codeTextApi.createMainComment(globalComment.markdown()); } catch (IOException e) { Log.warn("Could not create main comment", e); } }); Map, String> fileLineToComment = convertFileCommentsToMap(fileComments); - List githubFileComments = Collections.emptyList(); + List githubFileComments = Collections.emptyList(); try { - githubFileComments = codeApi.listFileComments(); + githubFileComments = codeTextApi.listFileComments(); } catch (IOException e) { Log.warn("Could not retrieve comments"); } - for (CodeFileComment review : githubFileComments) { + for (CodeTextFileComment review : githubFileComments) { String comment = fileLineToComment.get(review.getFileLine()); Log.info("Found existing file comment"); if (comment != null) { @@ -90,11 +74,29 @@ public void createOrUpdateAnnotations(Comment globalComment, List f for (Map.Entry, String> fileLineCommentEntry : fileLineToComment.entrySet()) { try { Log.infof("Creating %s", fileLineCommentEntry.getKey()); - codeApi.createFileComment(fileLineCommentEntry.getKey().getLeft(), + codeTextApi.createFileComment(fileLineCommentEntry.getKey().getLeft(), fileLineCommentEntry.getKey().getRight(), fileLineCommentEntry.getValue()); } catch (IOException e) { Log.warn("Could not create comment", e); } } } + + private static Map, String> convertFileCommentsToMap(List fileComments) { + Map, List> fileLineToCommentList = new HashMap<>(); + for (FileComment fileComment : fileComments) { + List comments = fileLineToCommentList + .computeIfAbsent(Pair.of(fileComment.fileName(), fileComment.linenumber()), k -> new ArrayList<>()); + comments.add(fileComment.comment()); + } + return fileLineToCommentList.entrySet() + .stream() + .map(e -> Map.entry(e.getKey(), e.getValue().stream().reduce((c1, c2) -> c1).orElse(Comment.EMPTY))) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().markdown())); + } + + private static void runCodeStructuredApi(CodeStructuredApi codeStructuredApi, MetricsAndIssues metricsAndIssues) + throws Exception { + codeStructuredApi.update(metricsAndIssues); + } } diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java deleted file mode 100644 index 7e7337d..0000000 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.qualityannotate.api.coderepository.api; - -import java.io.IOException; -import java.util.List; -import java.util.Optional; - -public interface CodeApi { - Optional getMainComment() throws IOException; - - void createMainComment(String comment) throws IOException; - - List listFileComments() throws IOException; - - void createFileComment(String file, Integer line, String comment) throws IOException; -} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java new file mode 100644 index 0000000..3c3dccd --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java @@ -0,0 +1,14 @@ +package org.qualityannotate.api.coderepository.api; + +import java.io.IOException; + +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + +/** + * Abstraction for the API of Code repositories like Gitlab, github, bitbucket, etc. + * Can be used best for APIs which are structured and know about issues with severity. So github-checks + * api and bitbucket-report api are well suited. + */ +public interface CodeStructuredApi { + void update(MetricsAndIssues metricsAndIssues) throws IOException; +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java new file mode 100644 index 0000000..f5bed86 --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java @@ -0,0 +1,20 @@ +package org.qualityannotate.api.coderepository.api; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +/** + * Abstraction for the API of Code repositories like Gitlab, github, bitbucket, etc. + * Can be used best for APIs which are only text-based. So github-checks api and bitbucket-report api should use a + * different interface. + */ +public interface CodeTextApi { + Optional getMainComment() throws IOException; + + void createMainComment(String comment) throws IOException; + + List listFileComments() throws IOException; + + void createFileComment(String file, Integer line, String comment) throws IOException; +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java similarity index 88% rename from src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java rename to src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java index 13f2fc1..3abcc98 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java @@ -4,7 +4,7 @@ import java.io.IOException; -public interface CodeFileComment { +public interface CodeTextFileComment { void update(String comment) throws IOException; void delete() throws IOException; diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java similarity index 77% rename from src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java rename to src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java index 960945c..c553e5e 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java @@ -2,6 +2,6 @@ import java.io.IOException; -public interface CodeMainComment { +public interface CodeTextMainComment { void update(String comment) throws IOException; } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java index 084d15f..c15240c 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java @@ -1,6 +1,12 @@ package org.qualityannotate.api.qualitytool; +import jakarta.annotation.Nullable; + import java.util.Map; +import java.util.Optional; -public record GlobalMetrics(Map metrics) { +public record GlobalMetrics(Map metrics, String url, @Nullable String statusUrl) { + public Optional getStatusUrl() { + return Optional.ofNullable(statusUrl); + } } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java index 170233b..28a2679 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java @@ -1,6 +1,22 @@ package org.qualityannotate.api.qualitytool; import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; -public record Issue(String fileName, Integer lineNumber, String comment, String severity, @Nullable String urlToIssue) { +public record Issue(String fileName, Integer startLine, @Nullable Integer endLine, @Nullable Integer startColumn, + @Nullable Integer endColumn, String comment, String severity, Severity severityEnum, + @Nullable String urlToIssue) { + + public Integer lineNumber() { + return startLine; + } + + @AllArgsConstructor + @Getter + public enum Severity { + LOW("ℹ\uFE0F"), MEDIUM("⚠\uFE0F"), HIGH("\uD83D\uDED1"); + + private final String unicodeIcon; + } } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java b/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java index 3b922bf..c066f28 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java @@ -3,11 +3,5 @@ public interface QualityTool { MetricsAndIssues getMetricsAndIssues() throws Exception; - String getSeverityReadable(String severity); - - String getSeverityIcon(String severity); - String printConfigWithoutSecrets(); - - String getUrl(); } diff --git a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java index fa581a8..73ec26d 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java +++ b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java @@ -1,29 +1,41 @@ package org.qualityannotate.coderepo.github; +import io.quarkus.logging.Log; import jakarta.enterprise.context.ApplicationScoped; +import lombok.AllArgsConstructor; + +import java.util.List; -import org.qualityannotate.api.coderepository.AbstractCodeRepository; import org.qualityannotate.api.coderepository.CodeRepository; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.coderepo.github.client.GithubApi; +import org.qualityannotate.api.coderepository.CodeStructuredExecutor; +import org.qualityannotate.api.coderepository.CodeTextExecutor; +import org.qualityannotate.api.coderepository.Comment; +import org.qualityannotate.api.coderepository.FileComment; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; +import org.qualityannotate.coderepo.github.client.GithubStructuredApi; +import org.qualityannotate.coderepo.github.client.GithubTextApi; @ApplicationScoped -public class GithubCodeRepository extends AbstractCodeRepository implements CodeRepository { +@AllArgsConstructor +public class GithubCodeRepository implements CodeRepository { private final GithubConfig config; - private final GithubApi githubApi; + private final GithubTextApi githubTextApi; + private final GithubStructuredApi githubStructuredApi; - public GithubCodeRepository(GithubConfig config) { - this.config = config; - this.githubApi = new GithubApi(config.token(), config.project(), config.pullRequest()); + @Override + public void createOrUpdateAnnotations(Comment globalComment, List fileComments, + MetricsAndIssues metricsAndIssues) throws Exception { + if (config.useChecks()) { + Log.info("Using the checks api"); + CodeStructuredExecutor.run(githubStructuredApi, metricsAndIssues); + } else { + Log.info("Using the comment-based api"); + CodeTextExecutor.run(githubTextApi, globalComment, fileComments); + } } @Override public String printConfigWithoutSecrets() { return config.printWithoutSecrets(); } - - @Override - protected CodeApi getCodeApi() { - return githubApi; - } } diff --git a/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java b/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java index 68be59e..2d6ca86 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java +++ b/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java @@ -1,6 +1,7 @@ package org.qualityannotate.coderepo.github; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; @ConfigMapping(prefix = GithubConfig.NAME) @@ -16,10 +17,18 @@ public interface GithubConfig { @WithName("pull_request") Integer pullRequest(); + /** + * when true will use the checks-api, otherwise it will write comments + */ + @WithName("use_checks") + @WithDefault("true") + Boolean useChecks(); + default String printWithoutSecrets() { return String.format(""" project: %s pull_request: %s - """, project(), pullRequest()); + use_checks: %s + """, project(), pullRequest(), useChecks()); } } diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java new file mode 100644 index 0000000..7bb0bdb --- /dev/null +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java @@ -0,0 +1,102 @@ +package org.qualityannotate.coderepo.github.client; + +import jakarta.enterprise.context.ApplicationScoped; +import org.kohsuke.github.GHCheckRun; +import org.kohsuke.github.GHCheckRunBuilder; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.PagedIterator; + +import java.io.IOException; +import java.sql.Date; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.qualitytool.Issue; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; +import org.qualityannotate.coderepo.github.GithubConfig; +import org.qualityannotate.core.CommentProcessor; + +/** + * This is using the Github checks api. + * It seems like it can't be executed locally but only in a github-actions job. + */ +@ApplicationScoped +public class GithubStructuredApi implements CodeStructuredApi { + public static final String NAME = "qualityannotate"; + + private final GitHubBuilder gitHubBuilder; + private final String project; + private final int pullRequestId; + + public GithubStructuredApi(GithubConfig config) { + gitHubBuilder = new GitHubBuilder().withOAuthToken(config.token()); + this.project = config.project(); + this.pullRequestId = config.pullRequest(); + } + + private static Optional findCheckRun(GHRepository repository, GHPullRequest pullRequest) + throws IOException { + PagedIterator iterator = repository.getCheckRuns(pullRequest.getHead().getSha()).iterator(); + while (iterator.hasNext()) { + GHCheckRun next = iterator.next(); + if (next.getName().equals(NAME)) { + return Optional.of(next); + } + } + return Optional.empty(); + } + + @Override + public void update(MetricsAndIssues metricsAndIssues) throws IOException { + GitHub github = gitHubBuilder.build(); + GHRepository repository = github.getRepository(project); + GHPullRequest pullRequest = repository.getPullRequest(pullRequestId); + Optional checkRunOpt = findCheckRun(repository, pullRequest); + if (checkRunOpt.isPresent()) { + GHCheckRun checkRun = checkRunOpt.get(); + GHCheckRunBuilder checkRunBuilder = checkRun.update(); + updateCheckRun(checkRunBuilder, metricsAndIssues); + } else { + GHCheckRunBuilder checkRunBuilder = repository.createCheckRun(NAME, pullRequest.getHead().getSha()); + updateCheckRun(checkRunBuilder, metricsAndIssues); + } + } + + private void updateCheckRun(GHCheckRunBuilder checkRunBuilder, MetricsAndIssues metricsAndIssues) + throws IOException { + checkRunBuilder.withDetailsURL(metricsAndIssues.globalMetrics().url()); + checkRunBuilder.withStatus(GHCheckRun.Status.COMPLETED); + checkRunBuilder.withConclusion(GHCheckRun.Conclusion.NEUTRAL); + checkRunBuilder.withCompletedAt(Date.from(Instant.now())); + + GHCheckRunBuilder.Output output = new GHCheckRunBuilder.Output("Result", + CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics()).markdown()); + List issues = metricsAndIssues.issues(); + for (Issue issue : issues) { + GHCheckRunBuilder.Annotation annotation = new GHCheckRunBuilder.Annotation(issue.fileName(), + issue.startLine(), Optional.ofNullable(issue.endLine()).orElse(issue.startLine()), + mapSeverity(issue.severityEnum()), CommentProcessor.createComment(issue).comment().text()); + Optional.ofNullable(issue.startColumn()).ifPresent(annotation::withStartColumn); + Optional.ofNullable(issue.endColumn()).ifPresent(annotation::withEndColumn); + output.add(annotation); + } + metricsAndIssues.globalMetrics() + .getStatusUrl() + .ifPresent(u -> output.add(new GHCheckRunBuilder.Image("Status", u))); + checkRunBuilder.add(output); + checkRunBuilder.create(); + } + + private GHCheckRun.AnnotationLevel mapSeverity(Issue.Severity severity) { + return switch (severity) { + case LOW -> GHCheckRun.AnnotationLevel.NOTICE; + case MEDIUM -> GHCheckRun.AnnotationLevel.WARNING; + case HIGH -> GHCheckRun.AnnotationLevel.FAILURE; + }; + } +} diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java similarity index 75% rename from src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java rename to src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java index 759087d..c0cce8b 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java @@ -1,25 +1,22 @@ package org.qualityannotate.coderepo.github.client; import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; import org.apache.commons.lang3.tuple.Pair; -import org.kohsuke.github.GHIssueComment; -import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHPullRequestReviewComment; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.PagedIterator; +import org.kohsuke.github.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.api.coderepository.api.CodeFileComment; -import org.qualityannotate.api.coderepository.api.CodeMainComment; +import org.qualityannotate.api.coderepository.api.CodeTextApi; +import org.qualityannotate.api.coderepository.api.CodeTextFileComment; +import org.qualityannotate.api.coderepository.api.CodeTextMainComment; +import org.qualityannotate.coderepo.github.GithubConfig; -public class GithubApi implements CodeApi { +@ApplicationScoped +public class GithubTextApi implements CodeTextApi { private final GitHubBuilder gitHubBuilder; private final String project; private final int pullRequestId; @@ -33,10 +30,10 @@ public class GithubApi implements CodeApi { */ private long userId = 0L; - public GithubApi(String token, String project, int pullRequestId) { - gitHubBuilder = new GitHubBuilder().withOAuthToken(token); - this.project = project; - this.pullRequestId = pullRequestId; + public GithubTextApi(GithubConfig config) { + gitHubBuilder = new GitHubBuilder().withOAuthToken(config.token()); + this.project = config.project(); + this.pullRequestId = config.pullRequest(); } private void init() throws IOException { @@ -57,8 +54,7 @@ private GHPullRequest getPR() throws IOException { } @Override - public Optional getMainComment() throws IOException { - init(); + public Optional getMainComment() throws IOException { GHPullRequest pullRequest = getPR(); PagedIterator commentIterator = pullRequest.listComments().iterator(); while (commentIterator.hasNext()) { @@ -75,21 +71,19 @@ public Optional getMainComment() throws IOException { @Override public void createMainComment(String comment) throws IOException { - init(); GHPullRequest pullRequest = getPR(); System.out.println("Creating new global comment"); pullRequest.comment(comment); } @Override - public List listFileComments() throws IOException { - init(); + public List listFileComments() throws IOException { Log.info("Start creating file comments"); GHPullRequest pullRequest = getPR(); - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (GHPullRequestReviewComment review : pullRequest.listReviewComments()) { if (review.getUser().getId() == userId) { - result.add(new CodeFileComment() { + result.add(new CodeTextFileComment() { @Override public void update(String comment) throws IOException { review.update(comment); @@ -117,7 +111,6 @@ public String getComment() { @Override public void createFileComment(String file, Integer line, String comment) throws IOException { - init(); GHPullRequest pullRequest = getPR(); String commitHash = pullRequest.getHead().getSha(); line = (line == null || line < 1) ? 1 : line; diff --git a/src/main/java/org/qualityannotate/core/CommentProcessor.java b/src/main/java/org/qualityannotate/core/CommentProcessor.java index 7d9911b..2c1bd47 100644 --- a/src/main/java/org/qualityannotate/core/CommentProcessor.java +++ b/src/main/java/org/qualityannotate/core/CommentProcessor.java @@ -7,19 +7,18 @@ import org.qualityannotate.api.coderepository.FileComment; import org.qualityannotate.api.qualitytool.GlobalMetrics; import org.qualityannotate.api.qualitytool.Issue; -import org.qualityannotate.api.qualitytool.QualityTool; public class CommentProcessor { private CommentProcessor() { } - public static Comment createGlobalComment(GlobalMetrics globalMetrics, QualityTool qualityTool) { + public static Comment createGlobalComment(GlobalMetrics globalMetrics) { String markdown = String.format(""" Code Quality Report for [SonarQube](%s) | Name | Value | |------|-------| %s - """, qualityTool.getUrl(), + """, globalMetrics.url(), globalMetrics.metrics() .entrySet() .stream() @@ -28,16 +27,20 @@ public static Comment createGlobalComment(GlobalMetrics globalMetrics, QualityTo return new Comment(markdown, markdown, "TODO-html"); } - public static List createFileComments(List issues, QualityTool qualityTool) { - return issues.stream().map(issue -> createComment(issue, qualityTool)).toList(); + public static List createFileComments(List issues) { + return issues.stream().map(CommentProcessor::createComment).toList(); } - private static FileComment createComment(Issue issue, QualityTool qualityTool) { + public static FileComment createComment(Issue issue) { String markdown = String.format(""" - %s %s%s - """, qualityTool.getSeverityIcon(issue.severity()), issue.comment(), + %s%s%s + """, issue.severityEnum().getUnicodeIcon(), issue.comment(), (issue.urlToIssue() == null ? "" : " [details](" + issue.urlToIssue() + ")")); - Comment comment = new Comment(markdown, markdown, "TODO-html"); + String text = String.format(""" + %s%s%s + """, issue.severityEnum().getUnicodeIcon(), issue.comment(), + (issue.urlToIssue() == null ? "" : " " + issue.urlToIssue())); + Comment comment = new Comment(text, markdown, "TODO-html"); return new FileComment(issue.fileName(), issue.lineNumber(), comment); } } diff --git a/src/main/java/org/qualityannotate/core/MainCommand.java b/src/main/java/org/qualityannotate/core/MainCommand.java index d2abc9e..4101382 100644 --- a/src/main/java/org/qualityannotate/core/MainCommand.java +++ b/src/main/java/org/qualityannotate/core/MainCommand.java @@ -35,17 +35,16 @@ public class MainCommand implements Runnable { @Parameters(paramLabel = "", defaultValue = GithubConfig.NAME, description = "To which code-repository you want to upload the data. Possible Values: ${COMPLETION-CANDIDATES}; Default: ${DEFAULT-VALUE}", completionCandidates = CodeRepoProvider.CodeRepositoryCandidates.class) String codeRepositoryParam; - private static void updateAnnotations(QualityTool qualityTool, CodeRepository codeRepository, - MetricsAndIssues metricsAndIssues) { - Comment globalComment = CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics(), qualityTool); - List fileComments = CommentProcessor.createFileComments(metricsAndIssues.issues(), qualityTool); + private static void updateAnnotations(CodeRepository codeRepository, MetricsAndIssues metricsAndIssues) { + Comment globalComment = CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics()); + List fileComments = CommentProcessor.createFileComments(metricsAndIssues.issues()); Log.infof("Global comment:\n%s", globalComment.text()); Log.infof("Issues:\n%s", fileComments.stream() .map(c -> String.format("%s:%d\n%s", c.fileName(), c.linenumber(), c.comment().text())) .collect(Collectors.joining("\n\n"))); try { - codeRepository.createOrUpdateAnnotations(globalComment, fileComments); + codeRepository.createOrUpdateAnnotations(globalComment, fileComments, metricsAndIssues); } catch (Exception e) { Log.error("Updating the code-repository failed", e); System.exit(1); @@ -72,6 +71,6 @@ public void run() { Log.infof("Code-Repository tool configured with\n%s", codeRepository.printConfigWithoutSecrets()); MetricsAndIssues metricsAndIssues = getMetricsAndIssues(qualityTool); Log.info(metricsAndIssues); - updateAnnotations(qualityTool, codeRepository, metricsAndIssues); + updateAnnotations(codeRepository, metricsAndIssues); } } diff --git a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java index f5f4f1c..7a92669 100644 --- a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java +++ b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java @@ -44,8 +44,9 @@ List getIssues() { List issues = new ArrayList<>(); for (SqIssue sqIssue : issuesSearch.issues()) { issues.add(new Issue(sqIssue.getPath(config.project()), sqIssue.textRange().startLine(), + sqIssue.textRange().endLine(), sqIssue.textRange().startOffset(), sqIssue.textRange().startOffset(), sqIssue.getQualityType().map(q -> q + ": ").orElse("") + sqIssue.message(), sqIssue.getSeverity(), - getIssueUrl(sqIssue.rule()))); + getSeverityEnum(sqIssue.getSeverity()), getIssueUrl(sqIssue.rule()))); } return issues; } @@ -79,7 +80,7 @@ GlobalMetrics getGlobalMetrics() { metrics.put(metric.getName(), value); } } - return new GlobalMetrics(metrics); + return new GlobalMetrics(metrics, getUrl(), getStatusUrl()); } private String getIssueUrl(String rule) { @@ -96,18 +97,12 @@ public MetricsAndIssues getMetricsAndIssues() { return new MetricsAndIssues(getGlobalMetrics(), getIssues()); } - @Override - public String getSeverityReadable(String severity) { - return severity; - } - - @Override - public String getSeverityIcon(String severity) { + public Issue.Severity getSeverityEnum(String severity) { return switch (severity.toLowerCase(Locale.ENGLISH)) { - case "low" -> "ℹ\uFE0F"; - case "medium" -> "⚠\uFE0F"; - case "high" -> "\uD83D\uDED1"; - default -> severity; + case "low" -> Issue.Severity.LOW; + case "medium" -> Issue.Severity.MEDIUM; + // case "high" -> Issue.Severity.HIGH; + default -> Issue.Severity.HIGH; }; } @@ -117,13 +112,21 @@ public String printConfigWithoutSecrets() { } /** - * E.g. https://sonarcloud.io/summary/new_code?id=quyt_qualityannotate&pullRequest=1 + * E.g. example */ - @Override public String getUrl() { return getBaseUrl() + "/summary/new_code?id=" + config.project() + "&pullRequest=" + config.pullRequest(); } + /** + * E.g. + * example + */ + public String getStatusUrl() { + return getBaseUrl() + "/api/project_badges/measure?project=" + config.project() + "&pullRequest=" + + config.pullRequest() + "&metric=alert_status"; + } + private String getBaseUrl() { return config.url().trim().replaceAll("/$", ""); } diff --git a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java index 5e6c03c..ce4abac 100644 --- a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java +++ b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java @@ -56,8 +56,10 @@ void testGetGlobalMetrics() { .withQueryParam("metricKeys", equalTo("test-metrics-1,test-metrics-2")) .withQueryParam("additionalFields", equalTo("metrics")) .willReturn(okJson(content))); - assertEquals(new GlobalMetrics(Map.of("New issues", "25", "Lines of code", "114", "Complexity", "12")), - cut.getGlobalMetrics()); + GlobalMetrics globalMetrics = cut.getGlobalMetrics(); + assertEquals(Map.of("Complexity", "12", "New issues", "25", "Lines of code", "114"), globalMetrics.metrics()); + assertEquals("http://localhost:42345/summary/new_code?id=Test_Project&pullRequest=Test_Pr".replaceAll(":[0-9]+", + ":123"), globalMetrics.url().replaceAll(":[0-9]+", ":123")); } @Test @@ -76,9 +78,9 @@ void testGetIssues() { .withQueryParam("statuses", equalTo("OPEN,REOPENED,CONFIRMED")) .willReturn(okJson(content))); assertEquals( - List.of(new Issue("om.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", 2, - "Remove this unused private \"getKee\" method.", "MAJOR", - "https://sonarcloud" + ".io/organizations/quyt/rules?open=java:S1144&rule_key=java:S1144")), + List.of(new Issue("om.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", 2, 2, + 0, 0, "Remove this unused private \"getKee\" method.", "MAJOR", Issue.Severity.HIGH, + "https://sonarcloud.io/organizations/quyt/rules?open=java:S1144&rule_key=java:S1144")), cut.getIssues()); }