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 }