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.notify; 24 25 import org.openjdk.skara.email.*; 26 import org.openjdk.skara.forge.HostedRepository; 27 import org.openjdk.skara.mailinglist.*; 28 import org.openjdk.skara.vcs.*; 29 import org.openjdk.skara.vcs.openjdk.OpenJDKTag; 30 31 import java.io.*; 32 import java.time.Duration; 33 import java.time.format.DateTimeFormatter; 34 import java.util.*; 35 import java.util.logging.Logger; 36 import java.util.regex.Pattern; 37 import java.util.stream.Collectors; 38 39 public class MailingListUpdater implements UpdateConsumer { 40 private final MailingList list; 41 private final EmailAddress recipient; 42 private final EmailAddress sender; 43 private final EmailAddress author; 44 private final boolean includeBranch; 45 private final Mode mode; 46 private final Map<String, String> headers; 47 private final Pattern allowedAuthorDomains; 48 private final Logger log = Logger.getLogger("org.openjdk.skara.bots.notify"); 49 50 enum Mode { 51 ALL, 52 PR, 53 PR_ONLY 54 } 55 56 MailingListUpdater(MailingList list, EmailAddress recipient, EmailAddress sender, EmailAddress author, 57 boolean includeBranch, Mode mode, Map<String, String> headers, Pattern allowedAuthorDomains) { 58 this.list = list; 59 this.recipient = recipient; 60 this.sender = sender; 61 this.author = author; 62 this.includeBranch = includeBranch; 63 this.mode = mode; 64 this.headers = headers; 65 this.allowedAuthorDomains = allowedAuthorDomains; 66 } 67 68 private String tagAnnotationToText(HostedRepository repository, Tag.Annotated annotation) { 69 var writer = new StringWriter(); 70 var printer = new PrintWriter(writer); 71 72 printer.println("Tagged by: " + annotation.author().name() + " <" + annotation.author().email() + ">"); 73 printer.println("Date: " + annotation.date().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss +0000"))); 74 printer.println(); 75 printer.print(String.join("\n", annotation.message())); 76 77 return writer.toString(); 78 } 79 80 private EmailAddress filteredAuthor(EmailAddress commitAddress) { 81 if (author != null) { 82 return author; 83 } 84 var allowedAuthorMatcher = allowedAuthorDomains.matcher(commitAddress.domain()); 85 if (!allowedAuthorMatcher.matches()) { 86 return sender; 87 } else { 88 return commitAddress; 89 } 90 } 91 92 private EmailAddress commitToAuthor(Commit commit) { 93 return filteredAuthor(EmailAddress.from(commit.committer().name(), commit.committer().email())); 94 } 95 96 private EmailAddress annotationToAuthor(Tag.Annotated annotation) { 97 return filteredAuthor(EmailAddress.from(annotation.author().name(), annotation.author().email())); 98 } 99 100 private String commitsToSubject(HostedRepository repository, List<Commit> commits, Branch branch) { 101 var subject = new StringBuilder(); 102 subject.append(repository.repositoryType().shortName()); 103 subject.append(": "); 104 subject.append(repository.name()); 105 subject.append(": "); 106 if (includeBranch) { 107 subject.append(branch.name()); 108 subject.append(": "); 109 } 110 if (commits.size() > 1) { 111 subject.append(commits.size()); 112 subject.append(" new changesets"); 113 } else { 114 subject.append(commits.get(0).message().get(0)); 115 } 116 return subject.toString(); 117 } 118 119 private String tagToSubject(HostedRepository repository, Hash hash, Tag tag) { 120 return repository.repositoryType().shortName() + 121 ": " + 122 repository.name() + 123 ": Added tag " + 124 tag + 125 " for changeset " + 126 hash.abbreviate(); 127 } 128 129 private List<Commit> filterAndSendPrCommits(HostedRepository repository, List<Commit> commits) { 130 var ret = new ArrayList<Commit>(); 131 132 var rfrs = list.conversations(Duration.ofDays(365)).stream() 133 .map(Conversation::first) 134 .filter(email -> email.subject().startsWith("RFR: ")) 135 .collect(Collectors.toList()); 136 137 for (var commit : commits) { 138 var candidates = repository.findPullRequestsWithComment(null, "Pushed as commit " + commit.hash() + "."); 139 if (candidates.size() != 1) { 140 log.warning("Commit " + commit.hash() + " matches " + candidates.size() + " pull requests - expected 1"); 141 ret.add(commit); 142 continue; 143 } 144 145 var candidate = candidates.get(0); 146 var prLink = candidate.webUrl(); 147 var prLinkPattern = Pattern.compile("^(?:PR: )?" + Pattern.quote(prLink.toString()), Pattern.MULTILINE); 148 149 var rfrCandidates = rfrs.stream() 150 .filter(email -> prLinkPattern.matcher(email.body()).find()) 151 .collect(Collectors.toList()); 152 if (rfrCandidates.size() != 1) { 153 log.warning("Pull request " + prLink + " found in " + rfrCandidates.size() + " RFR threads - expected 1"); 154 ret.add(commit); 155 continue; 156 } 157 var rfr = rfrCandidates.get(0); 158 159 var body = CommitFormatters.commitToText(repository, commit); 160 var email = Email.reply(rfr, "Re: [Integrated] " + rfr.subject(), body) 161 .sender(sender) 162 .author(commitToAuthor(commit)) 163 .recipient(recipient) 164 .headers(headers) 165 .build(); 166 list.post(email); 167 } 168 169 return ret; 170 } 171 172 private void sendCombinedCommits(HostedRepository repository, List<Commit> commits, Branch branch) { 173 if (commits.size() == 0) { 174 return; 175 } 176 177 var writer = new StringWriter(); 178 var printer = new PrintWriter(writer); 179 180 for (var commit : commits) { 181 printer.println(CommitFormatters.commitToText(repository, commit)); 182 } 183 184 var subject = commitsToSubject(repository, commits, branch); 185 var lastCommit = commits.get(commits.size() - 1); 186 var commitAddress = filteredAuthor(EmailAddress.from(lastCommit.committer().name(), lastCommit.committer().email())); 187 var email = Email.create(subject, writer.toString()) 188 .sender(sender) 189 .author(commitAddress) 190 .recipient(recipient) 191 .headers(headers) 192 .build(); 193 194 list.post(email); 195 } 196 197 @Override 198 public void handleCommits(HostedRepository repository, List<Commit> commits, Branch branch) { 199 switch (mode) { 200 case PR_ONLY: 201 filterAndSendPrCommits(repository, commits); 202 break; 203 case PR: 204 commits = filterAndSendPrCommits(repository, commits); 205 // fall-through 206 case ALL: 207 sendCombinedCommits(repository, commits, branch); 208 break; 209 } 210 } 211 212 @Override 213 public void handleOpenJDKTagCommits(HostedRepository repository, List<Commit> commits, OpenJDKTag tag, Tag.Annotated annotation) { 214 if (mode == Mode.PR_ONLY) { 215 return; 216 } 217 var writer = new StringWriter(); 218 var printer = new PrintWriter(writer); 219 220 var taggedCommit = commits.get(commits.size() - 1); 221 if (annotation != null) { 222 printer.println(tagAnnotationToText(repository, annotation)); 223 } 224 printer.println(CommitFormatters.commitToTextBrief(repository, taggedCommit)); 225 226 printer.println("The following commits are included in " + tag.tag()); 227 printer.println("========================================================"); 228 for (var commit : commits) { 229 printer.print(commit.hash().abbreviate()); 230 if (commit.message().size() > 0) { 231 printer.print(": " + commit.message().get(0)); 232 } 233 printer.println(); 234 } 235 236 var subject = tagToSubject(repository, taggedCommit.hash(), tag.tag()); 237 var email = Email.create(subject, writer.toString()) 238 .sender(sender) 239 .recipient(recipient) 240 .headers(headers); 241 242 if (annotation != null) { 243 email.author(annotationToAuthor(annotation)); 244 } else { 245 email.author(commitToAuthor(taggedCommit)); 246 } 247 248 list.post(email.build()); 249 } 250 251 @Override 252 public void handleTagCommit(HostedRepository repository, Commit commit, Tag tag, Tag.Annotated annotation) { 253 if (mode == Mode.PR_ONLY) { 254 return; 255 } 256 var writer = new StringWriter(); 257 var printer = new PrintWriter(writer); 258 259 if (annotation != null) { 260 printer.println(tagAnnotationToText(repository, annotation)); 261 } 262 printer.println(CommitFormatters.commitToTextBrief(repository, commit)); 263 264 var subject = tagToSubject(repository, commit.hash(), tag); 265 var email = Email.create(subject, writer.toString()) 266 .sender(sender) 267 .recipient(recipient) 268 .headers(headers); 269 270 if (annotation != null) { 271 email.author(annotationToAuthor(annotation)); 272 } else { 273 email.author(commitToAuthor(commit)); 274 } 275 276 list.post(email.build()); 277 } 278 279 private String newBranchSubject(HostedRepository repository, List<Commit> commits, Branch parent, Branch branch) { 280 var subject = new StringBuilder(); 281 subject.append(repository.repositoryType().shortName()); 282 subject.append(": "); 283 subject.append(repository.name()); 284 subject.append(": created branch "); 285 subject.append(branch); 286 subject.append(" based on the branch "); 287 subject.append(parent); 288 subject.append(" containing "); 289 subject.append(commits.size()); 290 subject.append(" unique commit"); 291 if (commits.size() != 1) { 292 subject.append("s"); 293 } 294 295 return subject.toString(); 296 } 297 298 @Override 299 public void handleNewBranch(HostedRepository repository, List<Commit> commits, Branch parent, Branch branch) { 300 var writer = new StringWriter(); 301 var printer = new PrintWriter(writer); 302 303 if (commits.size() > 0) { 304 printer.println("The following commits are unique to the " + branch.name() + " branch:"); 305 printer.println("========================================================"); 306 for (var commit : commits) { 307 printer.print(commit.hash().abbreviate()); 308 if (commit.message().size() > 0) { 309 printer.print(": " + commit.message().get(0)); 310 } 311 printer.println(); 312 } 313 } else { 314 printer.println("The new branch " + branch.name() + " is currently identical to the " + parent.name() + " branch."); 315 } 316 317 var subject = newBranchSubject(repository, commits, parent, branch); 318 var finalAuthor = commits.size() > 0 ? commitToAuthor(commits.get(commits.size() - 1)) : sender; 319 320 var email = Email.create(subject, writer.toString()) 321 .sender(sender) 322 .author(finalAuthor) 323 .recipient(recipient) 324 .headers(headers) 325 .build(); 326 list.post(email); 327 } 328 }