1 /*
  2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package org.openjdk.skara.bots.bridgekeeper;
 24 
 25 import org.openjdk.skara.bot.*;
 26 import org.openjdk.skara.forge.*;
 27 
 28 import java.nio.file.Path;
 29 import java.time.*;
 30 import java.util.*;
 31 import java.util.logging.Logger;
 32 
 33 class PullRequestPrunerBotWorkItem implements WorkItem {
 34     private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
 35     private final HostedRepository repository;
 36     private final PullRequest pr;
 37     private final Duration maxAge;
 38 
 39     PullRequestPrunerBotWorkItem(HostedRepository repository, PullRequest pr, Duration maxAge) {
 40         this.pr = pr;
 41         this.repository = repository;
 42         this.maxAge = maxAge;
 43     }
 44 
 45     @Override
 46     public boolean concurrentWith(WorkItem other) {
 47         if (!(other instanceof PullRequestPrunerBotWorkItem)) {
 48             return true;
 49         }
 50         PullRequestPrunerBotWorkItem otherItem = (PullRequestPrunerBotWorkItem) other;
 51         if (!pr.id().equals(otherItem.pr.id())) {
 52             return true;
 53         }
 54         if (!repository.name().equals(otherItem.repository.name())) {
 55             return true;
 56         }
 57         return false;
 58     }
 59 
 60     // Prune durations are on the order of days and weeks
 61     private String formatDuration(Duration duration) {
 62         var count = duration.toDays();
 63         var unit = "day";
 64 
 65         if (count > 14) {
 66             count /= 7;
 67             unit = "week";
 68         }
 69         if (count != 1) {
 70             unit += "s";
 71         }
 72         return count + " " + unit;
 73     }
 74 
 75     private final String noticeMarker = "<!-- PullrequestCloserBot auto close notification -->";
 76 
 77     @Override
 78     public void run(Path scratchPath) {
 79         var comments = pr.comments();
 80         if (comments.size() > 0) {
 81             var lastComment = comments.get(comments.size() - 1);
 82             if (lastComment.author().equals(repository.forge().currentUser()) && lastComment.body().contains(noticeMarker)) {
 83                 var message = "@" + pr.author().userName() + " This pull request has been inactive for more than " +
 84                         formatDuration(maxAge.multipliedBy(2)) + " and will now be automatically closed. If you would " +
 85                         "like to continue working on this pull request in the future, feel free to reopen it!";
 86                 log.fine("Posting prune message");
 87                 pr.addComment(message);
 88                 pr.setState(PullRequest.State.CLOSED);
 89                 return;
 90             }
 91         }
 92 
 93         var message = "@" + pr.author().userName() + " This pull request has been inactive for more than " +
 94                 formatDuration(maxAge) + " and will be automatically closed if another " + formatDuration(maxAge) +
 95                 " passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free " +
 96                 "to ask for assistance if you need help with progressing this pull request towards integration!";
 97 
 98         log.fine("Posting prune notification message");
 99         pr.addComment(noticeMarker + "\n\n" + message);
100     }
101 }
102 
103 public class PullRequestPrunerBot implements Bot {
104     private final HostedRepository repository;
105     private final Duration maxAge;
106 
107     PullRequestPrunerBot(HostedRepository repository, Duration maxAge) {
108         this.repository = repository;
109         this.maxAge = maxAge;
110     }
111 
112     @Override
113     public List<WorkItem> getPeriodicItems() {
114         List<WorkItem> ret = new LinkedList<>();
115         var oldestAllowed = ZonedDateTime.now().minus(maxAge);
116 
117         for (var pr : repository.pullRequests()) {
118             if (pr.updatedAt().isBefore(oldestAllowed)) {
119                 var item = new PullRequestPrunerBotWorkItem(repository, pr, maxAge);
120                 ret.add(item);
121             }
122         }
123 
124         return ret;
125     }
126 }