Skip to content
Permalink
Browse files

Add pull request prune functionality

Reviewed-by: ehelin, kcr
  • Loading branch information
Robin Westberg
Robin Westberg committed Nov 12, 2019
1 parent 56b9f65 commit e75c0b062d974330cf1273bc502f4a4b95ecfce9
@@ -24,6 +24,7 @@

import org.openjdk.skara.bot.*;

import java.time.Duration;
import java.util.*;

public class BridgekeeperBotFactory implements BotFactory {
@@ -37,11 +38,15 @@ public String name() {
var ret = new ArrayList<Bot>();
var specific = configuration.specific();

for (var repo : specific.get("repositories").asArray()) {
var bot = new BridgekeeperBot(configuration.repository(repo.asString()));
for (var repo : specific.get("mirrors").asArray()) {
var bot = new PullRequestCloserBot(configuration.repository(repo.asString()));
ret.add(bot);
}
for (var repo : specific.get("pruned").fields()) {
var maxAge = Duration.parse(repo.value().get("maxage").asString());
var bot = new PullRequestPrunerBot(configuration.repository(repo.name()), maxAge);
ret.add(bot);
}

return ret;
}
}
@@ -30,19 +30,19 @@
import java.util.function.Consumer;
import java.util.logging.Logger;

class BridgekeeperWorkItem implements WorkItem {
class PullRequestCloserBotWorkItem implements WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final HostedRepository repository;
private final PullRequest pr;
private final Consumer<RuntimeException> errorHandler;

BridgekeeperWorkItem(HostedRepository repository, PullRequest pr, Consumer<RuntimeException> errorHandler) {
PullRequestCloserBotWorkItem(HostedRepository repository, PullRequest pr, Consumer<RuntimeException> errorHandler) {
this.pr = pr;
this.repository = repository;
this.errorHandler = errorHandler;
}

private final String welcomeMarker = "<!-- BridgeKeeperBot welcome message -->";
private final String welcomeMarker = "<!-- PullrequestCloserBot welcome message -->";

private void checkWelcomeMessage() {
log.info("Checking welcome message of " + pr);
@@ -69,10 +69,10 @@ private void checkWelcomeMessage() {

@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof BridgekeeperWorkItem)) {
if (!(other instanceof PullRequestCloserBotWorkItem)) {
return true;
}
BridgekeeperWorkItem otherItem = (BridgekeeperWorkItem)other;
PullRequestCloserBotWorkItem otherItem = (PullRequestCloserBotWorkItem)other;
if (!pr.id().equals(otherItem.pr.id())) {
return true;
}
@@ -93,11 +93,11 @@ public void handleRuntimeException(RuntimeException e) {
}
}

public class BridgekeeperBot implements Bot {
public class PullRequestCloserBot implements Bot {
private final HostedRepository remoteRepo;
private final PullRequestUpdateCache updateCache;

BridgekeeperBot(HostedRepository repo) {
PullRequestCloserBot(HostedRepository repo) {
this.remoteRepo = repo;
this.updateCache = new PullRequestUpdateCache();
}
@@ -108,7 +108,7 @@ public void handleRuntimeException(RuntimeException e) {

for (var pr : remoteRepo.pullRequests()) {
if (updateCache.needsUpdate(pr)) {
var item = new BridgekeeperWorkItem(remoteRepo, pr, e -> updateCache.invalidate(pr));
var item = new PullRequestCloserBotWorkItem(remoteRepo, pr, e -> updateCache.invalidate(pr));
ret.add(item);
}
}
@@ -0,0 +1,126 @@
/*
* 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.bridgekeeper;

import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.*;

import java.nio.file.Path;
import java.time.*;
import java.util.*;
import java.util.logging.Logger;

class PullRequestPrunerBotWorkItem implements WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final HostedRepository repository;
private final PullRequest pr;
private final Duration maxAge;

PullRequestPrunerBotWorkItem(HostedRepository repository, PullRequest pr, Duration maxAge) {
this.pr = pr;
this.repository = repository;
this.maxAge = maxAge;
}

@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof PullRequestPrunerBotWorkItem)) {
return true;
}
PullRequestPrunerBotWorkItem otherItem = (PullRequestPrunerBotWorkItem) other;
if (!pr.id().equals(otherItem.pr.id())) {
return true;
}
if (!repository.name().equals(otherItem.repository.name())) {
return true;
}
return false;
}

// Prune durations are on the order of days and weeks
private String formatDuration(Duration duration) {
var count = duration.toDays();
var unit = "day";

if (count > 14) {
count /= 7;
unit = "week";
}
if (count != 1) {
unit += "s";
}
return count + " " + unit;
}

private final String noticeMarker = "<!-- PullrequestCloserBot auto close notification -->";

@Override
public void run(Path scratchPath) {
var comments = pr.comments();
if (comments.size() > 0) {
var lastComment = comments.get(comments.size() - 1);
if (lastComment.author().equals(repository.forge().currentUser()) && lastComment.body().contains(noticeMarker)) {
var message = "@" + pr.author().userName() + " This pull request has been inactive for more than " +
formatDuration(maxAge.multipliedBy(2)) + " and will now be automatically closed. If you would " +
"like to continue working on this pull request in the future, feel free to reopen it!";
log.fine("Posting prune message");
pr.addComment(message);
pr.setState(PullRequest.State.CLOSED);
return;
}
}

var message = "@" + pr.author().userName() + " This pull request has been inactive for more than " +
formatDuration(maxAge) + " and will be automatically closed if another " + formatDuration(maxAge) +
" passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free " +
"to ask for assistance if you need help with progressing this pull request towards integration!";

log.fine("Posting prune notification message");
pr.addComment(noticeMarker + "\n\n" + message);
}
}

public class PullRequestPrunerBot implements Bot {
private final HostedRepository repository;
private final Duration maxAge;

PullRequestPrunerBot(HostedRepository repository, Duration maxAge) {
this.repository = repository;
this.maxAge = maxAge;
}

@Override
public List<WorkItem> getPeriodicItems() {
List<WorkItem> ret = new LinkedList<>();
var oldestAllowed = ZonedDateTime.now().minus(maxAge);

for (var pr : repository.pullRequests()) {
if (pr.updatedAt().isBefore(oldestAllowed)) {
var item = new PullRequestPrunerBotWorkItem(repository, pr, maxAge);
ret.add(item);
}
}

return ret;
}
}
@@ -31,13 +31,13 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

class BridgekeeperBotTests {
class PullRequestCloserBotTests {
@Test
void simple(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new BridgekeeperBot(author);
var bot = new PullRequestCloserBot(author);

// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
@@ -63,7 +63,7 @@ void keepClosing(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new BridgekeeperBot(author);
var bot = new PullRequestCloserBot(author);

// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
@@ -0,0 +1,109 @@
/*
* 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.bridgekeeper;

import org.openjdk.skara.test.*;

import org.junit.jupiter.api.*;

import java.io.IOException;
import java.time.Duration;

import static org.junit.jupiter.api.Assertions.*;

class PullRequestPrunerBotTests {
@Test
void close(TestInfo testInfo) throws IOException, InterruptedException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestPrunerBot(author, Duration.ofMillis(1));

// 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(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");

// Make sure the timeout expires
Thread.sleep(100);

// Let the bot see it - it should give a notice
TestBotRunner.runPeriodicItems(bot);

assertEquals(1, pr.comments().size());
assertTrue(pr.comments().get(0).body().contains("will be automatically closed if"));

pr.addComment("I'm still working on it!");

// Make sure the timeout expires again
Thread.sleep(100);

// Let the bot see it - it should post a second notice
TestBotRunner.runPeriodicItems(bot);

assertEquals(3, pr.comments().size());
assertTrue(pr.comments().get(2).body().contains("will be automatically closed if"));

// Make sure the timeout expires again
Thread.sleep(100);

// The bot should now close it
TestBotRunner.runPeriodicItems(bot);

// There should now be no open PRs
var prs = author.pullRequests();
assertEquals(0, prs.size());
}
}

@Test
void dontClose(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestPrunerBot(author, Duration.ofDays(3));

// 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(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");

// Let the bot see it
TestBotRunner.runPeriodicItems(bot);

// There should still be an open PR
var prs = author.pullRequests();
assertEquals(1, prs.size());
}
}
}

0 comments on commit e75c0b0

Please sign in to comment.
You can’t perform that action at this time.