diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandExtractor.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandExtractor.java new file mode 100644 index 000000000..90bcb31bb --- /dev/null +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandExtractor.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, 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; + +import java.util.*; +import java.util.regex.Pattern; + +public class CommandExtractor { + private static final Pattern commandPattern = Pattern.compile("^\\s*/([A-Za-z]+)(?:\\s+(.*))?"); + + private static String formatId(String baseId, int subId) { + if (subId > 0) { + return String.format("%s:%d", baseId, subId); + } else { + return baseId; + } + } + + static List extractCommands(Map commandHandlers, String text, String baseId, HostUser user) { + var ret = new ArrayList(); + CommandHandler multiLineHandler = null; + List multiLineBuffer = null; + String multiLineCommand = null; + int subId = 0; + for (var line : text.split("\\R")) { + var commandMatcher = commandPattern.matcher(line); + if (commandMatcher.matches()) { + if (multiLineHandler != null) { + ret.add(new CommandInvocation(formatId(baseId, subId++), user, multiLineHandler, multiLineCommand, String.join("\n", multiLineBuffer))); + multiLineHandler = null; + } + var command = commandMatcher.group(1).toLowerCase(); + var handler = commandHandlers.get(command); + if (handler != null && handler.multiLine()) { + multiLineHandler = handler; + multiLineBuffer = new ArrayList<>(); + if (commandMatcher.group(2) != null) { + multiLineBuffer.add(commandMatcher.group(2)); + } + multiLineCommand = command; + } else { + ret.add(new CommandInvocation(formatId(baseId, subId++), user, handler, command, commandMatcher.group(2))); + } + } else { + if (multiLineHandler != null) { + multiLineBuffer.add(line); + } + } + } + if (multiLineHandler != null) { + ret.add(new CommandInvocation(formatId(baseId, subId), user, multiLineHandler, multiLineCommand, String.join("\n", multiLineBuffer))); + } + return ret; + } + +} diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandHandler.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandHandler.java index 7eeb94d45..02b37f78e 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandHandler.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandHandler.java @@ -24,19 +24,31 @@ import org.openjdk.skara.forge.PullRequest; import org.openjdk.skara.issuetracker.Comment; +import org.openjdk.skara.vcs.*; import java.io.PrintWriter; import java.nio.file.Path; import java.util.List; interface CommandHandler { - void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply); String description(); + default void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) + { + } + default void handleCommit(PullRequestBot bot, Hash hash, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) { + } + default boolean multiLine() { return false; } default boolean allowedInBody() { return false; } + default boolean allowedInCommit() { + return false; + } + default boolean allowedInPullRequest() { + return true; + } } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java index 74f69a269..faa217d90 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java @@ -24,8 +24,8 @@ import org.openjdk.skara.bot.WorkItem; import org.openjdk.skara.forge.*; -import org.openjdk.skara.host.HostUser; -import org.openjdk.skara.issuetracker.Comment; +import org.openjdk.skara.issuetracker.*; +import org.openjdk.skara.vcs.Hash; import java.io.*; import java.nio.file.Path; @@ -38,10 +38,10 @@ public class CommandWorkItem extends PullRequestWorkItem { private static final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr"); - private static final Pattern commandPattern = Pattern.compile("^\\s*/([A-Za-z]+)(?:\\s+(.*))?"); private static final String commandReplyMarker = ""; private static final Pattern commandReplyPattern = Pattern.compile(""); private static final String selfCommandMarker = ""; + private final static Pattern pushedPattern = Pattern.compile("Pushed as commit ([a-f0-9]{40})\\."); private static final Map commandHandlers = Map.ofEntries( Map.entry("help", new HelpCommand()), @@ -64,48 +64,40 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst reply.println("Available commands:"); Stream.concat( commandHandlers.entrySet().stream() + .filter(entry -> entry.getValue().allowedInPullRequest()) .map(entry -> entry.getKey() + " - " + entry.getValue().description()), bot.externalCommands().entrySet().stream() .map(entry -> entry.getKey() + " - " + entry.getValue()) ).sorted().forEachOrdered(c -> reply.println(" * " + c)); } + @Override + public void handleCommit(PullRequestBot bot, Hash hash, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) { + reply.println("Available commands:"); + Stream.concat( + commandHandlers.entrySet().stream() + .filter(entry -> entry.getValue().allowedInCommit()) + .map(entry -> entry.getKey() + " - " + entry.getValue().description()), + bot.externalCommands().entrySet().stream() + .map(entry -> entry.getKey() + " - " + entry.getValue()) + ).sorted().forEachOrdered(c -> reply.println(" * " + c)); + } + @Override public String description() { return "shows this text"; } + + @Override + public boolean allowedInCommit() { + return true; + } } CommandWorkItem(PullRequestBot bot, PullRequest pr, Consumer errorHandler) { super(bot, pr, errorHandler); } - private List> findCommandComments(List comments) { - var self = pr.repository().forge().currentUser(); - var handled = comments.stream() - .filter(comment -> comment.author().equals(self)) - .map(comment -> commandReplyPattern.matcher(comment.body())) - .filter(Matcher::find) - .map(matcher -> matcher.group(1)) - .collect(Collectors.toSet()); - - return comments.stream() - .filter(comment -> !comment.author().equals(self) || comment.body().endsWith(selfCommandMarker)) - .map(comment -> new AbstractMap.SimpleEntry<>(comment, commandPattern.matcher(comment.body()))) - .filter(entry -> entry.getValue().find()) - .filter(entry -> !handled.contains(entry.getKey().id())) - .map(entry -> new AbstractMap.SimpleEntry<>(entry.getValue().group(1), entry.getKey())) - .collect(Collectors.toList()); - } - - private String formatId(String baseId, int subId) { - if (subId > 0) { - return String.format("%s:%d", baseId, subId); - } else { - return baseId; - } - } - private static class InvalidBodyCommandHandler implements CommandHandler { @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) { @@ -118,53 +110,13 @@ public String description() { } } - private List extractCommands(String text, String baseId, HostUser user) { - var ret = new ArrayList(); - CommandHandler multiLineHandler = null; - List multiLineBuffer = null; - String multiLineCommand = null; - int subId = 0; - for (var line : text.split("\\R")) { - var commandMatcher = commandPattern.matcher(line); - if (commandMatcher.matches()) { - if (multiLineHandler != null) { - ret.add(new CommandInvocation(formatId(baseId, subId++), user, multiLineHandler, multiLineCommand, String.join("\n", multiLineBuffer))); - multiLineHandler = null; - } - var command = commandMatcher.group(1).toLowerCase(); - var handler = commandHandlers.get(command); - if (handler != null && baseId.equals("body") && !handler.allowedInBody()) { - handler = new InvalidBodyCommandHandler(); - } - if (handler != null && handler.multiLine()) { - multiLineHandler = handler; - multiLineBuffer = new ArrayList<>(); - if (commandMatcher.group(2) != null) { - multiLineBuffer.add(commandMatcher.group(2)); - } - multiLineCommand = command; - } else { - ret.add(new CommandInvocation(formatId(baseId, subId++), user, handler, command, commandMatcher.group(2))); - } - } else { - if (multiLineHandler != null) { - multiLineBuffer.add(line); - } - } - } - if (multiLineHandler != null) { - ret.add(new CommandInvocation(formatId(baseId, subId), user, multiLineHandler, multiLineCommand, String.join("\n", multiLineBuffer))); - } - return ret; - } - private Optional nextCommand(PullRequest pr, List comments) { var self = pr.repository().forge().currentUser(); var body = PullRequestBody.parse(pr).bodyText(); - var allCommands = Stream.concat(extractCommands(body, "body", pr.author()).stream(), + var allCommands = Stream.concat(CommandExtractor.extractCommands(commandHandlers, body, "body", pr.author()).stream(), comments.stream() .filter(comment -> !comment.author().equals(self) || comment.body().endsWith(selfCommandMarker)) - .flatMap(c -> extractCommands(c.body(), c.id(), c.author()).stream())) + .flatMap(c -> CommandExtractor.extractCommands(commandHandlers, c.body(), c.id(), c.author()).stream())) .collect(Collectors.toList()); var handled = comments.stream() @@ -180,7 +132,19 @@ private Optional nextCommand(PullRequest pr, List co .findFirst(); } - private void processCommand(PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments) { + private Optional resultingCommitHash(List allComments) { + return allComments.stream() + .filter(comment -> comment.author().id().equals(pr.repository().forge().currentUser().id())) + .map(Comment::body) + .map(pushedPattern::matcher) + .filter(Matcher::find) + .map(m -> m.group(1)) + .map(Hash::new) + .findAny(); + } + + private void processCommand(PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments, + boolean isCommit) { var writer = new StringWriter(); var printer = new PrintWriter(writer); @@ -191,7 +155,33 @@ private void processCommand(PullRequest pr, CensusInstance censusInstance, Path var handler = command.handler(); if (handler.isPresent()) { - handler.get().handle(bot, pr, censusInstance, scratchPath, command, allComments, printer); + if (isCommit) { + if (handler.get().allowedInCommit()) { + var hash = resultingCommitHash(allComments); + if (hash.isPresent()) { + handler.get().handleCommit(bot, hash.get(), scratchPath, command, allComments, printer); + } else { + printer.print("The command `"); + printer.print(command.name()); + printer.println("` can only be used in a pull request that has been integrated."); + } + } else { + printer.print("The command `"); + printer.print(command.name()); + printer.println("` can only be used in open pull requests."); + } + } else { + if (handler.get().allowedInPullRequest()) { + if (command.id().startsWith("body") && !handler.get().allowedInBody()) { + handler = Optional.of(new CommandWorkItem.InvalidBodyCommandHandler()); + } + handler.get().handle(bot, pr, censusInstance, scratchPath, command, allComments, printer); + } else { + printer.print("The command `"); + printer.print(command.name()); + printer.println("` can only be used in a pull request that has not yet been integrated."); + } + } } else { printer.print("Unknown command `"); printer.print(command.name()); @@ -205,20 +195,21 @@ private void processCommand(PullRequest pr, CensusInstance censusInstance, Path public Collection run(Path scratchPath) { log.info("Looking for PR commands"); - if (pr.labels().contains("integrated")) { - log.info("Skip checking for commands in integrated PR"); - return List.of(); - } - var comments = pr.comments(); var nextCommand = nextCommand(pr, comments); + if (nextCommand.isEmpty()) { log.info("No new non-external PR commands found, stopping further processing"); // When all commands are processed, it's time to check labels // Must re-fetch PR after running the command, the command might have updated the PR var updatedPR = pr.repository().pullRequest(pr.id()); - return List.of(new LabelerWorkItem(bot, updatedPR, errorHandler)); + if (!pr.labels().contains("integrated")) { + return List.of(new LabelerWorkItem(bot, updatedPR, errorHandler)); + } else { + log.info("Skip updating labels in integrated PR"); + return List.of(); + } } var seedPath = bot.seedStorage().orElse(scratchPath.resolve("seeds")); @@ -228,13 +219,18 @@ public Collection run(Path scratchPath) { bot.confOverrideRepository().orElse(null), bot.confOverrideName(), bot.confOverrideRef()); var command = nextCommand.get(); log.info("Processing command: " + command.id() + " - " + command.name()); - processCommand(pr, census, scratchPath.resolve("pr").resolve("command"), command, comments); - // Must re-fetch PR after running the command, the command might have updated the PR - var updatedPR = pr.repository().pullRequest(pr.id()); + if (!pr.labels().contains("integrated")) { + processCommand(pr, census, scratchPath.resolve("pr").resolve("command"), command, comments, false); + // Must re-fetch PR after running the command, the command might have updated the PR + var updatedPR = pr.repository().pullRequest(pr.id()); - // Run another check to reflect potential changes from commands - return List.of(new CheckWorkItem(bot, updatedPR, errorHandler)); + // Run another check to reflect potential changes from commands + return List.of(new CheckWorkItem(bot, updatedPR, errorHandler)); + } else { + processCommand(pr, census, scratchPath.resolve("pr").resolve("command"), command, comments, true); + return List.of(); + } } @Override diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommitCommandWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommitCommandWorkItem.java new file mode 100644 index 000000000..f35c21813 --- /dev/null +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommitCommandWorkItem.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020, 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.bot.WorkItem; +import org.openjdk.skara.forge.*; +import org.openjdk.skara.issuetracker.Comment; +import org.openjdk.skara.vcs.Hash; + +import java.io.*; +import java.nio.file.Path; +import java.util.*; +import java.util.logging.Logger; +import java.util.regex.*; +import java.util.stream.*; + +public class CommitCommandWorkItem implements WorkItem { + private final PullRequestBot bot; + private final CommitComment commitComment; + + private static final String commandReplyMarker = ""; + private static final Pattern commandReplyPattern = Pattern.compile(""); + + private static final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr"); + + private static final Map commandHandlers = Map.ofEntries( + Map.entry("help", new HelpCommand()) + ); + + static class HelpCommand implements CommandHandler { + @Override + public void handleCommit(PullRequestBot bot, Hash hash, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) { + reply.println("Available commands:"); + Stream.concat( + commandHandlers.entrySet().stream() + .map(entry -> entry.getKey() + " - " + entry.getValue().description()), + bot.externalCommands().entrySet().stream() + .map(entry -> entry.getKey() + " - " + entry.getValue()) + ).sorted().forEachOrdered(c -> reply.println(" * " + c)); + } + + @Override + public String description() { + return "shows this text"; + } + + @Override + public boolean allowedInCommit() { + return true; + } + } + + CommitCommandWorkItem(PullRequestBot bot, CommitComment commitComment) { + this.bot = bot; + this.commitComment = commitComment; + } + + @Override + public boolean concurrentWith(WorkItem other) { + if (!(other instanceof CommitCommandWorkItem)) { + return true; + } + CommitCommandWorkItem otherItem = (CommitCommandWorkItem) other; + if (!bot.repo().webUrl().equals(otherItem.bot.repo().webUrl())) { + return true; + } + if (!commitComment.commit().equals(otherItem.commitComment.commit())) { + return true; + } + return false; + } + + private Optional nextCommand(List allComments) { + var self = bot.repo().forge().currentUser(); + var command = CommandExtractor.extractCommands(commandHandlers, commitComment.body(), + commitComment.id(), commitComment.author()); + if (command.isEmpty()) { + return Optional.empty(); + } + + var handled = allComments.stream() + .filter(c -> c.author().equals(self)) + .map(c -> commandReplyPattern.matcher(c.body())) + .filter(Matcher::find) + .map(matcher -> matcher.group(1)) + .collect(Collectors.toSet()); + + if (handled.contains(commitComment.id())) { + return Optional.empty(); + } else { + return Optional.of(command.get(0)); + } + } + + private void processCommand(Path scratchPath, CommandInvocation command, List allComments) { + var writer = new StringWriter(); + var printer = new PrintWriter(writer); + + printer.println(String.format(commandReplyMarker, command.id())); + + var handler = command.handler(); + if (handler.isPresent()) { + if (handler.get().allowedInCommit()) { + var comments = allComments.stream() + .map(cc -> (Comment)cc) + .collect(Collectors.toList()); + handler.get().handleCommit(bot, commitComment.commit(), scratchPath, command, comments, printer); + } else { + printer.print("The command `"); + printer.print(command.name()); + printer.println("` can only be used in pull requests."); + } + } else { + printer.print("Unknown command `"); + printer.print(command.name()); + printer.println("` - for a list of valid commands use `/help`."); + } + + bot.repo().addCommitComment(commitComment.commit(), writer.toString()); + } + @Override + public Collection run(Path scratchPath) { + log.info("Looking for commit comment commands"); + + var allComments = bot.repo().commitComments(commitComment.commit()); + var nextCommand = nextCommand(allComments); + + if (nextCommand.isEmpty()) { + log.info("No new commit comments found, stopping further processing"); + } else { + processCommand(scratchPath, nextCommand.get(), allComments); + } + + return List.of(); + } +} diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java index d0db3d95b..08f93fe88 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java @@ -25,6 +25,7 @@ import org.openjdk.skara.bot.*; import org.openjdk.skara.census.Contributor; import org.openjdk.skara.forge.*; +import org.openjdk.skara.host.*; import org.openjdk.skara.issuetracker.*; import org.openjdk.skara.json.JSONValue; import org.openjdk.skara.vcs.Hash; @@ -36,6 +37,7 @@ import java.util.concurrent.*; import java.util.logging.Logger; import java.util.regex.Pattern; +import java.util.stream.Collectors; class PullRequestBot implements Bot { private final HostedRepository remoteRepo; @@ -57,12 +59,14 @@ class PullRequestBot implements Bot { private final String confOverrideName; private final String confOverrideRef; private final String censusLink; + private final Set commitCommandUsers; private final ConcurrentMap currentLabels; private final ConcurrentHashMap scheduledRechecks; private final PullRequestUpdateCache updateCache; private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr"); private Instant lastFullUpdate; + private final Set processedCommitComments; PullRequestBot(HostedRepository repo, HostedRepository censusRepo, String censusRef, LabelConfiguration labelConfiguration, Map externalCommands, @@ -71,7 +75,7 @@ class PullRequestBot implements Bot { Map readyComments, IssueProject issueProject, boolean ignoreStaleReviews, Set allowedIssueTypes, Pattern allowedTargetBranches, Path seedStorage, HostedRepository confOverrideRepo, String confOverrideName, - String confOverrideRef, String censusLink) { + String confOverrideRef, String censusLink, List commitCommandUsers) { remoteRepo = repo; this.censusRepo = censusRepo; this.censusRef = censusRef; @@ -92,12 +96,17 @@ class PullRequestBot implements Bot { this.confOverrideRef = confOverrideRef; this.censusLink = censusLink; + this.commitCommandUsers = commitCommandUsers.stream() + .map(HostUser::id) + .collect(Collectors.toSet()); + currentLabels = new ConcurrentHashMap<>(); scheduledRechecks = new ConcurrentHashMap<>(); updateCache = new PullRequestUpdateCache(); // Only check recently updated when starting up to avoid congestion lastFullUpdate = Instant.now(); + processedCommitComments = new HashSet<>(); } static PullRequestBotBuilder newBuilder() { @@ -147,28 +156,34 @@ void scheduleRecheckAt(PullRequest pr, Instant expiresAt) { scheduledRechecks.put(pr.webUrl().toString(), expiresAt); } - private List getWorkItems(List pullRequests) { + private List getWorkItems(List pullRequests, List commitComments) { var ret = new LinkedList(); for (var pr : pullRequests) { - if (pr.state() != Issue.State.OPEN) { - continue; - } if (updateCache.needsUpdate(pr) || checkHasExpired(pr)) { if (!isReady(pr)) { continue; } - - ret.add(new CheckWorkItem(this, pr, e -> updateCache.invalidate(pr))); + if (pr.state() == Issue.State.OPEN) { + ret.add(new CheckWorkItem(this, pr, e -> updateCache.invalidate(pr))); + } else { + // Closed PR's do not need to be checked + ret.add(new CommandWorkItem(this, pr, e -> updateCache.invalidate(pr))); + } } } + for (var commitComment : commitComments) { + ret.add(new CommitCommandWorkItem(this, commitComment)); + } + return ret; } @Override public List getPeriodicItems() { List prs; + List commitComments = List.of(); if (lastFullUpdate == null || lastFullUpdate.isBefore(Instant.now().minus(Duration.ofMinutes(10)))) { lastFullUpdate = Instant.now(); @@ -179,8 +194,19 @@ public List getPeriodicItems() { prs = remoteRepo.pullRequests(ZonedDateTime.now().minus(Duration.ofDays(1))); } + if (!commitCommandUsers.isEmpty()) { + commitComments = remoteRepo.recentCommitComments().stream() + .filter(cc -> !processedCommitComments.contains(cc.id())) + .filter(cc -> commitCommandUsers.contains(cc.author().id())) + .collect(Collectors.toList()); + if (!commitComments.isEmpty()) { + processedCommitComments.addAll(commitComments.stream() + .map(Comment::id) + .collect(Collectors.toList())); + } + } - return getWorkItems(prs); + return getWorkItems(prs, commitComments); } @Override @@ -190,7 +216,11 @@ public List processWebHook(JSONValue body) { return new ArrayList<>(); } - return getWorkItems(webHook.get().updatedPullRequests()); + return getWorkItems(webHook.get().updatedPullRequests(), List.of()); + } + + HostedRepository repo() { + return remoteRepo; } HostedRepository censusRepo() { diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotBuilder.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotBuilder.java index 933a655ad..9bd5848fa 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotBuilder.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotBuilder.java @@ -22,6 +22,7 @@ */ package org.openjdk.skara.bots.pr; +import org.openjdk.skara.host.HostUser; import org.openjdk.skara.vcs.Branch; import org.openjdk.skara.vcs.VCS; import org.openjdk.skara.forge.*; @@ -51,6 +52,7 @@ public class PullRequestBotBuilder { private String confOverrideName = ".conf/jcheck"; private String confOverrideRef = Branch.defaultFor(VCS.GIT).name(); private String censusLink = null; + private List commitCommandUsers = List.of(); PullRequestBotBuilder() { } @@ -150,11 +152,16 @@ public PullRequestBotBuilder censusLink(String censusLink) { return this; } + public PullRequestBotBuilder commitCommandUsers(List commitCommandUsers) { + this.commitCommandUsers = commitCommandUsers; + return this; + } + public PullRequestBot build() { return new PullRequestBot(repo, censusRepo, censusRef, labelConfiguration, externalCommands, blockingCheckLabels, readyLabels, twoReviewersLabels, twentyFourHoursLabels, readyComments, issueProject, ignoreStaleReviews, allowedIssueTypes, allowedTargetBranches, seedStorage, confOverrideRepo, confOverrideName, - confOverrideRef, censusLink); + confOverrideRef, censusLink, commitCommandUsers); } } diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotFactory.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotFactory.java index 95d043973..b68cac2de 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotFactory.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBotFactory.java @@ -24,6 +24,7 @@ import org.openjdk.skara.bot.*; import org.openjdk.skara.forge.*; +import org.openjdk.skara.host.HostUser; import org.openjdk.skara.json.*; import java.util.*; @@ -137,6 +138,14 @@ public List create(BotConfiguration configuration) { botBuilder.censusLink(repo.value().get("censuslink").asString()); } + if (repo.value().contains("commitcommanders")) { + var allowed = repo.value().get("commitcommanders").stream() + .map(JSONValue::asString) + .map(s -> HostUser.builder().id(s).build()) + .collect(Collectors.toList()); + botBuilder.commitCommandUsers(allowed); + } + ret.add(botBuilder.build()); } diff --git a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandAsserts.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandAsserts.java new file mode 100644 index 000000000..8c26b674f --- /dev/null +++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandAsserts.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 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.forge.CommitComment; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CommitCommandAsserts { + public static void assertLastCommentContains(List comments, String contains) { + assertTrue(!comments.isEmpty()); + var lastComment = comments.get(comments.size() - 1); + assertTrue(lastComment.body().contains(contains), lastComment.body()); + } +} diff --git a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandTests.java new file mode 100644 index 000000000..a8098d4bc --- /dev/null +++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommitCommandTests.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020, 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.junit.jupiter.api.*; +import org.openjdk.skara.issuetracker.Issue; +import org.openjdk.skara.test.*; + +import java.io.IOException; +import java.util.List; + +public class CommitCommandTests { + @Test + void simple(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var author = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var seedFolder = tempFolder.path().resolve("seed"); + var bot = PullRequestBot.newBuilder() + .repo(author) + .censusRepo(censusBuilder.build()) + .censusLink("https://census.com/{{contributor}}-profile") + .seedStorage(seedFolder) + .commitCommandUsers(List.of(author.forge().currentUser())) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + localRepo.push(masterHash, author.url(), "master", true); + + // Make a change directly on master + var editHash = CheckableRepository.appendAndCommit(localRepo); + localRepo.push(editHash, author.url(), "master"); + + // Add a help command + author.addCommitComment(editHash, "/help"); + TestBotRunner.runPeriodicItems(bot); + + // Look at the reply + var replies = author.commitComments(editHash); + CommitCommandAsserts.assertLastCommentContains(replies, "Available commands"); + + // Try an invalid one + author.addCommitComment(editHash, "/hello"); + TestBotRunner.runPeriodicItems(bot); + + replies = author.commitComments(editHash); + CommitCommandAsserts.assertLastCommentContains(replies, "Unknown"); + } + } + + @Test + void simplePullRequest(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var botRepo = credentials.getHostedRepository(); + var author = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var seedFolder = tempFolder.path().resolve("seed"); + var bot = PullRequestBot.newBuilder() + .repo(botRepo) + .censusRepo(censusBuilder.build()) + .censusLink("https://census.com/{{contributor}}-profile") + .seedStorage(seedFolder) + .commitCommandUsers(List.of(author.forge().currentUser())) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + localRepo.push(masterHash, author.url(), "master", true); + + // Make a change with a corresponding PR + var editHash = CheckableRepository.appendAndCommit(localRepo); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request"); + + // Simulate an integration + var botPr = botRepo.pullRequest(pr.id()); + localRepo.push(editHash, author.url(), "master"); + botPr.addComment("Pushed as commit " + editHash.hex() + "."); + botPr.addLabel("integrated"); + botPr.setState(Issue.State.CLOSED); + + // Add a help command + pr.addComment("/help"); + TestBotRunner.runPeriodicItems(bot); + PullRequestAsserts.assertLastCommentContains(pr, "Available commands"); + + // Try an unavailable one + pr.addComment("/integrate"); + TestBotRunner.runPeriodicItems(bot); + PullRequestAsserts.assertLastCommentContains(pr, "can only be used in open pull requests"); + } + } +} diff --git a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java index 4450340cf..92a44ac89 100644 --- a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java +++ b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java @@ -49,7 +49,7 @@ public TestHostedRepository(TestHost host, String projectName, Repository localR this.projectName = projectName; this.localRepository = localRepository; pullRequestPattern = Pattern.compile(url().toString() + "/pr/" + "(\\d+)"); - commitComments = new HashMap>(); + commitComments = new HashMap<>(); nextCommitCommentId = 0; }