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.host.*;
 26 import org.openjdk.skara.jcheck.*;
 27 import org.openjdk.skara.jcheck.Check;
 28 import org.openjdk.skara.vcs.Hash;
 29 
 30 import java.util.*;
 31 import java.util.logging.Logger;
 32 import java.util.stream.Collectors;
 33 
 34 class PullRequestCheckIssueVisitor implements IssueVisitor {
 35     private final Set<String> messages = new HashSet<>();
 36     private final List<CheckAnnotation> annotations = new LinkedList<>();
 37     private final Set<Check> enabledChecks;
 38     private final Set<Class<? extends Check>> failedChecks = new HashSet<>();
 39 
 40     private boolean readyForReview;
 41 
 42     private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr");
 43 
 44     private final Set<Class<? extends Check>> displayedChecks = Set.of(
 45             DuplicateIssuesCheck.class,
 46             ReviewersCheck.class,
 47             WhitespaceCheck.class,
 48             IssuesCheck.class
 49     );
 50 
 51     PullRequestCheckIssueVisitor(Set<Check> enabledChecks) {
 52         this.enabledChecks = enabledChecks;
 53         readyForReview = true;
 54     }
 55 
 56     List<String> getMessages() {
 57         return new ArrayList<>(messages);
 58     }
 59 
 60     Map<String, Boolean> getChecks() {
 61         return enabledChecks.stream()
 62                             .filter(check -> displayedChecks.contains(check.getClass()))
 63                             .collect(Collectors.toMap(Check::description,
 64                                                       check -> !failedChecks.contains(check.getClass())));
 65     }
 66 
 67     List<CheckAnnotation> getAnnotations() { return annotations; }
 68 
 69     boolean isReadyForReview() {
 70         return readyForReview;
 71     }
 72 
 73     public void visit(DuplicateIssuesIssue e) {
 74         var id = e.issue().id();
 75         var other = e.hashes()
 76                      .stream()
 77                      .map(Hash::abbreviate)
 78                      .map(s -> "         - " + s)
 79                      .collect(Collectors.toList());
 80 
 81         var output = new StringBuilder();
 82         output.append("Issue id ").append(id).append(" is already used in these commits:\n");
 83         other.forEach(h -> output.append(" * ").append(h).append("\n"));
 84         messages.add(output.toString());
 85         failedChecks.add(e.check().getClass());
 86         readyForReview = false;
 87     }
 88 
 89     @Override
 90     public void visit(TagIssue e) {
 91         log.fine("ignored: illegal tag name: " + e.tag().name());
 92     }
 93 
 94     @Override
 95     public void visit(BranchIssue e) {
 96         log.fine("ignored: illegal branch name: " + e.branch().name());
 97     }
 98 
 99     @Override
100     public void visit(SelfReviewIssue e)
101     {
102         messages.add("Self-reviews are not allowed");
103         failedChecks.add(e.check().getClass());
104         readyForReview = false;
105     }
106 
107     @Override
108     public void visit(TooFewReviewersIssue e) {
109         messages.add(String.format("Too few reviewers found (have %d, need at least %d)", e.numActual(), e.numRequired()));
110         failedChecks.add(e.check().getClass());
111     }
112 
113     @Override
114     public void visit(InvalidReviewersIssue e) {
115         log.fine("ignored: invalid reviewers: " + e.invalid());
116     }
117 
118     @Override
119     public void visit(MergeMessageIssue e) {
120         var hex = e.commit().hash().abbreviate();
121         log.fine("ignored: " + hex + ": merge commits should only have commit message 'Merge'");
122     }
123 
124     @Override
125     public void visit(HgTagCommitIssue e) {
126         log.fine("ignored: invalid tag commit");
127     }
128 
129     @Override
130     public void visit(CommitterIssue e) {
131         log.fine("ignored: invalid author: " + e.commit().author().name());
132     }
133 
134     @Override
135     public void visit(CommitterNameIssue issue) {
136         log.fine("ignored: invalid committer name");
137     }
138 
139     @Override
140     public void visit(CommitterEmailIssue issue) {
141         log.fine("ignored: invalid committer email");
142     }
143 
144     @Override
145     public void visit(AuthorNameIssue issue) {
146         log.fine("ignored: invalid author name");
147     }
148 
149     @Override
150     public void visit(AuthorEmailIssue issue) {
151         log.fine("ignored: invalid author email");
152     }
153 
154     @Override
155     public void visit(WhitespaceIssue e) {
156         var startColumn = Integer.MAX_VALUE;
157         var endColumn = Integer.MIN_VALUE;
158         var details = new LinkedList<String>();
159         for (var error : e.errors()) {
160             startColumn = Math.min(error.index(), startColumn);
161             endColumn = Math.max(error.index(), endColumn);
162             details.add("Column " + error.index() + ": " + error.kind().toString());
163         }
164 
165         var annotationBuilder = CheckAnnotationBuilder.create(
166                 e.path().toString(),
167                 e.row(),
168                 e.row(),
169                 CheckAnnotationLevel.FAILURE,
170                 String.join("  \n", details));
171 
172         if (startColumn < Integer.MAX_VALUE) {
173             annotationBuilder.startColumn(startColumn);
174         }
175         if (endColumn > Integer.MIN_VALUE) {
176             annotationBuilder.endColumn(endColumn);
177         }
178 
179         var annotation = annotationBuilder.title("Whitespace error").build();
180         annotations.add(annotation);
181 
182         messages.add("Whitespace errors");
183         failedChecks.add(e.check().getClass());
184         readyForReview = false;
185     }
186 
187     @Override
188     public void visit(MessageIssue issue) {
189         log.fine("ignored: incorrectly formatted commit message");
190     }
191 
192     @Override
193     public void visit(IssuesIssue issue) {
194         messages.add("The commit message does not reference any issue. To add an issue reference to this PR, " +
195                 "edit the title to be of the format <issue number>: <message>.");
196         failedChecks.add(issue.check().getClass());
197         readyForReview = false;
198     }
199 
200     @Override
201     public void visit(ExecutableIssue issue) {
202         messages.add(String.format("Executable files are not allowed (file: %s)", issue.path()));
203         failedChecks.add(issue.check().getClass());
204         readyForReview = false;
205     }
206 
207     @Override
208     public void visit(BlacklistIssue issue) {
209         log.fine("ignored: blacklisted commit");
210     }
211 }