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 5d637b56c..a28c2fed3 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 @@ -57,7 +57,17 @@ private Optional checkProblem(Map performedChecks, String @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())) { - reply.println("Only the author (@" + pr.author().userName() + ") is allowed to issue the `integrate` command."); + reply.print("Only the author (@" + pr.author().userName() + ") is allowed to issue the `integrate` command."); + + // 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())) { + 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; + } + } + reply.println(); return; } diff --git a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java index 8f4ffe494..308f70d41 100644 --- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java +++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java @@ -426,6 +426,55 @@ void invalidCommandAuthor(TestInfo testInfo) throws IOException { } } + @Test + void invalidCommandSponsor(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory()) { + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var external = credentials.getHostedRepository(); + + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addCommitter(external.forge().currentUser().id()); + var mergeBot = PullRequestBot.newBuilder().repo(integrator).censusRepo(censusBuilder.build()).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(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request"); + + // Approve it as another user + var approvalPr = integrator.pullRequest(pr.id()); + approvalPr.addReview(Review.Verdict.APPROVED, "Approved"); + TestBotRunner.runPeriodicItems(mergeBot); + + // Mark it as ready for integration + pr.addComment("/integrate"); + TestBotRunner.runPeriodicItems(mergeBot); + + // Issue a merge command not as the PR author + var externalPr = external.pullRequest(pr.id()); + externalPr.addComment("/integrate"); + TestBotRunner.runPeriodicItems(mergeBot); + + // The bot should reply with an error message + var error = pr.comments().stream() + .filter(comment -> comment.body().contains("Only the author")) + .filter(comment -> comment.body().contains("did you mean to")) + .filter(comment -> comment.body().contains("`/sponsor`")) + .count(); + assertEquals(1, error); + } + } + @Test void autoRebase(TestInfo testInfo) throws IOException { try (var credentials = new HostCredentials(testInfo);