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