Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

329: Add /cc command #645

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -41,17 +41,19 @@ public class CommandWorkItem extends PullRequestWorkItem {
private static final Pattern commandReplyPattern = Pattern.compile("<!-- Jmerge command reply message \\((\\S+)\\) -->");
private static final String selfCommandMarker = "<!-- Valid self-command -->";

private final static Map<String, CommandHandler> commandHandlers = Map.of(
"help", new HelpCommand(),
"integrate", new IntegrateCommand(),
"sponsor", new SponsorCommand(),
"contributor", new ContributorCommand(),
"summary", new SummaryCommand(),
"issue", new IssueCommand(),
"solves", new IssueCommand("solves"),
"reviewers", new ReviewersCommand(),
"csr", new CSRCommand(),
"reviewer", new ReviewerCommand()
private static final Map<String, CommandHandler> commandHandlers = Map.ofEntries(
Map.entry("help", new HelpCommand()),
Map.entry("integrate", new IntegrateCommand()),
Map.entry("sponsor", new SponsorCommand()),
Map.entry("contributor", new ContributorCommand()),
Map.entry("summary", new SummaryCommand()),
Map.entry("issue", new IssueCommand()),
Map.entry("solves", new IssueCommand("solves")),
Map.entry("reviewers", new ReviewersCommand()),
Map.entry("csr", new CSRCommand()),
Map.entry("reviewer", new ReviewerCommand()),
Map.entry("label", new LabelCommand()),
Map.entry("cc", new LabelCommand("cc"))
);

static class HelpCommand implements CommandHandler {
@@ -0,0 +1,121 @@
/*
* 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.*;
import org.openjdk.skara.issuetracker.Comment;

import java.io.*;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;

public class LabelCommand implements CommandHandler {
private final String commandName;

private static final Pattern argumentPattern = Pattern.compile("(?:(add|remove)\\s+)?((?:[A-Za-z0-9_-]+[\\s,]*)+)");

LabelCommand() {
this("label");
}

LabelCommand(String commandName) {
this.commandName = commandName;
}

private void showHelp(LabelConfiguration labelConfiguration, PrintWriter reply) {
reply.println("Usage: `/" + commandName + "` <add|remove> [label[, label, ...]]` where `label` is an additional classification that should " +
"be applied to this PR. These labels are valid:");
labelConfiguration.allowed().forEach(label -> reply.println(" * `" + label + "`"));
}

private Set<String> automaticLabels(PullRequestBot bot, PullRequest pr, Path scratchPath) throws IOException {
var path = scratchPath.resolve("pr").resolve("labelcommand").resolve(pr.repository().name());
var seedPath = bot.seedStorage().orElse(scratchPath.resolve("seeds"));
var hostedRepositoryPool = new HostedRepositoryPool(seedPath);
var localRepo = PullRequestUtils.materialize(hostedRepositoryPool, pr, path);
var files = PullRequestUtils.changedFiles(pr, localRepo);
return bot.labelConfiguration().fromChanges(files);
}

@Override
public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
if (!comment.author().equals(pr.author()) && (!ProjectPermissions.mayCommit(censusInstance, 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;
}

var argumentMatcher = argumentPattern.matcher(args);
if (!argumentMatcher.matches()) {
showHelp(bot.labelConfiguration(), reply);
return;
}

var labels = argumentMatcher.group(2).split("[\\s,]+");
for (var label : labels) {
if (!bot.labelConfiguration().allowed().contains(label)) {
reply.println("The label `" + label + "` is not a valid label. These labels are valid:");
bot.labelConfiguration().allowed().forEach(l -> reply.println(" * `" + l + "`"));
return;
}
}
if (labels.length == 0) {
showHelp(bot.labelConfiguration(), reply);
return;
}
var currentLabels = new HashSet<>(pr.labels());
if (argumentMatcher.group(1) == null || argumentMatcher.group(1).equals("add")) {
for (var label : labels) {
if (!currentLabels.contains(label)) {
pr.addLabel(label);
reply.println("The `" + label + "` label was successfully added.");
} else {
reply.println("The `" + label + "` label was already applied.");
}
}
} else if (argumentMatcher.group(1).equals("remove")) {
try {
var automaticLabels = automaticLabels(bot, pr, scratchPath);
for (var label : labels) {
if (currentLabels.contains(label)) {
if (automaticLabels.contains(label)) {
reply.println("The `" + label + "` label was automatically added and cannot be removed.");
} else {
pr.removeLabel(label);
reply.println("The `" + label + "` label was successfully removed.");
}
} else {
reply.println("The `" + label + "` label was not set.");
}
}
} catch (IOException e) {
reply.println("An error occurred when trying to check automatically added labels");
}
}
}

@Override
public String description() {
return "add or remove an additional classification label";
}
}
@@ -0,0 +1,110 @@
/*
* 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 java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;

public class LabelConfiguration {
private final Map<String, List<Pattern>> matchers;
private final Map<String, List<String>> groups;
private final Set<String> extra;
private final Set<String> allowed;

private LabelConfiguration(Map<String, List<Pattern>> matchers, Map<String, List<String>> groups, Set<String> extra) {
this.matchers = Collections.unmodifiableMap(matchers);
this.groups = Collections.unmodifiableMap(groups);
this.extra = Collections.unmodifiableSet(extra);

var allowed = new HashSet<String>();
allowed.addAll(matchers.keySet());
allowed.addAll(groups.keySet());
allowed.addAll(extra);
this.allowed = Collections.unmodifiableSet(allowed);
}

static class LabelConfigurationBuilder {
private final Map<String, List<Pattern>> matchers = new HashMap<>();
private final Map<String, List<String>> groups = new HashMap<>();
private final Set<String> extra = new HashSet<>();

public LabelConfigurationBuilder addMatchers(String label, List<Pattern> matchers) {
this.matchers.put(label, matchers);
return this;
}

public LabelConfigurationBuilder addGroup(String label, List<String> members) {
groups.put(label, members);
return this;
}

public LabelConfigurationBuilder addExtra(String label) {
extra.add(label);
return this;
}

public LabelConfiguration build() {
return new LabelConfiguration(matchers, groups, extra);
}
}

static LabelConfigurationBuilder newBuilder() {
return new LabelConfigurationBuilder();
}

public Set<String> fromChanges(Set<Path> changes) {
var labels = new HashSet<String>();
for (var file : changes) {
for (var label : matchers.entrySet()) {
for (var pattern : label.getValue()) {
var matcher = pattern.matcher(file.toString());
if (matcher.find()) {
labels.add(label.getKey());
break;
}
}
}
}

// If the current labels matches at least two members of a group, the group is also included
for (var group : groups.entrySet()) {
var count = 0;
for (var groupEntry : group.getValue()) {
if (labels.contains(groupEntry)) {
count++;
if (count == 2) {
labels.add(group.getKey());
break;
}
}
}
}

return labels;
}

public Set<String> allowed() {
return allowed;
}
}
@@ -42,20 +42,8 @@ public String toString() {
}

private Set<String> getLabels(Repository localRepo) throws IOException {
var labels = new HashSet<String>();
var files = PullRequestUtils.changedFiles(pr, localRepo);
for (var file : files) {
for (var label : bot.labelPatterns().entrySet()) {
for (var pattern : label.getValue()) {
var matcher = pattern.matcher(file.toString());
if (matcher.find()) {
labels.add(label.getKey());
break;
}
}
}
}
return labels;
return bot.labelConfiguration().fromChanges(files);
}

@Override
@@ -70,7 +58,7 @@ public void run(Path scratchPath) {
var localRepo = PullRequestUtils.materialize(hostedRepositoryPool, pr, path);
var newLabels = getLabels(localRepo);
var currentLabels = pr.labels().stream()
.filter(key -> bot.labelPatterns().containsKey(key))
.filter(key -> bot.labelConfiguration().allowed().contains(key))
.collect(Collectors.toSet());

// Add all labels not already set
@@ -38,7 +38,7 @@ class PullRequestBot implements Bot {
private final HostedRepository remoteRepo;
private final HostedRepository censusRepo;
private final String censusRef;
private final Map<String, List<Pattern>> labelPatterns;
private final LabelConfiguration labelConfiguration;
private final Map<String, String> externalCommands;
private final Map<String, String> blockingCheckLabels;
private final Set<String> readyLabels;
@@ -52,14 +52,14 @@ class PullRequestBot implements Bot {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr");

PullRequestBot(HostedRepository repo, HostedRepository censusRepo, String censusRef,
Map<String, List<Pattern>> labelPatterns, Map<String, String> externalCommands,
LabelConfiguration labelConfiguration, Map<String, String> externalCommands,
Map<String, String> blockingCheckLabels, Set<String> readyLabels,
Map<String, Pattern> readyComments, IssueProject issueProject, boolean ignoreStaleReviews,
Pattern allowedTargetBranches, Path seedStorage) {
remoteRepo = repo;
this.censusRepo = censusRepo;
this.censusRef = censusRef;
this.labelPatterns = labelPatterns;
this.labelConfiguration = labelConfiguration;
this.externalCommands = externalCommands;
this.blockingCheckLabels = blockingCheckLabels;
this.readyLabels = readyLabels;
@@ -149,8 +149,8 @@ String censusRef() {
return censusRef;
}

Map<String, List<Pattern>> labelPatterns() {
return labelPatterns;
LabelConfiguration labelConfiguration() {
return labelConfiguration;
}

Map<String, String> externalCommands() {
@@ -33,7 +33,7 @@ public class PullRequestBotBuilder {
private HostedRepository repo;
private HostedRepository censusRepo;
private String censusRef = "master";
private Map<String, List<Pattern>> labelPatterns = Map.of();
private LabelConfiguration labelConfiguration = LabelConfiguration.newBuilder().build();
private Map<String, String> externalCommands = Map.of();
private Map<String, String> blockingCheckLabels = Map.of();
private Set<String> readyLabels = Set.of();
@@ -61,8 +61,8 @@ public PullRequestBotBuilder censusRef(String censusRef) {
return this;
}

public PullRequestBotBuilder labelPatterns(Map<String, List<Pattern>> labelPatterns) {
this.labelPatterns = labelPatterns;
public PullRequestBotBuilder labelConfiguration(LabelConfiguration labelConfiguration) {
this.labelConfiguration = labelConfiguration;
return this;
}

@@ -107,7 +107,7 @@ public PullRequestBotBuilder seedStorage(Path seedStorage) {
}

public PullRequestBot build() {
return new PullRequestBot(repo, censusRepo, censusRef, labelPatterns, externalCommands, blockingCheckLabels,
return new PullRequestBot(repo, censusRepo, censusRef, labelConfiguration, externalCommands, blockingCheckLabels,
readyLabels, readyComments, issueProject, ignoreStaleReviews, allowedTargetBranches,
seedStorage);
}