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.cli; 24 25 import org.openjdk.skara.args.ArgumentParser; 26 import org.openjdk.skara.args.Input; 27 import org.openjdk.skara.args.Option; 28 import org.openjdk.skara.args.Switch; 29 import org.openjdk.skara.vcs.Repository; 30 31 import java.io.IOException; 32 import java.net.URI; 33 import java.net.URISyntaxException; 34 import java.net.http.HttpClient; 35 import java.net.http.HttpRequest; 36 import java.net.http.HttpResponse; 37 import java.nio.file.Files; 38 import java.nio.file.Path; 39 import java.nio.file.Paths; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 import java.util.regex.Matcher; 44 import java.util.regex.Pattern; 45 46 class GitWImport { 47 48 private static final Pattern findPatchPattern = Pattern.compile( 49 "[ ]*(?:<td>)?<a href=\".*\">(?<patchName>.*\\.patch)</a></td>(?:</tr>)?$"); 50 51 public static void main(String[] args) throws Exception { 52 var flags = List.of( 53 Option.shortcut("n") 54 .fullname("name") 55 .describe("NAME") 56 .helptext("Use NAME as a name for the patch (default is patch file name)") 57 .optional(), 58 Switch.shortcut("f") 59 .fullname("file") 60 .helptext("Input is a file path") 61 .optional(), 62 Switch.shortcut("k") 63 .fullname("keep") 64 .helptext("Keep downloaded patch file in current directory") 65 .optional(), 66 Switch.shortcut("d") 67 .fullname("direct") 68 .helptext("Directly apply patch, without creating a branch or commit") 69 .optional()); 70 71 var inputs = List.of( 72 Input.position(0) 73 .describe("webrev url|file path") 74 .singular() 75 .required()); 76 77 var parser = new ArgumentParser("git wimport", flags, inputs); 78 var arguments = parser.parse(args); 79 80 var inputString = arguments.at(0).asString(); 81 Path patchFile; 82 String patchName; 83 if (arguments.contains("file")) { 84 patchFile = Paths.get(inputString); 85 patchName = arguments.get("name") 86 .or(dropSuffix(patchFile.getFileName().toString(), ".patch")) 87 .asString(); 88 } else { 89 var uri = sanitizeURI(inputString); 90 var remotePatchFile = getPatchFileName(uri); 91 patchName = arguments.get("name") 92 .or(dropSuffix(remotePatchFile, ".patch")) 93 .asString(); 94 patchFile = downloadPatchFile( 95 uri.resolve(remotePatchFile), 96 patchName, 97 arguments.contains("keep")); 98 } 99 100 var cwd = Paths.get("").toAbsolutePath(); 101 var repository = Repository.get(cwd); 102 if (repository.isEmpty()) { 103 System.err.println(String.format("error: %s is not a repository", cwd.toString())); 104 System.exit(1); 105 } 106 var repo = repository.get(); 107 108 if (!check(patchFile)) { 109 System.err.println("Patch does not apply cleanly!"); 110 System.exit(1); 111 } 112 113 if (!arguments.contains("direct")) { 114 System.out.println("Creating branch: " + patchName + ", based on current head: " + repo.head()); 115 var branch = repo.branch(repo.head(), patchName); 116 repo.checkout(branch, false); 117 } 118 119 System.out.println("Applying patch file: " + patchFile); 120 stat(patchFile); 121 apply(patchFile); 122 123 if (!arguments.contains("direct")) { 124 System.out.println("Creating commit for changes"); 125 repo.commit("Imported patch '" + patchName + "'", "wimport", ""); 126 } 127 } 128 129 private static String dropSuffix(String s, String suffix) { 130 if (s.endsWith(suffix)) { 131 s = s.substring(0, s.length() - suffix.length()); 132 } 133 return s; 134 } 135 136 private static URI sanitizeURI(String uri) throws URISyntaxException { 137 uri = dropSuffix(uri, "index.html"); 138 return new URI(uri); 139 } 140 141 private static String getPatchFileName(URI uri) throws IOException, InterruptedException { 142 var client = HttpClient.newHttpClient(); 143 var findPatchFileRcequest = HttpRequest.newBuilder() 144 .uri(uri) 145 .build(); 146 return client.send(findPatchFileRcequest, HttpResponse.BodyHandlers.ofLines()) 147 .body() 148 .map(findPatchPattern::matcher) 149 .filter(Matcher::matches) 150 .findFirst() 151 .map(m -> m.group("patchName")) 152 .orElseThrow(() -> new IllegalStateException("Can not find patch file name in webrev body")); 153 } 154 155 private static Path downloadPatchFile(URI uri, String patchName, boolean keep) throws IOException, InterruptedException { 156 var client = HttpClient.newHttpClient(); 157 var patchFile = Paths.get(patchName + ".patch"); 158 if (keep) { 159 if (Files.exists(patchFile)) { 160 System.err.println("Patch file: " + patchFile + " already exists! Re-using"); 161 return patchFile; 162 } else { 163 Files.createFile(patchFile); 164 } 165 }else { 166 patchFile = Files.createTempFile(patchName, ""); 167 } 168 169 var patchFileRequest = HttpRequest.newBuilder() 170 .uri(uri) 171 .build(); 172 client.send(patchFileRequest, HttpResponse.BodyHandlers.ofFile(patchFile)); 173 return patchFile; 174 } 175 176 private static boolean check(Path patchFile) throws IOException, InterruptedException { 177 return applyInternal(patchFile, "--check", "--index") == 0; 178 } 179 180 private static void stat(Path patchFile) throws IOException, InterruptedException { 181 applyInternal(patchFile, "--stat", "--index"); 182 } 183 184 private static void apply(Path patchFile) throws IOException, InterruptedException { 185 applyInternal(patchFile, "--index"); 186 } 187 188 private static int applyInternal(Path patchFile, String...options) throws IOException, InterruptedException { 189 List<String> args = new ArrayList<>(); 190 args.add("git"); 191 args.add("apply"); 192 args.addAll(Arrays.asList(options)); 193 args.add(patchFile.toString()); 194 var pb = new ProcessBuilder(args.toArray(String[]::new)); 195 pb.inheritIO(); 196 return pb.start().waitFor(); 197 } 198 199 }