diff --git a/args/src/main/java/org/openjdk/skara/args/Command.java b/args/src/main/java/org/openjdk/skara/args/Command.java new file mode 100644 index 000000000..7c15720a4 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/Command.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +public class Command implements Main { + private final String name; + private final String helpText; + private final Main main; + + Command(String name, String helpText, Main main) { + this.name = name; + this.helpText = helpText; + this.main = main; + } + + public String name() { + return name; + } + + public String helpText() { + return helpText; + } + + public Main main() { + return main; + } + + public static CommandHelpText name(String name) { + return new CommandHelpText<>(Command::new, name); + } + + @Override + public void main(String[] args) throws Exception { + main.main(args); + } +} diff --git a/args/src/main/java/org/openjdk/skara/args/CommandCtor.java b/args/src/main/java/org/openjdk/skara/args/CommandCtor.java new file mode 100644 index 000000000..33a807a20 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/CommandCtor.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +public interface CommandCtor { + T construct(String name, String helpText, Main main); +} diff --git a/args/src/main/java/org/openjdk/skara/args/CommandHelpText.java b/args/src/main/java/org/openjdk/skara/args/CommandHelpText.java new file mode 100644 index 000000000..0cffbdc4a --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/CommandHelpText.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +public class CommandHelpText { + private final CommandCtor ctor; + private final String name; + + CommandHelpText(CommandCtor ctor, String name) { + this.ctor = ctor; + this.name = name; + } + + public CommandMain helptext(String helpText) { + return new CommandMain<>(ctor, name, helpText); + } +} diff --git a/args/src/main/java/org/openjdk/skara/args/CommandMain.java b/args/src/main/java/org/openjdk/skara/args/CommandMain.java new file mode 100644 index 000000000..e489cd759 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/CommandMain.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +public class CommandMain { + private final CommandCtor ctor; + private final String name; + private final String helpText; + + CommandMain(CommandCtor ctor, String name, String helpText) { + this.ctor = ctor; + this.name = name; + this.helpText = helpText; + } + + public T main(Main main) { + return ctor.construct(name, helpText, main); + } +} diff --git a/args/src/main/java/org/openjdk/skara/args/Default.java b/args/src/main/java/org/openjdk/skara/args/Default.java new file mode 100644 index 000000000..c7f89c6e6 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/Default.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +public class Default extends Command { + Default(String name, String helpText, Main main) { + super(name, helpText, main); + } + + public static CommandHelpText name(String name) { + return new CommandHelpText<>(Default::new, name); + } +} diff --git a/args/src/main/java/org/openjdk/skara/args/Executable.java b/args/src/main/java/org/openjdk/skara/args/Executable.java new file mode 100644 index 000000000..9d72ca73c --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/Executable.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +@FunctionalInterface +public interface Executable { + void execute() throws Exception; +} diff --git a/args/src/main/java/org/openjdk/skara/args/Main.java b/args/src/main/java/org/openjdk/skara/args/Main.java new file mode 100644 index 000000000..ad61c58c9 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/Main.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +@FunctionalInterface +public interface Main { + void main(String[] args) throws Exception; +} diff --git a/args/src/main/java/org/openjdk/skara/args/MultiCommandParser.java b/args/src/main/java/org/openjdk/skara/args/MultiCommandParser.java new file mode 100644 index 000000000..379991308 --- /dev/null +++ b/args/src/main/java/org/openjdk/skara/args/MultiCommandParser.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.args; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MultiCommandParser { + private final String programName; + private final String defaultCommand; + private final Map subCommands; + + public MultiCommandParser(String programName, List commands) { + var defaults = commands.stream().filter(Default.class::isInstance).collect(Collectors.toList()); + if (defaults.size() != 1) { + throw new IllegalArgumentException("Expecting exactly one default command"); + } + this.defaultCommand = defaults.get(0).name(); + + this.programName = programName; + this.subCommands = commands.stream() + .collect(Collectors.toMap( + Command::name, + Function.identity())); + this.subCommands.put("help", helpCommand()); + } + + private Command helpCommand() { + return new Command("help", "print a help message", args -> showUsage()); + } + + public Executable parse(String[] args) { + if (args.length > 0) { + var p = subCommands.get(args[0]); + if (p != null) { + var forwardedArgs = Arrays.copyOfRange(args, 1, args.length); + return () -> p.main(forwardedArgs); + } + } + return () -> subCommands.get(defaultCommand).main(args); + } + + private void showUsage() { + showUsage(System.out); + } + + private void showUsage(PrintStream ps) { + ps.print("usage: "); + ps.print(programName); + ps.print(subCommands.keySet().stream().collect(Collectors.joining("|", " <", ">"))); + ps.println(" "); + + int spacing = subCommands.keySet().stream().mapToInt(String::length).max().orElse(0); + spacing += 8; // some room + + for (var subCommand : subCommands.values()) { + ps.println(String.format(" %-" + spacing + "s%s", subCommand.name(), subCommand.helpText())); + } + } +} diff --git a/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java b/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java index a37fb4c8b..c7b79c25f 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java @@ -22,6 +22,7 @@ */ package org.openjdk.skara.cli; +import org.openjdk.skara.args.Main; import org.openjdk.skara.vcs.Repository; import java.io.IOException; @@ -32,15 +33,10 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.function.Consumer; public class GitSkara { - @FunctionalInterface - private interface Command { - void execute(String[] args) throws Exception; - } - private static final Map commands = new TreeMap<>(); + private static final Map commands = new TreeMap<>(); private static void usage(String[] args) { var names = new ArrayList(); @@ -127,7 +123,7 @@ public static void main(String[] args) throws Exception { var command = isEmpty ? "help" : args[0]; var commandArgs = isEmpty ? new String[0] : Arrays.copyOfRange(args, 1, args.length); if (commands.containsKey(command)) { - commands.get(command).execute(commandArgs); + commands.get(command).main(commandArgs); } else { System.err.println("error: unknown command: " + command); usage(args); diff --git a/cli/src/main/java/org/openjdk/skara/cli/GitWebrev.java b/cli/src/main/java/org/openjdk/skara/cli/GitWebrev.java index 0b901ab96..a5b85a4d0 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitWebrev.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitWebrev.java @@ -23,16 +23,18 @@ package org.openjdk.skara.cli; import org.openjdk.skara.args.*; +import org.openjdk.skara.proxy.HttpProxy; import org.openjdk.skara.vcs.*; import org.openjdk.skara.webrev.*; import java.io.*; import java.net.URI; import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.file.*; import java.util.*; -import java.util.jar.Manifest; -import java.util.stream.*; import java.util.regex.Pattern; public class GitWebrev { @@ -79,7 +81,7 @@ private static Hash resolve(ReadOnlyRepository repo, String ref) { } } - public static void main(String[] args) throws IOException { + private static void generate(String[] args) throws IOException { var flags = List.of( Option.shortcut("r") .fullname("rev") @@ -251,4 +253,56 @@ public static void main(String[] args) throws IOException { .version(version) .generate(rev); } + + private static void apply(String[] args) throws Exception { + var inputs = List.of( + Input.position(0) + .describe("webrev url") + .singular() + .required()); + + var parser = new ArgumentParser("git webrev apply", List.of(), inputs); + var arguments = parser.parse(args); + + var cwd = Paths.get("").toAbsolutePath(); + var repository = Repository.get(cwd).orElseGet(() -> { + System.err.println(String.format("error: %s is not a repository", cwd.toString())); + System.exit(1); + return null; + }); + + var inputString = arguments.at(0).asString(); + var webrevMetaData = WebrevMetaData.from(URI.create(inputString)); + var patchFileURI = webrevMetaData.patchURI() + .orElseThrow(() -> new IllegalStateException("Could not find patch file in webrev")); + var patchFile = downloadPatchFile(patchFileURI); + + repository.apply(patchFile, false); + } + + private static Path downloadPatchFile(URI uri) throws IOException, InterruptedException { + var client = HttpClient.newHttpClient(); + var patchFile = Files.createTempFile("patch", ".patch"); + var patchFileRequest = HttpRequest.newBuilder() + .uri(uri) + .build(); + client.send(patchFileRequest, HttpResponse.BodyHandlers.ofFile(patchFile)); + return patchFile; + } + + public static void main(String[] args) throws Exception { + var commands = List.of( + Default.name("generate") + .helptext("generate a webrev") + .main(GitWebrev::generate), + Command.name("apply") + .helptext("apply a webrev from a webrev url") + .main(GitWebrev::apply) + ); + HttpProxy.setup(); + + var parser = new MultiCommandParser("git webrev", commands); + var command = parser.parse(args); + command.execute(); + } } diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java index 91cdec7f5..3a9d07d61 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/Repository.java @@ -101,6 +101,7 @@ Hash amend(String message, void addRemote(String name, String path) throws IOException; void setPaths(String remote, String pullPath, String pushPath) throws IOException; void apply(Diff diff, boolean force) throws IOException; + void apply(Path patchFile, boolean force) throws IOException; void copy(Path from, Path to) throws IOException; void move(Path from, Path to) throws IOException; default void setPaths(String remote, String pullPath) throws IOException { diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java b/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java index 2d8f0e17b..0a87a4008 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/git/GitRepository.java @@ -963,6 +963,12 @@ public void apply(Diff diff, boolean force) throws IOException { // ignore force, no such concept in git var patchFile = Files.createTempFile("apply", ".patch"); diff.toFile(patchFile); + apply(patchFile, force); + Files.delete(patchFile); + } + + @Override + public void apply(Path patchFile, boolean force) throws IOException { var cmd = new ArrayList(); cmd.addAll(List.of("git", "apply", "--index", "--unidiff-zero")); cmd.add(patchFile.toAbsolutePath().toString()); diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java b/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java index 968bcb390..bbbdf8995 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/hg/HgRepository.java @@ -936,6 +936,12 @@ private void setPermissions(Patch.Info target) throws IOException { public void apply(Diff diff, boolean force) throws IOException { var patchFile = Files.createTempFile("import", ".patch"); diff.toFile(patchFile); + apply(patchFile, force); + Files.delete(patchFile); + } + + @Override + public void apply(Path patchFile, boolean force) throws IOException { var cmd = new ArrayList(); cmd.addAll(List.of("hg", "import", "--no-commit")); if (force) { @@ -945,7 +951,6 @@ public void apply(Diff diff, boolean force) throws IOException { try (var p = capture(cmd)) { await(p); } - //Files.delete(patchFile); } @Override diff --git a/webrev/src/main/java/module-info.java b/webrev/src/main/java/module-info.java index ef3de43aa..95a8879ef 100644 --- a/webrev/src/main/java/module-info.java +++ b/webrev/src/main/java/module-info.java @@ -22,6 +22,7 @@ */ module org.openjdk.skara.webrev { requires org.openjdk.skara.vcs; + requires java.net.http; exports org.openjdk.skara.webrev; } diff --git a/webrev/src/main/java/org/openjdk/skara/webrev/WebrevMetaData.java b/webrev/src/main/java/org/openjdk/skara/webrev/WebrevMetaData.java new file mode 100644 index 000000000..e2aeb799b --- /dev/null +++ b/webrev/src/main/java/org/openjdk/skara/webrev/WebrevMetaData.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.skara.webrev; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WebrevMetaData { + private static final Pattern findPatchPattern = Pattern.compile( + "[ ]*(?:)?(?.*\\.patch)(?:)?$"); + + private final Optional patchURI; + + public WebrevMetaData(Optional patchURI) { + this.patchURI = patchURI; + } + + public static WebrevMetaData from(URI uri) throws IOException, URISyntaxException, InterruptedException { + var sanatizedUri = sanitizeURI(uri); + var patchFile = getPatchFile(sanatizedUri); + + return new WebrevMetaData(patchFile); + } + + private static String dropSuffix(String s, String suffix) { + if (s.endsWith(suffix)) { + s = s.substring(0, s.length() - suffix.length()); + } + return s; + } + + private static URI sanitizeURI(URI uri) throws URISyntaxException { + var path = dropSuffix(uri.getPath(), "index.html"); + return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), + path, uri.getQuery(), uri.getFragment()); + } + + private static Optional getPatchFile(URI uri) throws IOException, InterruptedException { + var client = HttpClient.newHttpClient(); + var findPatchFileRcequest = HttpRequest.newBuilder() + .uri(uri) + .build(); + return client.send(findPatchFileRcequest, HttpResponse.BodyHandlers.ofLines()) + .body() + .map(findPatchPattern::matcher) + .filter(Matcher::matches) + .findFirst() + .map(m -> m.group("patchName")) + .map(uri::resolve); + } + + public Optional patchURI() { + return patchURI; + } +}