1 package org.openjdk.skara.bots.mlbridge;
  2 
  3 import org.openjdk.skara.email.Email;
  4 import org.openjdk.skara.host.*;
  5 import org.openjdk.skara.vcs.*;
  6 
  7 import java.net.URI;
  8 import java.time.format.DateTimeFormatter;
  9 import java.util.Arrays;
 10 import java.util.regex.Pattern;
 11 import java.util.stream.Collectors;
 12 
 13 class ArchiveMessages {
 14     private static String formatCommit(Commit commit) {
 15         var ret = new StringBuilder();
 16         var message = commit.message();
 17         if (message.size() == 0) {
 18             ret.append("<no commit message found>");
 19         } else {
 20             var abbrev = commit.hash().abbreviate();
 21             ret.append(" - ").append(abbrev).append(": ").append(message.get(0).strip());
 22         }
 23         return ret.toString();
 24     }
 25 
 26     private static final String infoSeparator = "----------------";
 27 
 28     private static final Pattern commentPattern = Pattern.compile("<!--.*?-->",
 29                                                                   Pattern.DOTALL | Pattern.MULTILINE);
 30     private static final Pattern cutoffPattern = Pattern.compile("(.*?)<!-- Anything below this marker will be .*? -->",
 31                                                                  Pattern.DOTALL | Pattern.MULTILINE);
 32     private static String filterComments(String body) {
 33         var cutoffMatcher = cutoffPattern.matcher(body);
 34         if (cutoffMatcher.find()) {
 35             body = cutoffMatcher.group(1);
 36         }
 37 
 38         var commentMatcher = commentPattern.matcher(body);
 39         body = commentMatcher.replaceAll("");
 40 
 41         body = MarkdownToText.removeFormatting(body);
 42         return body.strip();
 43     }
 44 
 45     private static String quoteBody(String body) {
 46         return Arrays.stream(body.strip().split("\\R"))
 47                      .map(line -> line.length() > 0 ? line.charAt(0) == '>' ? ">" + line : "> " + line : "> ")
 48                      .collect(Collectors.joining("\n"));
 49     }
 50 
 51     private static String replyFooter(PullRequestInstance prInstance) {
 52         return "PR: " + prInstance.pr().getWebUrl();
 53     }
 54 
 55     // When changing this, ensure that the PR pattern in the notifier still matches
 56     static String composeConversation(PullRequestInstance prInstance, URI webrev) {
 57         var commitMessages = prInstance.formatCommitMessages(prInstance.baseHash(), prInstance.headHash(), ArchiveMessages::formatCommit);
 58         var filteredBody = filterComments(prInstance.pr().getBody());
 59         if (filteredBody.isEmpty()) {
 60             filteredBody = prInstance.pr().getTitle().strip();
 61         }
 62         var issueString = prInstance.issueUrl().map(url -> "  Issue: " + url + "\n").orElse("");
 63         return filteredBody + "\n\n" +
 64                 infoSeparator + "\n\n" +
 65                 "Commits:\n" +
 66                 commitMessages + "\n\n" +
 67                 "Changes: " + prInstance.changeUrl() + "\n" +
 68                 " Webrev: " + webrev.toString() + "\n" +
 69                 issueString +
 70                 "  Stats: " + prInstance.stats(prInstance.baseHash(), prInstance.headHash()) + "\n" +
 71                 "  Patch: " + prInstance.diffUrl() + "\n" +
 72                 "  Fetch: " + prInstance.fetchCommand() + "\n\n" +
 73                 replyFooter(prInstance);
 74     }
 75 
 76     static String composeRebaseComment(PullRequestInstance prInstance, URI fullWebrev) {
 77         var commitMessages = prInstance.formatCommitMessages(prInstance.baseHash(), prInstance.headHash(), ArchiveMessages::formatCommit);
 78         var issueString = prInstance.issueUrl().map(url -> "  Issue: " + url + "\n").orElse("");
 79         return "The pull request has been updated with a complete new set of changes (possibly due to a rebase).\n\n" +
 80                 infoSeparator + "\n\n" +
 81                 "Commits:\n" +
 82                 commitMessages + "\n\n" +
 83                 "Changes: " + prInstance.changeUrl() + "\n" +
 84                 " Webrev: " + fullWebrev.toString() + "\n" +
 85                 issueString +
 86                 "  Stats: " + prInstance.stats(prInstance.baseHash(), prInstance.headHash()) + "\n" +
 87                 "  Patch: " + prInstance.diffUrl() + "\n" +
 88                 "  Fetch: " + prInstance.fetchCommand() + "\n\n" +
 89                 replyFooter(prInstance);    }
 90 
 91     static String composeIncrementalComment(Hash lastHead, PullRequestInstance prInstance, URI fullWebrev, URI incrementalWebrev) {
 92         var newCommitMessages = prInstance.formatCommitMessages(lastHead, prInstance.headHash(), ArchiveMessages::formatCommit);
 93         var issueString = prInstance.issueUrl().map(url -> "  Issue: " + url + "\n").orElse("");
 94         return "The pull request has been updated with additional changes.\n\n" +
 95                 infoSeparator + "\n\n" +
 96                 "Added commits:\n" +
 97                 newCommitMessages + "\n\n" +
 98                 "Changes:\n" +
 99                 "  - all: " + prInstance.pr().getWebUrl() + "/files\n" +
100                 "  - new: " + prInstance.changeUrl(lastHead, prInstance.headHash()) + "\n\n" +
101                 "Webrevs:\n" +
102                 " - full: " + fullWebrev.toString() + "\n" +
103                 " - incr: " + incrementalWebrev.toString() + "\n\n" +
104                 issueString +
105                 "  Stats: " + prInstance.stats(lastHead, prInstance.headHash()) + "\n" +
106                 "  Patch: " + prInstance.diffUrl() + "\n" +
107                 "  Fetch: " + prInstance.fetchCommand() + "\n\n" +
108                 replyFooter(prInstance);
109     }
110 
111     private static String filterParentBody(Email parent, PullRequestInstance prInstance) {
112         var parentFooter = ArchiveMessages.replyFooter(prInstance);
113         var filteredParentBody = parent.body().strip();
114         if (filteredParentBody.endsWith(parentFooter)) {
115             return filteredParentBody.substring(0, filteredParentBody.length() - parentFooter.length()).strip();
116         } else {
117             return filteredParentBody;
118         }
119     }
120 
121     static String composeReply(Email parent, String body, PullRequestInstance prInstance) {
122         return "On " + parent.date().format(DateTimeFormatter.RFC_1123_DATE_TIME) + ", " + parent.author().toString() + " wrote:\n" +
123                 "\n" +
124                 quoteBody(filterParentBody(parent, prInstance)) +
125                 "\n\n" +
126                 filterComments(body) +
127                 "\n\n" +
128                 replyFooter(prInstance);
129     }
130 
131     static String composeCombinedReply(Email parent, String body, PullRequestInstance prInstance) {
132         return filterParentBody(parent, prInstance) +
133                 "\n\n" +
134                 filterComments(body) +
135                 "\n\n" +
136                 replyFooter(prInstance);
137     }
138 
139     static String reviewCommentBody(String body, Review.Verdict verdict, String user, String role) {
140         var result = new StringBuilder(filterComments(body));
141         if (verdict != Review.Verdict.NONE) {
142             if (result.length() > 0) {
143                 result.append("\n\n");
144                 result.append(infoSeparator);
145                 result.append("\n\n");
146             }
147             if (verdict == Review.Verdict.APPROVED) {
148                 result.append("Approved");
149             } else {
150                 result.append("Disapproved");
151             }
152             result.append(" by ");
153             result.append(user);
154             result.append(" (");
155             result.append(role);
156             result.append(").");
157         }
158         return result.toString();
159     }
160 }