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.forge.*;
 26 import org.openjdk.skara.host.*;
 27 import org.openjdk.skara.issuetracker.*;
 28 import org.openjdk.skara.vcs.Hash;
 29 
 30 import java.io.*;
 31 import java.nio.charset.StandardCharsets;
 32 import java.nio.file.Path;
 33 import java.security.*;
 34 import java.time.*;
 35 import java.util.*;
 36 import java.util.function.Consumer;
 37 import java.util.logging.Logger;
 38 import java.util.regex.Pattern;
 39 import java.util.stream.Collectors;
 40 
 41 class CheckWorkItem extends PullRequestWorkItem {
 42     private final HostedRepository censusRepo;
 43     private final String censusRef;
 44     private final Map<String, String> blockingLabels;
 45     private final IssueProject issueProject;
 46 
 47     private final Pattern metadataComments = Pattern.compile("<!-- (?:(add|remove) contributor)|(?:summary: ')|(?:solves: ')");
 48     private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr");
 49 
 50     CheckWorkItem(PullRequest pr, HostedRepository censusRepo, String censusRef, Map<String, String> blockingLabels,
 51                   Consumer<RuntimeException> errorHandler, IssueProject issueProject) {
 52         super(pr, errorHandler);
 53         this.censusRepo = censusRepo;
 54         this.censusRef = censusRef;
 55         this.blockingLabels = blockingLabels;
 56         this.issueProject = issueProject;
 57     }
 58 
 59     private String encodeReviewer(HostUser reviewer, CensusInstance censusInstance) {
 60         var census = censusInstance.census();
 61         var project = censusInstance.project();
 62         var namespace = censusInstance.namespace();
 63         var contributor = namespace.get(reviewer.id());
 64         if (contributor == null) {
 65             return "unknown-" + reviewer.id();
 66         } else {
 67             var censusVersion = census.version().format();
 68             var userName = contributor.username();
 69             return contributor.username() + project.isLead(userName, censusVersion) +
 70                     project.isReviewer(userName, censusVersion) + project.isCommitter(userName, censusVersion) +
 71                     project.isAuthor(userName, censusVersion);
 72         }
 73     }
 74 
 75     String getMetadata(String title, String body, List<Comment> comments, List<Review> reviews, Set<String> labels,
 76                        CensusInstance censusInstance, Hash target) {
 77         try {
 78             var approverString = reviews.stream()
 79                                         .filter(review -> review.verdict() == Review.Verdict.APPROVED)
 80                                         .map(review -> encodeReviewer(review.reviewer(), censusInstance) + review.hash().hex())
 81                                         .sorted()
 82                                         .collect(Collectors.joining());
 83             var commentString = comments.stream()
 84                                         .filter(comment -> comment.author().id().equals(pr.repository().forge().currentUser().id()))
 85                                         .flatMap(comment -> comment.body().lines())
 86                                         .filter(line -> metadataComments.matcher(line).find())
 87                                         .collect(Collectors.joining());
 88             var labelString = labels.stream()
 89                                     .sorted()
 90                                     .collect(Collectors.joining());
 91             var digest = MessageDigest.getInstance("SHA-256");
 92             digest.update(title.getBytes(StandardCharsets.UTF_8));
 93             digest.update(body.getBytes(StandardCharsets.UTF_8));
 94             digest.update(approverString.getBytes(StandardCharsets.UTF_8));
 95             digest.update(commentString.getBytes(StandardCharsets.UTF_8));
 96             digest.update(labelString.getBytes(StandardCharsets.UTF_8));
 97             digest.update(target.hex().getBytes(StandardCharsets.UTF_8));
 98 
 99             return Base64.getUrlEncoder().encodeToString(digest.digest());
100         } catch (NoSuchAlgorithmException e) {
101             throw new RuntimeException("Cannot find SHA-256");
102         }
103     }
104 
105     private boolean currentCheckValid(CensusInstance censusInstance, List<Comment> comments, List<Review> reviews, Set<String> labels) {
106         var hash = pr.headHash();
107         var targetHash = pr.targetHash();
108         var metadata = getMetadata(pr.title(), pr.body(), comments, reviews, labels, censusInstance, targetHash);
109         var currentChecks = pr.checks(hash);
110 
111         if (currentChecks.containsKey("jcheck")) {
112             var check = currentChecks.get("jcheck");
113             // Check if the currently running check seems stale - perhaps the checker failed to complete
114             if (check.completedAt().isEmpty()) {
115                 var runningTime = Duration.between(check.startedAt().toInstant(), Instant.now());
116                 if (runningTime.toMinutes() > 10) {
117                     log.warning("Previous jcheck running for more than 10 minutes - checking again");
118                 } else {
119                     log.finer("Jcheck in progress for " + runningTime.toMinutes() + " minutes, not starting another one");
120                     return true;
121                 }
122             } else {
123                 if (check.metadata().isPresent() && check.metadata().get().equals(metadata)) {
124                     log.finer("No activity since last check, not checking again");
125                     return true;
126                 } else {
127                     log.info("PR updated after last check, checking again");
128                     if (check.metadata().isPresent() && (!check.metadata().get().equals(metadata))) {
129                         log.fine("Previous metadata: " + check.metadata().get() + " - current: " + metadata);
130                     }
131                 }
132             }
133         }
134 
135         return false;
136     }
137 
138     @Override
139     public String toString() {
140         return "CheckWorkItem@" + pr.repository().name() + "#" + pr.id();
141     }
142 
143     @Override
144     public void run(Path scratchPath) {
145         // First determine if the current state of the PR has already been checked
146         var census = CensusInstance.create(censusRepo, censusRef, scratchPath.resolve("census"), pr);
147         var comments = pr.comments();
148         var allReviews = pr.reviews();
149         var labels = new HashSet<>(pr.labels());
150 
151         // Filter out the active reviews
152         var activeReviews = PullRequestInstance.filterActiveReviews(allReviews);
153         if (!currentCheckValid(census, comments, activeReviews, labels)) {
154             if (labels.contains("integrated")) {
155                 log.info("Skipping check of integrated PR");
156                 return;
157             }
158 
159             try {
160                 var prInstance = new PullRequestInstance(scratchPath.resolve("pr"), pr);
161                 CheckRun.execute(this, pr, prInstance, comments, allReviews, activeReviews, labels, census,
162                                  blockingLabels, issueProject);
163             } catch (IOException e) {
164                 throw new UncheckedIOException(e);
165             }
166         }
167     }
168 }