diff --git a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java index ef7b11b72..5669b1a30 100644 --- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java +++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java @@ -18,13 +18,10 @@ class ArchiveMessages { private static final Pattern commentPattern = Pattern.compile("", Pattern.DOTALL | Pattern.MULTILINE); - private static final Pattern cutoffPattern = Pattern.compile("(.*?)", - Pattern.DOTALL | Pattern.MULTILINE); + private static String filterComments(String body) { - var cutoffMatcher = cutoffPattern.matcher(body); - if (cutoffMatcher.find()) { - body = cutoffMatcher.group(1); - } + var parsedBody = PullRequestBody.parse(body); + body = parsedBody.bodyText(); var commentMatcher = commentPattern.matcher(body); body = commentMatcher.replaceAll(""); diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CSRCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CSRCommand.java index a41c5d87d..4e34f05f5 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CSRCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CSRCommand.java @@ -57,7 +57,7 @@ private static void linkReply(PullRequest pr, Issue issue, PrintWriter writer) { @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List allComments, PrintWriter reply) { - if (!ProjectPermissions.mayReview(censusInstance, comment.author())) { + if (!censusInstance.isReviewer(comment.author())) { reply.println("only [Reviewers](https://openjdk.java.net/bylaws#reviewer) are allowed require a CSR."); return; } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java index 2f30974de..cc8fdbfcc 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java @@ -24,8 +24,9 @@ import org.openjdk.skara.census.*; import org.openjdk.skara.forge.*; +import org.openjdk.skara.host.HostUser; import org.openjdk.skara.jcheck.JCheckConfiguration; -import org.openjdk.skara.vcs.*; +import org.openjdk.skara.vcs.Repository; import java.io.*; import java.net.URLEncoder; @@ -119,4 +120,36 @@ Project project() { Namespace namespace() { return namespace; } + + Optional contributor(HostUser hostUser) { + var contributor = namespace.get(hostUser.id()); + return Optional.ofNullable(contributor); + } + + boolean isAuthor(HostUser hostUser) { + int version = census.version().format(); + var contributor = namespace.get(hostUser.id()); + if (contributor == null) { + return false; + } + return project.isAuthor(contributor.username(), version); + } + + boolean isCommitter(HostUser hostUser) { + int version = census.version().format(); + var contributor = namespace.get(hostUser.id()); + if (contributor == null) { + return false; + } + return project.isCommitter(contributor.username(), version); + } + + boolean isReviewer(HostUser hostUser) { + int version = census.version().format(); + var contributor = namespace.get(hostUser.id()); + if (contributor == null) { + return false; + } + return project.isReviewer(contributor.username(), version); + } } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java index ebd164314..15741abd3 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java @@ -586,7 +586,7 @@ private String getMergeReadyComment(String commitMessage, List reviews) message.append("`.\n"); } - if (!ProjectPermissions.mayCommit(censusInstance, pr.author())) { + if (!censusInstance.isCommitter(pr.author())) { message.append("\n"); var contributor = censusInstance.namespace().get(pr.author().id()); if (contributor == null) { @@ -598,7 +598,7 @@ private String getMergeReadyComment(String commitMessage, List reviews) message.append("an existing [Committer](https://openjdk.java.net/bylaws#committer) must agree to "); message.append("[sponsor](https://openjdk.java.net/sponsor/) your change. "); var candidates = reviews.stream() - .filter(review -> ProjectPermissions.mayCommit(censusInstance, review.reviewer())) + .filter(review -> censusInstance.isCommitter(review.reviewer())) .map(review -> "@" + review.reviewer().userName()) .collect(Collectors.joining(", ")); if (candidates.length() > 0) { diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java index dc10ac255..e35043972 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java @@ -60,7 +60,7 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst // If the command author is allowed to sponsor this change, suggest that command var readyHash = ReadyForSponsorTracker.latestReadyForSponsor(pr.repository().forge().currentUser(), allComments); if (readyHash.isPresent()) { - if (ProjectPermissions.mayCommit(censusInstance, comment.author())) { + if (censusInstance.isCommitter(comment.author())) { reply.print(" As this PR is ready to be sponsored, and you are an eligible sponsor, did you mean to issue the `/sponsor` command?"); return; } @@ -126,7 +126,7 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst } // Finally check if the author is allowed to perform the actual push - if (!ProjectPermissions.mayCommit(censusInstance, pr.author())) { + if (!censusInstance.isCommitter(pr.author())) { reply.println(ReadyForSponsorTracker.addIntegrationMarker(pr.headHash())); reply.println("Your change (at version " + pr.headHash() + ") is now ready to be sponsored by a Committer."); if (!args.isBlank()) { diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IssueCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IssueCommand.java index 37de18615..31f62a005 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IssueCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IssueCommand.java @@ -22,8 +22,10 @@ */ package org.openjdk.skara.bots.pr; -import org.openjdk.skara.forge.PullRequest; +import org.openjdk.skara.forge.*; +import org.openjdk.skara.host.HostUser; import org.openjdk.skara.issuetracker.Comment; +import org.openjdk.skara.json.*; import org.openjdk.skara.vcs.openjdk.Issue; import java.io.PrintWriter; @@ -54,11 +56,18 @@ public class IssueCommand implements CommandHandler { private final String name; private void showHelp(PrintWriter reply) { - reply.println("Command syntax: `/" + name + " [add|remove] [,,...]` or `/issue [add] : `. For example:"); + reply.println("Command syntax:"); + reply.println(" * `/" + name + " [add|remove] [,,...]`"); + reply.println(" * `/" + name + " [add] : `"); + reply.println(" * `/" + name + " create [PX] /[subcomponent]"); + reply.println(); + reply.println("Some examples:"); reply.println(); reply.println(" * `/" + name + " add JDK-1234567,4567890`"); reply.println(" * `/" + name + " remove JDK-4567890`"); reply.println(" * `/" + name + " 1234567: Use this exact title`"); + reply.println(" * `/" + name + " create hotspot/jfr"); + reply.println(" * `/" + name + " create P4 core-libs/java.nio"); reply.println(); reply.print("If issues are specified only by their ID, the title will be automatically retrieved from JBS. "); reply.print("The project prefix (`JDK-` in the above examples) is optional. "); @@ -66,7 +75,7 @@ private void showHelp(PrintWriter reply) { } private static final Pattern shortIssuePattern = Pattern.compile("((?:[A-Za-z]+-)?[0-9]+)(?:,| |$)"); - private final static Pattern subCommandPattern = Pattern.compile("^(add|remove|delete|(?:[A-Za-z]+-)?[0-9]+:?)[ ,]?.*$"); + private final static Pattern subCommandPattern = Pattern.compile("^(add|remove|delete|create|(?:[A-Za-z]+-)?[0-9]+:?)[ ,]?.*$"); private List parseIssueList(String allowedPrefix, String issueList) throws InvalidIssue { List ret; @@ -98,6 +107,170 @@ private List parseIssueList(String allowedPrefix, String issueList) throw this("issue"); } + private void addIssue(PullRequestBot bot, PullRequest pr, String args, Set currentSolved, PrintWriter reply) throws InvalidIssue { + if (args.startsWith("add")) { + var issueListStart = args.indexOf(' '); + if (issueListStart == -1) { + showHelp(reply); + return; + } + args = args.substring(issueListStart); + } + var issues = parseIssueList(bot.issueProject() == null ? "" : bot.issueProject().name(), args); + if (issues.size() == 0) { + showHelp(reply); + return; + } + var validatedIssues = new ArrayList(); + for (var issue : issues) { + try { + if (bot.issueProject() == null) { + if (issue.description() == null) { + reply.print("This repository does not have an issue project configured - you will need to input the issue title manually "); + reply.println("using the syntax `/" + name + " " + issue.shortId() + ": title of the issue`."); + return; + } else { + validatedIssues.add(issue); + continue; + } + } + var validatedIssue = bot.issueProject().issue(issue.shortId()); + if (validatedIssue.isEmpty()) { + reply.println("The issue `" + issue.shortId() + "` was not found in the `" + bot.issueProject().name() + "` project - make sure you have entered it correctly."); + continue; + } + if (validatedIssue.get().state() != org.openjdk.skara.issuetracker.Issue.State.OPEN) { + reply.println("The issue [" + validatedIssue.get().id() + "](" + validatedIssue.get().webUrl() + ") isn't open - make sure you have selected the correct issue."); + continue; + } + if (issue.description() == null) { + validatedIssues.add(new Issue(validatedIssue.get().id(), validatedIssue.get().title())); + } else { + validatedIssues.add(new Issue(validatedIssue.get().id(), issue.description())); + } + + } catch (RuntimeException e) { + if (issue.description() == null) { + reply.print("Temporary failure when trying to look up issue `" + issue.shortId() + "` - you will need to input the issue title manually "); + reply.println("using the syntax `/" + name + " " + issue.shortId() + ": title of the issue`."); + return; + } else { + validatedIssues.add(issue); + } + } + } + if (validatedIssues.size() != issues.size()) { + reply.println("As there were validation problems, no additional issues will be added to the list of solved issues."); + return; + } + + var titleIssue = Issue.fromStringRelaxed(pr.title()); + for (var issue : validatedIssues) { + if (titleIssue.isEmpty()) { + reply.print("The primary solved issue for a PR is set through the PR title. Since the current title does "); + reply.println("not contain an issue reference, it will now be updated."); + pr.setTitle(issue.toShortString()); + titleIssue = Optional.of(issue); + continue; + } + if (titleIssue.get().shortId().equals(issue.shortId())) { + reply.println("This issue is referenced in the PR title - it will now be updated."); + pr.setTitle(issue.toShortString()); + continue; + } + reply.println(SolvesTracker.setSolvesMarker(issue)); + if (currentSolved.contains(issue.shortId())) { + reply.println("Updating description of additional solved issue: `" + issue.toShortString() + "`."); + } else { + reply.println("Adding additional issue to " + name + " list: `" + issue.toShortString() + "`."); + } + } + } + + private void removeIssue(PullRequestBot bot, String args, Set currentSolved, PrintWriter reply) throws InvalidIssue { + var issueListStart = args.indexOf(' '); + if (issueListStart == -1) { + showHelp(reply); + return; + } + if (currentSolved.isEmpty()) { + reply.println("This PR does not contain any additional solved issues that can be removed. To remove the primary solved issue, simply edit the title of this PR."); + return; + } + var issuesToRemove = parseIssueList(bot.issueProject() == null ? "" : bot.issueProject().name(), args.substring(issueListStart)); + for (var issue : issuesToRemove) { + if (currentSolved.contains(issue.shortId())) { + reply.println(SolvesTracker.removeSolvesMarker(issue)); + reply.println("Removing additional issue from " + name + " list: `" + issue.shortId() + "`."); + } else { + reply.print("The issue `" + issue.shortId() + "` was not found in the list of additional solved issues. The list currently contains these issues: "); + var currentList = currentSolved.stream() + .map(id -> "`" + id + "`") + .collect(Collectors.joining(", ")); + reply.println(currentList); + } + } + } + + private void createIssue(PullRequestBot bot, PullRequest pr, String args, CensusInstance censusInstance, HostUser author, PrintWriter reply) { + if (!censusInstance.isAuthor(author)) { + reply.println("Only [Authors](https://openjdk.java.net/bylaws#author) are allowed to create issues."); + return; + } + + var currentTitleIssue = Issue.fromString(pr.title()); + if (currentTitleIssue.isPresent()) { + reply.println("The PR title already references an issue (`" + currentTitleIssue.get().shortId() + "`) - will not create a new one."); + return; + } + + var argSplit = new LinkedList<>(Arrays.asList(args.split("(?:\\s+|/)"))); + argSplit.pollFirst(); + + String priority = null; + String subComponent = null; + + // First argument can be a priority + var next = argSplit.pollFirst(); + if (next != null && next.matches("^[pP]\\d$")) { + priority = next.substring(1); + next = argSplit.pollFirst(); + } + + // Next argument is the mandatory component name + if (next == null) { + showHelp(reply); + return; + } + var component = next; + next = argSplit.pollFirst(); + + // Finally there can be a subcomponent present + if (next != null) { + subComponent = next; + } + + var properties = new HashMap(); + properties.put("components", JSON.array().add(JSON.of(component))); + if (subComponent != null) { + properties.put("customfield_10008", JSON.of(subComponent)); + } + if (priority != null) { + properties.put("priority", JSON.of(priority)); + } + properties.put("issuetype", JSON.of("enhancement")); + + var bodyText = PullRequestBody.parse(pr).bodyText(); + try { + var issue = bot.issueProject().createIssue(pr.title(), bodyText.lines().collect(Collectors.toList()), properties); + reply.println("The issue `" + issue.id() + "` was successfully created - the title of this PR will be updated to reference it. "); + var shortId = issue.id().contains("-") ? issue.id().split("-", 2)[1] : issue.id(); + pr.setTitle(shortId + ": " + issue.title()); + } catch (RuntimeException e) { + reply.println("An error occurred when attempting to create an issue: " + e.getMessage()); + } + } + @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List allComments, PrintWriter reply) { if (!comment.author().equals(pr.author())) { @@ -120,108 +293,12 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst .collect(Collectors.toSet()); try { if (args.startsWith("remove") || args.startsWith("delete")) { - var issueListStart = args.indexOf(' '); - if (issueListStart == -1) { - showHelp(reply); - return; - } - if (currentSolved.isEmpty()) { - reply.println("This PR does not contain any additional solved issues that can be removed. To remove the primary solved issue, simply edit the title of this PR."); - return; - } - var issuesToRemove = parseIssueList(bot.issueProject() == null ? "" : bot.issueProject().name(), args.substring(issueListStart)); - for (var issue : issuesToRemove) { - if (currentSolved.contains(issue.shortId())) { - reply.println(SolvesTracker.removeSolvesMarker(issue)); - reply.println("Removing additional issue from " + name + " list: `" + issue.shortId() + "`."); - } else { - reply.print("The issue `" + issue.shortId() + "` was not found in the list of additional solved issues. The list currently contains these issues: "); - var currentList = currentSolved.stream() - .map(id -> "`" + id + "`") - .collect(Collectors.joining(", ")); - reply.println(currentList); - } - } + removeIssue(bot, args, currentSolved, reply); + } else if (args.startsWith("create")) { + createIssue(bot, pr, args, censusInstance, comment.author(), reply); } else { - if (args.startsWith("add")) { - var issueListStart = args.indexOf(' '); - if (issueListStart == -1) { - showHelp(reply); - return; - } - args = args.substring(issueListStart); - } - var issues = parseIssueList(bot.issueProject() == null ? "" : bot.issueProject().name(), args); - if (issues.size() == 0) { - showHelp(reply); - return; - } - var validatedIssues = new ArrayList(); - for (var issue : issues) { - try { - if (bot.issueProject() == null) { - if (issue.description() == null) { - reply.print("This repository does not have an issue project configured - you will need to input the issue title manually "); - reply.println("using the syntax `/" + name + " " + issue.shortId() + ": title of the issue`."); - return; - } else { - validatedIssues.add(issue); - continue; - } - } - var validatedIssue = bot.issueProject().issue(issue.shortId()); - if (validatedIssue.isEmpty()) { - reply.println("The issue `" + issue.shortId() + "` was not found in the `" + bot.issueProject().name() + "` project - make sure you have entered it correctly."); - continue; - } - if (validatedIssue.get().state() != org.openjdk.skara.issuetracker.Issue.State.OPEN) { - reply.println("The issue [" + validatedIssue.get().id() + "](" + validatedIssue.get().webUrl() + ") isn't open - make sure you have selected the correct issue."); - continue; - } - if (issue.description() == null) { - validatedIssues.add(new Issue(validatedIssue.get().id(), validatedIssue.get().title())); - } else { - validatedIssues.add(new Issue(validatedIssue.get().id(), issue.description())); - } - - } catch (RuntimeException e) { - if (issue.description() == null) { - reply.print("Temporary failure when trying to look up issue `" + issue.shortId() + "` - you will need to input the issue title manually "); - reply.println("using the syntax `/" + name + " " + issue.shortId() + ": title of the issue`."); - return; - } else { - validatedIssues.add(issue); - } - } - } - if (validatedIssues.size() != issues.size()) { - reply.println("As there were validation problems, no additional issues will be added to the list of solved issues."); - return; - } - - var titleIssue = Issue.fromStringRelaxed(pr.title()); - for (var issue : validatedIssues) { - if (titleIssue.isEmpty()) { - reply.print("The primary solved issue for a PR is set through the PR title. Since the current title does "); - reply.println("not contain an issue reference, it will now be updated."); - pr.setTitle(issue.toShortString()); - titleIssue = Optional.of(issue); - continue; - } - if (titleIssue.get().shortId().equals(issue.shortId())) { - reply.println("This issue is referenced in the PR title - it will now be updated."); - pr.setTitle(issue.toShortString()); - continue; - } - reply.println(SolvesTracker.setSolvesMarker(issue)); - if (currentSolved.contains(issue.shortId())) { - reply.println("Updating description of additional solved issue: `" + issue.toShortString() + "`."); - } else { - reply.println("Adding additional issue to " + name + " list: `" + issue.toShortString() + "`."); - } - } + addIssue(bot, pr, args, currentSolved, reply); } - } catch (InvalidIssue invalidIssue) { reply.println("The issue identifier `" + invalidIssue.identifier() + "` is invalid: " + invalidIssue.reason() + "."); } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelCommand.java index 415697357..12c800626 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelCommand.java @@ -60,7 +60,7 @@ private Set automaticLabels(PullRequestBot bot, PullRequest pr, Path scr @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List allComments, PrintWriter reply) { - if (!comment.author().equals(pr.author()) && (!ProjectPermissions.mayCommit(censusInstance, comment.author()))) { + if (!comment.author().equals(pr.author()) && (!censusInstance.isCommitter(comment.author()))) { reply.println("Only the PR author and project [Committers](https://openjdk.java.net/bylaws#committer) are allowed to modify labels on a PR."); return; } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java deleted file mode 100644 index 079105015..000000000 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package org.openjdk.skara.bots.pr; - -import org.openjdk.skara.host.HostUser; - -class ProjectPermissions { - static boolean mayCommit(CensusInstance censusInstance, HostUser user) { - var census = censusInstance.census(); - var project = censusInstance.project(); - var namespace = censusInstance.namespace(); - int version = census.version().format(); - - var contributor = namespace.get(user.id()); - if (contributor == null) { - return false; - } - return project.isCommitter(contributor.username(), version) || - project.isReviewer(contributor.username(), version) || - project.isLead(contributor.username(), version); - } - - static boolean mayReview(CensusInstance censusInstance, HostUser user) { - var census = censusInstance.census(); - var project = censusInstance.project(); - var namespace = censusInstance.namespace(); - int version = census.version().format(); - - var contributor = namespace.get(user.id()); - if (contributor == null) { - return false; - } - return project.isReviewer(contributor.username(), version) || - project.isLead(contributor.username(), version); - } -} diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java index 7ea69e047..5d4183eec 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java @@ -36,7 +36,7 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst reply.println("You can't reject your own changes."); return; } - if (!ProjectPermissions.mayReview(censusInstance, comment.author())) { + if (!censusInstance.isReviewer(comment.author())) { reply.println("Only [Reviewers](https://openjdk.java.net/bylaws#reviewer) are allowed to reject changes."); return; } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReviewersCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReviewersCommand.java index e1aca18dd..eb8d5d81a 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReviewersCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReviewersCommand.java @@ -49,7 +49,7 @@ private void showHelp(PrintWriter reply) { @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List allComments, PrintWriter reply) { - if (!ProjectPermissions.mayReview(censusInstance, comment.author())) { + if (!censusInstance.isReviewer(comment.author())) { reply.println("Only [Reviewers](https://openjdk.java.net/bylaws#reviewer) are allowed to change the number of required reviewers."); return; } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java index e472a697b..330204708 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java @@ -36,11 +36,11 @@ public class SponsorCommand implements CommandHandler { @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List allComments, PrintWriter reply) { - if (ProjectPermissions.mayCommit(censusInstance, pr.author())) { + if (censusInstance.isCommitter(pr.author())) { reply.println("This change does not need sponsoring - the author is allowed to integrate it."); return; } - if (!ProjectPermissions.mayCommit(censusInstance, comment.author())) { + if (!censusInstance.isReviewer(comment.author())) { reply.println("Only [Committers](https://openjdk.java.net/bylaws#committer) are allowed to sponsor changes."); return; } diff --git a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IssueTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IssueTests.java index de16e236e..e1d373533 100644 --- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IssueTests.java +++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IssueTests.java @@ -22,15 +22,15 @@ */ package org.openjdk.skara.bots.pr; -import org.openjdk.skara.forge.Review; -import org.openjdk.skara.issuetracker.Comment; +import org.junit.jupiter.api.*; +import org.openjdk.skara.forge.*; +import org.openjdk.skara.issuetracker.*; import org.openjdk.skara.test.*; import org.openjdk.skara.vcs.Repository; -import org.junit.jupiter.api.*; - import java.io.IOException; import java.util.*; +import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; import static org.openjdk.skara.bots.pr.PullRequestAsserts.assertLastCommentContains; @@ -65,14 +65,16 @@ void simple(TestInfo testInfo) throws IOException { TestBotRunner.runPeriodicItems(prBot); // The bot should reply with a help message - assertLastCommentContains(pr,"Command syntax: `/issue"); + assertLastCommentContains(pr,"Command syntax:"); + assertLastCommentContains(pr, "`/issue"); // Check that the alias works as well pr.addComment("/solves"); TestBotRunner.runPeriodicItems(prBot); // The bot should reply with a help message - assertLastCommentContains(pr,"Command syntax: `/solves"); + assertLastCommentContains(pr,"Command syntax:"); + assertLastCommentContains(pr, "`/solves"); // Invalid syntax pr.addComment("/issue something I guess"); @@ -383,6 +385,119 @@ void issueInBody(TestInfo testInfo) throws IOException { } } + private static final Pattern addedIssuePattern = Pattern.compile("`(.*)` was successfully created", Pattern.MULTILINE); + + private static Issue issueFromLastComment(PullRequest pr, IssueProject issueProject) { + var comments = pr.comments(); + var lastComment = comments.get(comments.size() - 1); + var addedIssueMatcher = addedIssuePattern.matcher(lastComment.body()); + assertTrue(addedIssueMatcher.find(), lastComment.body()); + return issueProject.issue(addedIssueMatcher.group(1)).orElseThrow(); + } + + @Test + void createIssue(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()); + var prBot = PullRequestBot.newBuilder().repo(integrator).censusRepo(censusBuilder.build()).issueProject(issues).build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + assertFalse(CheckableRepository.hasBeenEdited(localRepo)); + localRepo.push(masterHash, author.url(), "master", true); + + // Make a change with a corresponding PR + var editHash = CheckableRepository.appendAndCommit(localRepo); + localRepo.push(editHash, author.url(), "edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request"); + pr.setBody("This is the body"); + + // Create an issue + pr.addComment("/issue create hotspot"); + TestBotRunner.runPeriodicItems(prBot); + + // Verify it + var issue = issueFromLastComment(pr, issues); + assertEquals("This is a pull request", issue.title()); + assertEquals("hotspot", issue.properties().get("components").asArray().get(0).asString()); + assertEquals("This is the body", issue.body()); + + var updatedPr = author.pullRequest(pr.id()); + var issueNr = issue.id().split("-", 2)[1]; + assertEquals(issueNr + ": This is a pull request", updatedPr.title()); + } + } + + @Test + void createIssueParameterized(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()); + var prBot = PullRequestBot.newBuilder().repo(integrator).censusRepo(censusBuilder.build()).issueProject(issues).build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + assertFalse(CheckableRepository.hasBeenEdited(localRepo)); + localRepo.push(masterHash, author.url(), "master", true); + + // Make a change with a corresponding PR + var editHash = CheckableRepository.appendAndCommit(localRepo); + localRepo.push(editHash, author.url(), "edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request"); + + // Create an issue + pr.addComment("/issue create P2 hotspot"); + TestBotRunner.runPeriodicItems(prBot); + + // Verify it + var issue = issueFromLastComment(pr, issues); + assertEquals("This is a pull request", issue.title()); + assertEquals("hotspot", issue.properties().get("components").asArray().get(0).asString()); + assertEquals("2", issue.properties().get("priority").asString()); + + // Reset and try some more + pr.setTitle("This is another pull request"); + + // Create an issue + pr.addComment("/issue create P4 hotspot"); + TestBotRunner.runPeriodicItems(prBot); + + // Verify it + issue = issueFromLastComment(pr, issues); + assertEquals("This is another pull request", issue.title()); + assertEquals("hotspot", issue.properties().get("components").asArray().get(0).asString()); + assertEquals("4", issue.properties().get("priority").asString()); + assertEquals("enhancement", issue.properties().get("issuetype").asString().toLowerCase()); + + // Reset and try some more + pr.setTitle("This is yet another pull request"); + + // Create an issue + pr.addComment("/issue create core-libs/java.io"); + TestBotRunner.runPeriodicItems(prBot); + + // Verify it + issue = issueFromLastComment(pr, issues); + assertEquals("This is yet another pull request", issue.title()); + assertEquals("core-libs", issue.properties().get("components").asArray().get(0).asString()); + assertEquals("enhancement", issue.properties().get("issuetype").asString().toLowerCase()); + assertEquals("java.io", issue.properties().get("customfield_10008").asString()); + } + } + @Test void projectPrefix(TestInfo testInfo) throws IOException { try (var credentials = new HostCredentials(testInfo); diff --git a/forge/src/main/java/org/openjdk/skara/forge/PullRequestBody.java b/forge/src/main/java/org/openjdk/skara/forge/PullRequestBody.java index 22c79c537..93eb3aa67 100644 --- a/forge/src/main/java/org/openjdk/skara/forge/PullRequestBody.java +++ b/forge/src/main/java/org/openjdk/skara/forge/PullRequestBody.java @@ -23,19 +23,26 @@ package org.openjdk.skara.forge; import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.regex.Pattern; public class PullRequestBody { + private final String bodyText; private final List issues; private final List contributors; - private PullRequestBody(List issues, List contributors) { + private static final Pattern cutoffPattern = Pattern.compile("^$"); + + private PullRequestBody(String bodyText, List issues, List contributors) { + this.bodyText = bodyText; this.issues = issues; this.contributors = contributors; } + public String bodyText() { + return bodyText; + } + public List issues() { return issues; } @@ -48,9 +55,15 @@ public static PullRequestBody parse(PullRequest pr) { return parse(Arrays.asList(pr.body().split("\n"))); } + public static PullRequestBody parse(String body) { + return parse(Arrays.asList(body.split("\n"))); + } + public static PullRequestBody parse(List lines) { var issues = new ArrayList(); var contributors = new ArrayList(); + var bodyText = new StringBuilder(); + var inBotComment = false; var i = 0; while (i < lines.size()) { @@ -85,8 +98,14 @@ public static PullRequestBody parse(List lines) { } else { i++; } + if (line.startsWith("