blob: 76f2c0facdea2d1a0272adbf8b52e19363c64cd0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.jackpot.prs.handler.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.reflectoring.diffparser.api.UnifiedDiffParser;
import io.reflectoring.diffparser.api.model.Diff;
import io.reflectoring.diffparser.api.model.Hunk;
import io.reflectoring.diffparser.api.model.Line;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.event.ChangeListener;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.queries.BinaryForSourceQuery;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.Sources;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.api.sendopts.CommandException;
import org.netbeans.modules.jackpot.prs.handler.impl.SiteWrapper.Factory;
import org.netbeans.modules.java.hints.spiimpl.batch.BatchUtilities;
import org.netbeans.modules.java.hints.spiimpl.hints.HintsInvoker;
import org.netbeans.modules.java.hints.spiimpl.options.HintsSettings;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation;
import org.netbeans.spi.sendopts.Env;
import org.netbeans.spi.sendopts.Option;
import org.netbeans.spi.sendopts.OptionProcessor;
import org.openide.LifecycleManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Places;
import org.openide.util.Exceptions;
import org.openide.util.lookup.ServiceProvider;
/**
*
* @author lahvac
*/
@ServiceProvider(service=OptionProcessor.class)
public class HandlePullRequest extends OptionProcessor {
private static final Option HANDLE_PULL_REQUEST = Option.withoutArgument('\0', "handle-pull-request");
public static Factory factory = GHSite.FACTORY;
protected Set<Option> getOptions() {
return Collections.singleton(HANDLE_PULL_REQUEST);
}
protected void process(Env env, Map<Option,String[]> optionValues) throws CommandException {
try {
String content = System.getenv().get("PR_CONTENT");
String token = System.getenv().get("OAUTH_TOKEN");
String appToken = System.getenv().get("OAUTH_APP_TOKEN");
processPullRequest(content, token, appToken);
LifecycleManager.getDefault().exit();
} catch (Throwable ex) {
System.err.println("error:");
ex.printStackTrace();
throw (CommandException) new CommandException(1).initCause(ex);
}
}
public static void processPullRequest(String inputData, String oauthToken, String oauthAppToken) throws Exception {
File tempDir = Places.getCacheSubdirectory("checkout");
Map<String, Object> inputParsed = new ObjectMapper().readValue(inputData, Map.class);
Object action = inputParsed.get("action");
if (!"opened".equals(action))
return ;
Map<String, Object> pullRequest = (Map<String, Object>) inputParsed.get("pull_request");
if (pullRequest == null) {
return ;
}
Map<String, Object> base = (Map<String, Object>) pullRequest.get("base");
Map<String, Object> baseRepo = (Map<String, Object>) base.get("repo");
Map<String, Object> head = (Map<String, Object>) pullRequest.get("head");
Map<String, Object> headRepo = (Map<String, Object>) head.get("repo");
SiteWrapper statusGithub = factory.create(oauthToken);
String fullRepoName = (String) baseRepo.get("full_name");
String sha = (String) head.get("sha");
statusGithub.createCommitStatusPending(fullRepoName, sha, "Running Jackpot verification");
String cloneURL = (String) headRepo.get("clone_url");
new ProcessBuilder("git", "clone", cloneURL, "workdir").directory(tempDir).inheritIO().start().waitFor();
FileObject workdir = FileUtil.toFileObject(new File(tempDir, "workdir"));
new ProcessBuilder("git", "checkout", sha).directory(FileUtil.toFile(workdir)).inheritIO().start().waitFor();
FileObject jlObject = workdir.getFileObject("src/java.base/share/classes/java/lang/Object.java");
if (jlObject != null) {
Project prj = FileOwnerQuery.getOwner(jlObject); //ensure external roots (tests) are registered
ProjectUtils.getSources(prj).getSourceGroups(Sources.TYPE_GENERIC); //register external roots
}
FileObject jlmSourceVersion = workdir.getFileObject("src/java.compiler/share/classes/javax/lang/model/SourceVersion.java");
if (jlmSourceVersion != null) {
Project prj = FileOwnerQuery.getOwner(jlmSourceVersion); //ensure external roots (tests) are registered
ProjectUtils.getSources(prj).getSourceGroups(Sources.TYPE_GENERIC); //register external roots
}
Set<Project> projects = new HashSet<>();
Map<FileObject, FileData> file2Remap = new HashMap<>();
String diffURL = (String) pullRequest.get("diff_url");
URLConnection conn = new URL(diffURL).openConnection();
String text;
try (InputStream in = conn.getInputStream()) {
String encoding = conn.getContentEncoding();
if (encoding == null) encoding = "UTF-8";
//workaround for the diff parser, which does not handle git diffs properly:
text = new String(in.readAllBytes(), encoding)
.replace("\ndiff --git", "\n\ndiff --git");
}
List<Diff> diffs = new UnifiedDiffParser().parse(text.getBytes());
for (Diff diff : diffs) {
String filename = diff.getToFileName().substring(2);
if (filename.endsWith(".java")) {
FileObject file = workdir.getFileObject(filename);
if (file == null) {
//TODO: how to handle? log?
continue;
}
Project project = FileOwnerQuery.getOwner(file);
if (project != null) {
int[] remap = new int[file.asLines().size() + 1]; //TODO: encoding?
Arrays.fill(remap, -1);
int idx = 1;
for (Hunk hunk : diff.getHunks()) {
int pointer = hunk.getToFileRange().getLineStart();
for (Line line : hunk.getLines()) {
switch (line.getLineType()) {
case NEUTRAL: pointer++; idx++; break;
case TO: remap[pointer++] = idx++; break;
}
}
}
projects.add(project);
file2Remap.put(file, new FileData(filename, remap));
} else {
System.err.println("no project found for: " + filename);
}
}
}
int prId = (int) pullRequest.get("number");
SiteWrapper[] commentGitHub = new SiteWrapper[1];
boolean[] hasWarnings = {false};
for (Project project : projects) {
switch (project.getClass().getName()) {//XXX: ensure that the environment variables are dropped here!
case "org.netbeans.modules.maven.NbMavenProjectImpl":
new ProcessBuilder("mvn", "dependency:go-offline").directory(FileUtil.toFile(project.getProjectDirectory())).inheritIO().start().waitFor();
break;
case "org.netbeans.modules.apisupport.project.NbModuleProject":
FileObject nbbuild = project.getProjectDirectory().getFileObject("../../nbbuild");
if (nbbuild == null) {
nbbuild = project.getProjectDirectory().getFileObject("../nbbuild");
}
if (nbbuild != null) { //TODO: only once
new ProcessBuilder("ant", "-autoproxy", "download-all-extbins").directory(FileUtil.toFile(nbbuild)).inheritIO().start().waitFor();
}
//TODO: download extbins!
break;
case "org.netbeans.modules.java.openjdk.project.JDKProject":
//no bootstrap at this time
break;
default:
System.err.println("project name: " + project.getClass().getName());
break;
}
}
OpenProjects.getDefault().open(projects.toArray(new Project[0]), false);
Map<ClasspathInfo, Collection<FileObject>> sorted = BatchUtilities.sortFiles(file2Remap.keySet());
for (Entry<ClasspathInfo, Collection<FileObject>> e : sorted.entrySet()) {
System.err.println("Running hints for:");
System.err.println("files: " + e.getValue());
System.err.println("classpath: " + e.getKey());
try {
JavaSource.create(e.getKey(), e.getValue()).runWhenScanFinished(cc -> {
FileData fileData = file2Remap.get(cc.getFileObject());
if (fileData == null)
return ;
cc.toPhase(JavaSource.Phase.RESOLVED); //XXX
List<ErrorDescription> warnings = new HintsInvoker(HintsSettings.getGlobalSettings(), new AtomicBoolean()).computeHints(cc);
System.err.println("warnings=" + warnings);
for (ErrorDescription ed : warnings) {
int startLine = ed.getRange().getBegin().getLine() + 1;
int targetPosition = fileData.remap[startLine];
if (targetPosition == (-1))
continue;
String comment = "Jackpot:\nwarning: " + ed.getDescription();
//TODO: fixes
// if (additions != null) {
// comment += "```suggestion\n" + additions.toString() + "```";
// }
hasWarnings[0] = true;
if (commentGitHub[0] == null) {
commentGitHub[0] = factory.create(oauthAppToken);
}
commentGitHub[0].createReviewComment(fullRepoName, prId, comment, sha, fileData.filename, targetPosition);
}
}, false).get();
} catch (Throwable ex) {
System.err.println("error while processing: " + e.getValue());
Exceptions.printStackTrace(ex);
}
}
// String file = (String) m.get("file");
// int startLine = (Integer) m.get("startLine");
// List<String> fixes = (List<String>) m.get("fixes");
// StringBuilder additions = null;
// if (fixes != null && fixes.size() > 1) { //TODO: or == 1?
// List<Diff> fixDiffs = new UnifiedDiffParser().parse(fixes.get(0).getBytes("UTF-8"));
// if (fixDiffs.size() == 1 && fixDiffs.get(0).getHunks().size() == 1) {
// int start = fixDiffs.get(0).getHunks().get(0).getToFileRange().getLineStart();
// additions = new StringBuilder();
// boolean seenRemoval = false;
// for (Line line : fixDiffs.get(0).getHunks().get(0).getLines()) {
// if (line.getLineType() == Line.LineType.FROM) {
// if (seenRemoval) {
// start = -1;
// break;
// } else {
// seenRemoval = true;
// }
// } else if (line.getLineType() == Line.LineType.TO) {
// additions.append(line.getContent());
// additions.append("\n");
// }
// }
// if (start != (-1) && seenRemoval) {
// startLine = start;
// } else {
// additions = null;
// }
// }
// }
// }
// }
//
String mainComment;
if (!hasWarnings[0]) {
mainComment = "Jackpot: no warnings.";
} else {
mainComment = "Jackpot: warnings found.";
}
//TODO: set status on crash/error!
statusGithub.createCommitStatusSuccess(fullRepoName, sha, mainComment);
}
private static final class FileData {
public final String filename;
public final int[] remap;
public FileData(String filename, int[] remap) {
this.filename = filename;
this.remap = remap;
}
}
//if java.base/share/classes indexing fails, copy classfiles from the default JDK platform
//these will typically not match the sources, but if the classfiles would be missing
//altogether, all subsequent indexing would fail as well:
@ServiceProvider(service=BinaryForSourceQueryImplementation.class, position=0)
public static class BinaryForSourceQueryImpl implements BinaryForSourceQueryImplementation {
@Override
public BinaryForSourceQuery.Result findBinaryRoots(URL sourceRoot) {
if (sourceRoot.toString().contains("src/java.base/share/classes")) {
return new BinaryForSourceQuery.Result() {
@Override
public URL[] getRoots() {
return JavaPlatform.getDefault().getBootstrapLibraries().entries().stream().map(e -> e.getURL()).filter(u -> "java.base".equals(SourceUtils.getModuleName(u))).toArray(s -> new URL[s]);
}
@Override
public void addChangeListener(ChangeListener l) {
}
@Override
public void removeChangeListener(ChangeListener l) {
}
};
}
return null;
}
}
}