diff --git a/cli/build.gradle b/cli/build.gradle index f2e467162..2d41791ae 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -62,7 +62,8 @@ images { 'git-info': 'org.openjdk.skara.cli/org.openjdk.skara.cli.GitInfo', 'git-translate': 'org.openjdk.skara.cli/org.openjdk.skara.cli.GitTranslate', 'git-skara': 'org.openjdk.skara.cli/org.openjdk.skara.cli.GitSkara', - 'hg-openjdk-import': 'org.openjdk.skara.cli/org.openjdk.skara.cli.HgOpenJDKImport' + 'hg-openjdk-import': 'org.openjdk.skara.cli/org.openjdk.skara.cli.HgOpenJDKImport', + 'git-sync': 'org.openjdk.skara.cli/org.openjdk.skara.cli.GitSync' ] ext.modules = ['jdk.crypto.ec'] diff --git a/cli/src/main/java/org/openjdk/skara/cli/GitFork.java b/cli/src/main/java/org/openjdk/skara/cli/GitFork.java index eebabd020..ee5a0d65a 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitFork.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitFork.java @@ -29,7 +29,7 @@ import java.io.IOException; import java.net.URI; -import java.nio.file.Path; +import java.nio.file.*; import java.util.List; import java.util.function.Supplier; import java.util.logging.Level; @@ -175,6 +175,14 @@ public static void main(String[] args) throws IOException { upstreamUrl = "git+" + upstreamUrl; } repo.addRemote("upstream", upstreamUrl); + var gitConfig = repo.root().resolve(".git").resolve("config"); + if (!isMercurial && Files.exists(gitConfig)) { + var lines = List.of( + "[sync]", + " remote = upstream" + ); + Files.write(gitConfig, lines, StandardOpenOption.WRITE, StandardOpenOption.APPEND); + } System.out.println("done"); } else { System.out.println(webUrl); diff --git a/cli/src/main/java/org/openjdk/skara/cli/GitPr.java b/cli/src/main/java/org/openjdk/skara/cli/GitPr.java index e51017bd8..75a491e3d 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitPr.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitPr.java @@ -212,46 +212,6 @@ private static void apply(Path patch) throws IOException { await(pb.start()); } - private static URI toURI(String remotePath) throws IOException { - if (remotePath.startsWith("git+")) { - remotePath = remotePath.substring("git+".length()); - } - if (remotePath.startsWith("http")) { - return URI.create(remotePath); - } else { - if (remotePath.startsWith("ssh://")) { - remotePath = remotePath.substring("ssh://".length()).replaceFirst("/", ":"); - } - var indexOfColon = remotePath.indexOf(':'); - var indexOfSlash = remotePath.indexOf('/'); - if (indexOfColon != -1) { - if (indexOfSlash == -1 || indexOfColon < indexOfSlash) { - var path = remotePath.contains("@") ? remotePath.split("@")[1] : remotePath; - var name = path.split(":")[0]; - - // Could be a Host in the ~/.ssh/config file - var sshConfig = Path.of(System.getProperty("user.home"), ".ssh", "config"); - if (Files.exists(sshConfig)) { - for (var host : SSHConfig.parse(sshConfig).hosts()) { - if (host.name().equals(name)) { - var hostName = host.hostName(); - if (hostName != null) { - return URI.create("https://" + hostName + "/" + path.split(":")[1]); - } - } - } - } - - // Otherwise is must be a domain - return URI.create("https://" + path.replace(":", "/")); - } - } - } - - exit("error: cannot find remote repository for " + remotePath); - return null; // will never reach here - } - private static int longest(List strings) { return strings.stream().mapToInt(String::length).max().orElse(0); } @@ -347,7 +307,7 @@ public static void main(String[] args) throws IOException, InterruptedException var remotePullPath = repo.pullPath(remote); var username = arguments.contains("username") ? arguments.get("username").asString() : null; var token = isMercurial ? System.getenv("HG_TOKEN") : System.getenv("GIT_TOKEN"); - var uri = toURI(remotePullPath); + var uri = Remote.toWebURI(remotePullPath); var credentials = GitCredentials.fill(uri.getHost(), uri.getPath(), username, token, uri.getScheme()); var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password())); 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 c7b79c25f..237ce160d 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitSkara.java @@ -116,6 +116,7 @@ public static void main(String[] args) throws Exception { commands.put("token", GitToken::main); commands.put("info", GitInfo::main); commands.put("translate", GitTranslate::main); + commands.put("sync", GitSync::main); commands.put("update", GitSkara::update); commands.put("help", GitSkara::usage); diff --git a/cli/src/main/java/org/openjdk/skara/cli/GitSync.java b/cli/src/main/java/org/openjdk/skara/cli/GitSync.java new file mode 100644 index 000000000..767b248d5 --- /dev/null +++ b/cli/src/main/java/org/openjdk/skara/cli/GitSync.java @@ -0,0 +1,182 @@ +/* + * 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.cli; + +import org.openjdk.skara.args.*; +import org.openjdk.skara.vcs.*; + +import java.io.*; +import java.net.URI; +import java.nio.file.*; +import java.util.*; +import java.util.logging.*; + +public class GitSync { + private static IOException die(String message) { + System.err.println(message); + System.exit(1); + return new IOException("will never reach here"); + } + + private static int fetch() throws IOException, InterruptedException { + var pb = new ProcessBuilder("git", "fetch"); + pb.inheritIO(); + return pb.start().waitFor(); + } + + private static int pull() throws IOException, InterruptedException { + var pb = new ProcessBuilder("git", "pull"); + pb.inheritIO(); + return pb.start().waitFor(); + } + + public static void main(String[] args) throws IOException, InterruptedException { + var flags = List.of( + Option.shortcut("") + .fullname("from") + .describe("REMOTE") + .helptext("Fetch changes from this remote") + .optional(), + Option.shortcut("") + .fullname("to") + .describe("REMOTE") + .helptext("Push changes to this remote") + .optional(), + Option.shortcut("") + .fullname("branches") + .describe("BRANCHES") + .helptext("Comma separated list of branches to sync") + .optional(), + Switch.shortcut("") + .fullname("pull") + .helptext("Pull current branch from origin after successful sync") + .optional(), + Switch.shortcut("") + .fullname("fetch") + .helptext("Fetch current branch from origin after successful sync") + .optional(), + Switch.shortcut("m") + .fullname("mercurial") + .helptext("Force use of mercurial") + .optional(), + Switch.shortcut("") + .fullname("verbose") + .helptext("Turn on verbose output") + .optional(), + Switch.shortcut("") + .fullname("debug") + .helptext("Turn on debugging output") + .optional(), + Switch.shortcut("v") + .fullname("version") + .helptext("Print the version of this tool") + .optional() + ); + + var parser = new ArgumentParser("git sync", flags); + var arguments = parser.parse(args); + + if (arguments.contains("version")) { + System.out.println("git-sync version: " + Version.fromManifest().orElse("unknown")); + System.exit(0); + } + + if (arguments.contains("verbose") || arguments.contains("debug")) { + var level = arguments.contains("debug") ? Level.FINER : Level.FINE; + Logging.setup(level); + } + + var cwd = Paths.get("").toAbsolutePath(); + var repo = Repository.get(cwd).orElseThrow(() -> + die("error: no repository found at " + cwd.toString()) + ); + + var remotes = repo.remotes(); + + String upstream = null; + if (arguments.contains("from")) { + upstream = arguments.get("from").asString(); + } else { + var lines = repo.config("sync.from"); + if (lines.size() == 1 && remotes.contains(lines.get(0))) { + upstream = lines.get(0); + } else { + die("No remote provided to fetch from, please set the --from flag"); + } + } + var upstreamPullPath = remotes.contains(upstream) ? + Remote.toURI(repo.pullPath(upstream)) : URI.create(upstream); + + String origin = null; + if (arguments.contains("to")) { + origin = arguments.get("to").asString(); + } else { + var lines = repo.config("sync.to"); + if (lines.size() == 1) { + if (!remotes.contains(lines.get(0))) { + die("The given remote to push to, " + lines.get(0) + ", does not exist"); + } else { + origin = lines.get(0); + } + } else { + origin = "origin"; + } + } + var originPushPath = Remote.toURI(repo.pushPath(origin)); + + var branches = new HashSet(); + if (arguments.contains("branches")) { + var requested = arguments.get("branches").asString().split(","); + for (var branch : requested) { + branches.add(branch.trim()); + } + } + + for (var branch : repo.remoteBranches(upstream)) { + var name = branch.name(); + if (!branches.isEmpty() && !branches.contains(name)) { + System.out.println("Skipping branch " + name); + continue; + } + System.out.print("Syncing " + upstream + "/" + name + " to " + origin + "/" + name + "... "); + System.out.flush(); + var fetchHead = repo.fetch(upstreamPullPath, branch.hash().hex()); + repo.push(fetchHead, originPushPath, name); + System.out.println("done"); + } + + if (arguments.contains("fetch")) { + int err = fetch(); + if (err != 0) { + System.exit(err); + } + } + + if (arguments.contains("pull")) { + int err = pull(); + if (err != 0) { + System.exit(err); + } + } + } +} diff --git a/cli/src/main/java/org/openjdk/skara/cli/Remote.java b/cli/src/main/java/org/openjdk/skara/cli/Remote.java new file mode 100644 index 000000000..334f597cd --- /dev/null +++ b/cli/src/main/java/org/openjdk/skara/cli/Remote.java @@ -0,0 +1,117 @@ +/* + * 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.cli; + +import org.openjdk.skara.ssh.SSHConfig; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Files; + +class Remote { + public static URI toWebURI(String remotePath) throws IOException { + if (remotePath.startsWith("git+")) { + remotePath = remotePath.substring("git+".length()); + } + if (remotePath.startsWith("http")) { + return URI.create(remotePath); + } else { + if (remotePath.startsWith("ssh://")) { + remotePath = remotePath.substring("ssh://".length()).replaceFirst("/", ":"); + } + var indexOfColon = remotePath.indexOf(':'); + var indexOfSlash = remotePath.indexOf('/'); + if (indexOfColon != -1) { + if (indexOfSlash == -1 || indexOfColon < indexOfSlash) { + var path = remotePath.contains("@") ? remotePath.split("@")[1] : remotePath; + var name = path.split(":")[0]; + + // Could be a Host in the ~/.ssh/config file + var sshConfig = Path.of(System.getProperty("user.home"), ".ssh", "config"); + if (Files.exists(sshConfig)) { + for (var host : SSHConfig.parse(sshConfig).hosts()) { + if (host.name().equals(name)) { + var hostName = host.hostName(); + if (hostName != null) { + return URI.create("https://" + hostName + "/" + path.split(":")[1]); + } + } + } + } + + // Otherwise is must be a domain + return URI.create("https://" + path.replace(":", "/")); + } + } + } + + throw new IOException("error: cannot find remote repository for " + remotePath); + } + + public static URI toURI(String remotePath) throws IOException { + if (remotePath.startsWith("git+")) { + remotePath = remotePath.substring("git+".length()); + } + if (remotePath.startsWith("http://") || + remotePath.startsWith("https://") || + remotePath.startsWith("ssh://") || + remotePath.startsWith("file://") || + remotePath.startsWith("git://")) { + return URI.create(remotePath); + } + + var indexOfColon = remotePath.indexOf(':'); + var indexOfSlash = remotePath.indexOf('/'); + if (indexOfColon != -1) { + if (indexOfSlash == -1 || indexOfColon < indexOfSlash) { + var path = remotePath.contains("@") ? remotePath.split("@")[1] : remotePath; + var name = path.split(":")[0]; + + // Could be a Host in the ~/.ssh/config file + var sshConfig = Path.of(System.getProperty("user.home"), ".ssh", "config"); + if (Files.exists(sshConfig)) { + for (var host : SSHConfig.parse(sshConfig).hosts()) { + if (host.name().equals(name)) { + var hostName = host.hostName(); + if (hostName != null) { + var username = host.user(); + if (username == null) { + username = remotePath.contains("@") ? remotePath.split("@")[0] : null; + } + var userPrefix = username == null ? "" : username + "@"; + return URI.create("ssh://" + userPrefix + hostName + "/" + path.split(":")[1]); + } + } + } + } + + // Otherwise is must be a domain + var userPrefix = remotePath.contains("@") ? remotePath.split("@")[0] + "@" : ""; + return URI.create("ssh://" + userPrefix + path.replace(":", "/")); + } + } + + throw new IOException("error: cannot construct proper URI for " + remotePath); + } +} diff --git a/jcheck/src/test/java/org/openjdk/skara/jcheck/TestRepository.java b/jcheck/src/test/java/org/openjdk/skara/jcheck/TestRepository.java index 6192bdd98..12f89ea0b 100644 --- a/jcheck/src/test/java/org/openjdk/skara/jcheck/TestRepository.java +++ b/jcheck/src/test/java/org/openjdk/skara/jcheck/TestRepository.java @@ -224,4 +224,12 @@ public List status(Hash from, Hash to) throws IOException { public boolean contains(Branch b, Hash h) throws IOException { return false; } + + public List remoteBranches(String remote) throws IOException { + return null; + } + + public List remotes() throws IOException { + return null; + } } diff --git a/skara.gitconfig b/skara.gitconfig index f2dec2ab6..3e152333d 100644 --- a/skara.gitconfig +++ b/skara.gitconfig @@ -31,3 +31,4 @@ token = ! git skara token info = ! git skara info translate = ! git skara translate + sync = ! git skara sync diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java b/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java index ecfb86bd6..6f79770fa 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java @@ -84,6 +84,8 @@ default List files(Hash h, Path... paths) throws IOException { String pushPath(String remote) throws IOException; boolean isValidRevisionRange(String expression) throws IOException; Optional upstreamFor(Branch branch) throws IOException; + List remoteBranches(String remote) throws IOException; + List remotes() throws IOException; static Optional get(Path p) throws IOException { return Repository.get(p).map(r -> r); diff --git a/vcs/src/main/java/org/openjdk/skara/vcs/Reference.java b/vcs/src/main/java/org/openjdk/skara/vcs/Reference.java new file mode 100644 index 000000000..b1505311c --- /dev/null +++ b/vcs/src/main/java/org/openjdk/skara/vcs/Reference.java @@ -0,0 +1,63 @@ +/* + * 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.vcs; + +import java.util.Objects; + +public class Reference { + private final String name; + private final Hash hash; + + public Reference(String name, Hash hash) { + this.name = name; + this.hash = hash; + } + + public String name() { + return name; + } + + public Hash hash() { + return hash; + } + + @Override + public String toString() { + return name + ": " + hash.hex(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Reference)) { + return false; + } + + var r = (Reference) o; + return Objects.equals(name, r.name) && Objects.equals(hash, r.hash); + } + + @Override + public int hashCode() { + return Objects.hash(name, hash); + } +} 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 c7256fd40..a4fd308d3 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 @@ -1065,4 +1065,28 @@ public boolean contains(Branch b, Hash h) throws IOException { return false; } + + @Override + public List remoteBranches(String remote) throws IOException { + var refs = new ArrayList(); + try (var p = capture("git", "ls-remote", "--heads", "--refs", remote)) { + for (var line : await(p).stdout()) { + var parts = line.split("\t"); + var name = parts[1].replace("refs/heads/", ""); + refs.add(new Reference(name, new Hash(parts[0]))); + } + } + return refs; + } + + @Override + public List remotes() throws IOException { + var remotes = new ArrayList(); + try (var p = capture("git", "remote")) { + for (var line : await(p).stdout()) { + remotes.add(line); + } + } + return remotes; + } } 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 c1e0698d8..8f25b5ac5 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 @@ -1092,4 +1092,38 @@ public boolean contains(Branch b, Hash h) throws IOException { return line.equals(b.name()); } } + + @Override + public List remoteBranches(String remote) throws IOException { + var refs = new ArrayList(); + + var ext = Files.createTempFile("ext", ".py"); + copyResource(EXT_PY, ext); + + try (var p = capture("hg", "--config", "extensions.ls-remote=" + ext, "ls-remote", remote)) { + var res = await(p); + for (var line : res.stdout()) { + var parts = line.split("\t"); + refs.add(new Reference(parts[1], new Hash(parts[0]))); + } + } + return refs; + } + + @Override + public List remotes() throws IOException { + var remotes = new ArrayList(); + try (var p = capture("hg", "paths")) { + for (var line : await(p).stdout()) { + var parts = line.split(" = "); + var name = parts[0]; + if (name.endsWith("-push") || name.endsWith(":push")) { + continue; + } else { + remotes.add(name); + } + } + } + return remotes; + } } diff --git a/vcs/src/main/resources/ext.py b/vcs/src/main/resources/ext.py index 412a6499c..aaff800c4 100644 --- a/vcs/src/main/resources/ext.py +++ b/vcs/src/main/resources/ext.py @@ -23,6 +23,8 @@ import mercurial.patch import mercurial.mdiff import mercurial.util +import mercurial.hg +import mercurial.node import difflib import sys @@ -288,3 +290,12 @@ def ls_tree(ui, repo, rev, **opts): write(nullHash) write('\t') writeln(filename) + +@command('ls-remote', [], 'hg ls-remote PATH') +def ls_remote(ui, repo, path, **opts): + peer = mercurial.hg.peer(ui or repo, opts, ui.expandpath(path)) + for branch, heads in peer.branchmap().iteritems(): + for head in heads: + write(mercurial.node.hex(head)) + write("\t") + writeln(branch) diff --git a/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java b/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java index 8c24388d2..44a14b873 100644 --- a/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java +++ b/vcs/src/test/java/org/openjdk/skara/vcs/RepositoryTests.java @@ -1817,4 +1817,35 @@ void testReset(VCS vcs) throws IOException { assertEquals(1, repo.commits().asList().size()); } } + + @ParameterizedTest + @EnumSource(VCS.class) + void testRemotes(VCS vcs) throws IOException { + try (var dir = new TemporaryDirectory()) { + var repo = Repository.init(dir.path(), vcs); + assertEquals(List.of(), repo.remotes()); + repo.addRemote("foobar", "https://foo/bar"); + assertEquals(List.of("foobar"), repo.remotes()); + } + } + + @ParameterizedTest + @EnumSource(VCS.class) + void testRemoteBranches(VCS vcs) throws IOException { + try (var dir = new TemporaryDirectory()) { + var upstream = Repository.init(dir.path().resolve("upstream"), vcs); + var readme = upstream.root().resolve("README"); + Files.writeString(readme, "Hello\n"); + upstream.add(readme); + var head = upstream.commit("Added README", "duke", "duke@openjdk.org"); + + var fork = Repository.init(dir.path().resolve("fork"), vcs); + fork.addRemote("upstream", "file://" + upstream.root()); + var refs = fork.remoteBranches("upstream"); + assertEquals(1, refs.size()); + var ref = refs.get(0); + assertEquals(head, ref.hash()); + assertEquals(upstream.defaultBranch().name(), ref.name()); + } + } }