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.pr; 24 25 import org.openjdk.skara.census.*; 26 import org.openjdk.skara.forge.*; 27 import org.openjdk.skara.host.*; 28 import org.openjdk.skara.jcheck.JCheck; 29 import org.openjdk.skara.vcs.*; 30 import org.openjdk.skara.vcs.openjdk.Issue; 31 import org.openjdk.skara.vcs.openjdk.*; 32 33 import java.io.*; 34 import java.nio.file.Path; 35 import java.util.*; 36 import java.util.stream.Collectors; 37 38 class PullRequestInstance { 39 private final PullRequest pr; 40 private final Repository localRepo; 41 private final Hash targetHash; 42 private final Hash headHash; 43 private final Hash baseHash; 44 45 PullRequestInstance(Path localRepoPath, PullRequest pr) throws IOException { 46 this.pr = pr; 47 var repository = pr.repository(); 48 49 // Materialize the PR's target ref 50 localRepo = Repository.materialize(localRepoPath, repository.url(), pr.targetRef()); 51 targetHash = localRepo.fetch(repository.url(), pr.targetRef()); 52 headHash = localRepo.fetch(repository.url(), pr.headHash().hex()); 53 baseHash = localRepo.mergeBase(targetHash, headHash); 54 } 55 56 /** 57 * The Review list is in chronological order, the latest one from a particular reviewer is the 58 * one that is "active". 59 * @param allReviews 60 * @return 61 */ 62 static List<Review> filterActiveReviews(List<Review> allReviews) { 63 var reviewPerUser = new LinkedHashMap<HostUser, Review>(); 64 for (var review : allReviews) { 65 reviewPerUser.put(review.reviewer(), review); 66 } 67 return new ArrayList<>(reviewPerUser.values()); 68 } 69 70 private String commitMessage(List<Review> activeReviews, Namespace namespace, boolean isMerge) throws IOException { 71 var reviewers = activeReviews.stream() 72 .filter(review -> review.verdict() == Review.Verdict.APPROVED) 73 .map(review -> review.reviewer().id()) 74 .map(namespace::get) 75 .filter(Objects::nonNull) 76 .map(Contributor::username) 77 .collect(Collectors.toList()); 78 79 var comments = pr.comments(); 80 var additionalContributors = Contributors.contributors(pr.repository().forge().currentUser(), 81 comments).stream() 82 .map(email -> Author.fromString(email.toString())) 83 .collect(Collectors.toList()); 84 85 var additionalIssues = SolvesTracker.currentSolved(pr.repository().forge().currentUser(), comments); 86 var summary = Summary.summary(pr.repository().forge().currentUser(), comments); 87 var issue = Issue.fromString(pr.title()); 88 var commitMessageBuilder = issue.map(CommitMessage::title).orElseGet(() -> CommitMessage.title(isMerge ? "Merge" : pr.title())); 89 if (issue.isPresent()) { 90 commitMessageBuilder.issues(additionalIssues); 91 } 92 commitMessageBuilder.contributors(additionalContributors) 93 .reviewers(reviewers); 94 summary.ifPresent(commitMessageBuilder::summary); 95 96 return String.join("\n", commitMessageBuilder.format(CommitMessageFormatters.v1)); 97 } 98 99 private Hash commitSquashed(List<Review> activeReviews, Namespace namespace, String censusDomain, String sponsorId) throws IOException { 100 localRepo.checkout(baseHash, true); 101 localRepo.squash(headHash); 102 if (localRepo.isClean()) { 103 // There are no changes remaining after squashing 104 return baseHash; 105 } 106 107 Author committer; 108 Author author; 109 var contributor = namespace.get(pr.author().id()); 110 111 if (contributor == null) { 112 // Use the information contained in the head commit - jcheck has verified that it contains sane values 113 var headCommit = localRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0); 114 author = headCommit.author(); 115 } else { 116 author = new Author(contributor.fullName().orElseThrow(), contributor.username() + "@" + censusDomain); 117 } 118 119 if (sponsorId != null) { 120 var sponsorContributor = namespace.get(sponsorId); 121 committer = new Author(sponsorContributor.fullName().orElseThrow(), sponsorContributor.username() + "@" + censusDomain); 122 } else { 123 committer = author; 124 } 125 126 var commitMessage = commitMessage(activeReviews, namespace, false); 127 return localRepo.commit(commitMessage, author.name(), author.email(), committer.name(), committer.email()); 128 } 129 130 private Hash commitMerge(List<Review> activeReviews, Namespace namespace, String censusDomain) throws IOException { 131 localRepo.checkout(headHash, true); 132 133 var contributor = namespace.get(pr.author().id()); 134 if (contributor == null) { 135 throw new RuntimeException("Merges can only be performed by Committers"); 136 } 137 138 var author = new Author(contributor.fullName().orElseThrow(), contributor.username() + "@" + censusDomain); 139 140 var commitMessage = commitMessage(activeReviews, namespace, true); 141 return localRepo.amend(commitMessage, author.name(), author.email(), author.name(), author.email()); 142 } 143 144 Hash commit(Namespace namespace, String censusDomain, String sponsorId) throws IOException { 145 var activeReviews = filterActiveReviews(pr.reviews()); 146 if (!pr.title().startsWith("Merge")) { 147 return commitSquashed(activeReviews, namespace, censusDomain, sponsorId); 148 } else { 149 return commitMerge(activeReviews, namespace, censusDomain); 150 } 151 } 152 153 List<Commit> divergingCommits() { 154 try { 155 return localRepo.commits(baseHash + ".." + targetHash).asList(); 156 } catch (IOException e) { 157 throw new RuntimeException(e); 158 } 159 } 160 161 Optional<Hash> rebase(Hash commitHash, PrintWriter reply) { 162 var divergingCommits = divergingCommits(); 163 if (divergingCommits.size() > 0) { 164 reply.print("The following commits have been pushed to "); 165 reply.print(pr.targetRef()); 166 reply.println(" since your change was applied:"); 167 divergingCommits.forEach(c -> reply.println(" * " + c.hash().hex() + ": " + c.message().get(0))); 168 169 try { 170 var commit = localRepo.lookup(commitHash).orElseThrow(); 171 localRepo.rebase(targetHash, commit.committer().name(), commit.committer().email()); 172 reply.println(); 173 reply.println("Your commit was automatically rebased without conflicts."); 174 var hash = localRepo.head(); 175 return Optional.of(hash); 176 } catch (IOException e) { 177 reply.println(); 178 reply.print("It was not possible to rebase your changes automatically. Please merge `"); 179 reply.print(pr.targetRef()); 180 reply.println("` into your branch and try again."); 181 try { 182 localRepo.checkout(commitHash, true); 183 } catch (IOException e2) { 184 throw new UncheckedIOException(e2); 185 } 186 return Optional.empty(); 187 } 188 } else { 189 // No rebase needed 190 return Optional.of(commitHash); 191 } 192 } 193 194 Repository localRepo() { 195 return this.localRepo; 196 } 197 198 Hash baseHash() { 199 return this.baseHash; 200 } 201 202 Set<Path> changedFiles() throws IOException { 203 var ret = new HashSet<Path>(); 204 var changes = localRepo.diff(baseHash, headHash); 205 for (var patch : changes.patches()) { 206 patch.target().path().ifPresent(ret::add); 207 patch.source().path().ifPresent(ret::add); 208 } 209 return ret; 210 } 211 212 PullRequestCheckIssueVisitor createVisitor(Hash localHash, CensusInstance censusInstance) throws IOException { 213 var checks = JCheck.checks(localRepo(), censusInstance.census(), localHash); 214 return new PullRequestCheckIssueVisitor(checks); 215 } 216 217 void executeChecks(Hash localHash, CensusInstance censusInstance, PullRequestCheckIssueVisitor visitor) throws Exception { 218 try (var issues = JCheck.check(localRepo(), censusInstance.census(), CommitMessageParsers.v1, "HEAD~1..HEAD", 219 localHash, new HashMap<>(), new HashSet<>())) { 220 for (var issue : issues) { 221 issue.accept(visitor); 222 } 223 } 224 } 225 }