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 patchToText(Patch patch) { 69 if (patch.status().isAdded()) { 70 return "+ " + patch.target().path().orElseThrow(); 71 } else if (patch.status().isDeleted()) { 72 return "- " + patch.source().path().orElseThrow(); 73 } else if (patch.status().isModified()) { 74 return "! " + patch.target().path().orElseThrow(); 75 } else { 76 return "= " + patch.target().path().orElseThrow(); 77 } 78 } 79 80 private String commitToTextBrief(HostedRepository repository, Commit commit) { 81 var writer = new StringWriter(); 82 var printer = new PrintWriter(writer); 83 84 printer.println("Changeset: " + commit.hash().abbreviate()); 85 printer.println("Author: " + commit.author().name() + " <" + commit.author().email() + ">"); 86 if (!commit.author().equals(commit.committer())) { 87 printer.println("Committer: " + commit.committer().name() + " <" + commit.committer().email() + ">"); 88 } 89 printer.println("Date: " + commit.date().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss +0000"))); 90 printer.println("URL: " + repository.webUrl(commit.hash())); 91 92 return writer.toString(); 93 } 94 95 private String commitToText(HostedRepository repository, Commit commit) { 96 var writer = new StringWriter(); 97 var printer = new PrintWriter(writer); 98 99 printer.print(commitToTextBrief(repository, commit)); 100 printer.println(); 101 printer.println(String.join("\n", commit.message())); 102 printer.println(); 103 104 for (var diff : commit.parentDiffs()) { 105 for (var patch : diff.patches()) { 106 printer.println(patchToText(patch)); 107 } 108 } 109 110 return writer.toString(); 111 } 112 113 private String tagAnnotationToText(HostedRepository repository, Tag.Annotated annotation) { 114 var writer = new StringWriter(); 115 var printer = new PrintWriter(writer); 116 117 printer.println("Tagged by: " + annotation.author().name() + " <" + annotation.author().email() + ">"); 118 printer.println("Date: " + annotation.date().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss +0000"))); 119 printer.println(); 120 printer.print(String.join("\n", annotation.message())); 121 122 return writer.toString(); 123 } 124 125 private EmailAddress filteredAuthor(EmailAddress commitAddress) { 126 if (author != null) { 127 return author; 128 } 129 var allowedAuthorMatcher = allowedAuthorDomains.matcher(commitAddress.domain()); 130 if (!allowedAuthorMatcher.matches()) { 131 return sender; 132 } else { 133 return commitAddress; 134 } 135 } 136 137 private EmailAddress commitToAuthor(Commit commit) { 138 return filteredAuthor(EmailAddress.from(commit.committer().name(), commit.committer().email())); 139 } 140 141 private EmailAddress annotationToAuthor(Tag.Annotated annotation) { 142 return filteredAuthor(EmailAddress.from(annotation.author().name(), annotation.author().email())); 143 } 144 145 private String commitsToSubject(HostedRepository repository, List<Commit> commits, Branch branch) { 146 var subject = new StringBuilder(); 147 subject.append(repository.repositoryType().shortName()); 148 subject.append(": "); 149 subject.append(repository.name()); 150 subject.append(": "); 151 if (includeBranch) { 152 subject.append(branch.name()); 153 subject.append(": "); 154 } 155 if (commits.size() > 1) { 156 subject.append(commits.size()); 157 subject.append(" new changesets"); 158 } else { 159 subject.append(commits.get(0).message().get(0)); 160 } 161 return subject.toString(); 162 } 163 164 private String tagToSubject(HostedRepository repository, Hash hash, Tag tag) { 165 return repository.repositoryType().shortName() + 166 ": " + 167 repository.name() + 168 ": Added tag " + 169 tag + 170 " for changeset " + 171 hash.abbreviate(); 172 } 173 174 private List<Commit> filterAndSendPrCommits(HostedRepository repository, List<Commit> commits) { 175 var ret = new ArrayList<Commit>(); 176 177 var rfrs = list.conversations(Duration.ofDays(365)).stream() 178 .map(Conversation::first) 179 .filter(email -> email.subject().startsWith("RFR: ")) 180 .collect(Collectors.toList()); 181 182 for (var commit : commits) { 183 var candidates = repository.findPullRequestsWithComment(null, "Pushed as commit " + commit.hash() + "."); 184 if (candidates.size() != 1) { 185 log.warning("Commit " + commit.hash() + " matches " + candidates.size() + " pull requests - expected 1"); 186 ret.add(commit); 187 continue; 188 } 189 190 var candidate = candidates.get(0); 191 var prLink = candidate.webUrl(); 192 var prLinkPattern = Pattern.compile("^(?:PR: )?" + Pattern.quote(prLink.toString()), Pattern.MULTILINE); 193 194 var rfrCandidates = rfrs.stream() 195 .filter(email -> prLinkPattern.matcher(email.body()).find()) 196 .collect(Collectors.toList()); 197 if (rfrCandidates.size() != 1) { 198 log.warning("Pull request " + prLink + " found in " + rfrCandidates.size() + " RFR threads - expected 1"); 199 ret.add(commit); 200 continue; 201 } 202 var rfr = rfrCandidates.get(0); 203 204 var body = commitToText(repository, commit); 205 var email = Email.reply(rfr, "Re: [Integrated] " + rfr.subject(), body) 206 .sender(sender) 207 .author(commitToAuthor(commit)) 208 .recipient(recipient) 209 .headers(headers) 210 .build(); 211 list.post(email); 212 } 213 214 return ret; 215 } 216 217 private void sendCombinedCommits(HostedRepository repository, List<Commit> commits, Branch branch) { 218 if (commits.size() == 0) { 219 return; 220 } 221 222 var writer = new StringWriter(); 223 var printer = new PrintWriter(writer); 224 225 for (var commit : commits) { 226 printer.println(commitToText(repository, commit)); 227 } 228 229 var subject = commitsToSubject(repository, commits, branch); 230 var lastCommit = commits.get(commits.size() - 1); 231 var commitAddress = filteredAuthor(EmailAddress.from(lastCommit.committer().name(), lastCommit.committer().email())); 232 var email = Email.create(subject, writer.toString()) 233 .sender(sender) 234 .author(commitAddress) 235 .recipient(recipient) 236 .headers(headers) 237 .build(); 238 239 list.post(email); 240 } 241 242 @Override 243 public void handleCommits(HostedRepository repository, List<Commit> commits, Branch branch) { 244 switch (mode) { 245 case PR_ONLY: 246 filterAndSendPrCommits(repository, commits); 247 break; 248 case PR: 249 commits = filterAndSendPrCommits(repository, commits); 250 // fall-through 251 case ALL: 252 sendCombinedCommits(repository, commits, branch); 253 break; 254 } 255 } 256 257 @Override 258 public void handleOpenJDKTagCommits(HostedRepository repository, List<Commit> commits, OpenJDKTag tag, Tag.Annotated annotation) { 259 if (mode == Mode.PR_ONLY) { 260 return; 261 } 262 var writer = new StringWriter(); 263 var printer = new PrintWriter(writer); 264 265 var taggedCommit = commits.get(commits.size() - 1); 266 if (annotation != null) { 267 printer.println(tagAnnotationToText(repository, annotation)); 268 } 269 printer.println(commitToTextBrief(repository, taggedCommit)); 270 271 printer.println("The following commits are included in " + tag.tag()); 272 printer.println("========================================================"); 273 for (var commit : commits) { 274 printer.print(commit.hash().abbreviate()); 275 if (commit.message().size() > 0) { 276 printer.print(": " + commit.message().get(0)); 277 } 278 printer.println(); 279 } 280 281 var subject = tagToSubject(repository, taggedCommit.hash(), tag.tag()); 282 var email = Email.create(subject, writer.toString()) 283 .sender(sender) 284 .recipient(recipient) 285 .headers(headers); 286 287 if (annotation != null) { 288 email.author(annotationToAuthor(annotation)); 289 } else { 290 email.author(commitToAuthor(taggedCommit)); 291 } 292 293 list.post(email.build()); 294 } 295 296 @Override 297 public void handleTagCommit(HostedRepository repository, Commit commit, Tag tag, Tag.Annotated annotation) { 298 if (mode == Mode.PR_ONLY) { 299 return; 300 } 301 var writer = new StringWriter(); 302 var printer = new PrintWriter(writer); 303 304 if (annotation != null) { 305 printer.println(tagAnnotationToText(repository, annotation)); 306 } 307 printer.println(commitToTextBrief(repository, commit)); 308 309 var subject = tagToSubject(repository, commit.hash(), tag); 310 var email = Email.create(subject, writer.toString()) 311 .sender(sender) 312 .recipient(recipient) 313 .headers(headers); 314 315 if (annotation != null) { 316 email.author(annotationToAuthor(annotation)); 317 } else { 318 email.author(commitToAuthor(commit)); 319 } 320 321 list.post(email.build()); 322 } 323 324 private String newBranchSubject(HostedRepository repository, List<Commit> commits, Branch parent, Branch branch) { 325 var subject = new StringBuilder(); 326 subject.append(repository.repositoryType().shortName()); 327 subject.append(": "); 328 subject.append(repository.name()); 329 subject.append(": created branch "); 330 subject.append(branch); 331 subject.append(" based on the branch "); 332 subject.append(parent); 333 subject.append(" containing "); 334 subject.append(commits.size()); 335 subject.append(" unique commit"); 336 if (commits.size() != 1) { 337 subject.append("s"); 338 } 339 340 return subject.toString(); 341 } 342 343 @Override 344 public void handleNewBranch(HostedRepository repository, List<Commit> commits, Branch parent, Branch branch) { 345 var writer = new StringWriter(); 346 var printer = new PrintWriter(writer); 347 348 if (commits.size() > 0) { 349 printer.println("The following commits are unique to the " + branch.name() + " branch:"); 350 printer.println("========================================================"); 351 for (var commit : commits) { 352 printer.print(commit.hash().abbreviate()); 353 if (commit.message().size() > 0) { 354 printer.print(": " + commit.message().get(0)); 355 } 356 printer.println(); 357 } 358 } else { 359 printer.println("The new branch " + branch.name() + " is currently identical to the " + parent.name() + " branch."); 360 } 361 362 var subject = newBranchSubject(repository, commits, parent, branch); 363 var finalAuthor = commits.size() > 0 ? commitToAuthor(commits.get(commits.size() - 1)) : sender; 364 365 var email = Email.create(subject, writer.toString()) 366 .sender(sender) 367 .author(finalAuthor) 368 .recipient(recipient) 369 .headers(headers) 370 .build(); 371 list.post(email); 372 } 373 }