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.*;
 26 import org.openjdk.skara.host.*;
 27 import org.openjdk.skara.vcs.Repository;
 28 import org.openjdk.skara.proxy.HttpProxy;
 29 
 30 import java.io.IOException;
 31 import java.net.URI;
 32 import java.nio.file.*;
 33 import java.util.List;
 34 import java.util.function.Supplier;
 35 import java.util.logging.Level;
 36 
 37 public class GitFork {
 38     private static void exit(String fmt, Object...args) {
 39         System.err.println(String.format(fmt, args));
 40         System.exit(1);
 41     }
 42 
 43     private static <T> Supplier<T> die(String fmt, Object... args) {
 44         return () -> {
 45             exit(fmt, args);
 46             return null;
 47         };
 48     }
 49 
 50     private static void sleep(int ms) {
 51         try {
 52             Thread.sleep(ms);
 53         } catch (InterruptedException e) {
 54             // do nothing
 55         }
 56     }
 57 
 58     private static Repository clone(URI from, Path dest, boolean isMercurial) throws IOException {
 59         try {
 60             var to = dest == null ? Path.of(from.getPath()).getFileName() : dest;
 61             if (to.toString().endsWith(".git")) {
 62                 to = Path.of(to.toString().replace(".git", ""));
 63             }
 64 
 65             var vcs = isMercurial ? "hg" : "git";
 66             var pb = new ProcessBuilder(vcs, "clone", from.toString(), to.toString());
 67             pb.inheritIO();
 68             var p = pb.start();
 69             var res = p.waitFor();
 70             if (res != 0) {
 71                 exit("'" + vcs + " clone " + from.toString() + " " + to.toString() + "' failed with exit code: " + res);
 72             }
 73             return Repository.get(to).orElseThrow(() -> new IOException("Could not find repository"));
 74         } catch (InterruptedException e) {
 75             throw new IOException(e);
 76         }
 77     }
 78 
 79     public static void main(String[] args) throws IOException {
 80         var flags = List.of(
 81             Option.shortcut("u")
 82                   .fullname("username")
 83                   .describe("NAME")
 84                   .helptext("Username on host")
 85                   .optional(),
 86             Switch.shortcut("")
 87                   .fullname("verbose")
 88                   .helptext("Turn on verbose output")
 89                   .optional(),
 90             Switch.shortcut("")
 91                   .fullname("debug")
 92                   .helptext("Turn on debugging output")
 93                   .optional(),
 94             Switch.shortcut("")
 95                   .fullname("version")
 96                   .helptext("Print the version of this tool")
 97                   .optional(),
 98             Switch.shortcut("")
 99                   .fullname("mercurial")
100                   .helptext("Force use of mercurial")
101                   .optional());
102 
103         var inputs = List.of(
104             Input.position(0)
105                  .describe("URI")
106                  .singular()
107                  .required(),
108             Input.position(1)
109                  .describe("NAME")
110                  .singular()
111                  .optional());
112 
113         var parser = new ArgumentParser("git-fork", flags, inputs);
114         var arguments = parser.parse(args);
115         var isMercurial = arguments.contains("mercurial");
116 
117         if (arguments.contains("version")) {
118             System.out.println("git-fork version: " + Version.fromManifest().orElse("unknown"));
119             System.exit(0);
120         }
121 
122         if (arguments.contains("verbose") || arguments.contains("debug")) {
123             var level = arguments.contains("debug") ? Level.FINER : Level.FINE;
124             Logging.setup(level);
125         }
126 
127         HttpProxy.setup();
128 
129         final var uri = URI.create(arguments.at(0).or(die("No URI for upstream repository provided")).asString());
130         if (uri == null) {
131             exit("Not a valid URI: " + uri);
132         }
133         final var hostName = uri.getHost();
134         var path = uri.getPath();
135         final var protocol = uri.getScheme();
136         final var token = isMercurial ? System.getenv("HG_TOKEN") : System.getenv("GIT_TOKEN");
137         final var username = arguments.contains("username") ? arguments.get("username").asString() : null;
138         final var credentials = GitCredentials.fill(hostName, path, username, token, protocol);
139 
140         if (credentials.password() == null) {
141             exit("No token for host " + hostName + " found, use git-credentials or the environment variable GIT_TOKEN");
142         }
143 
144         if (credentials.username() == null) {
145             exit("No username for host " + hostName + " found, use git-credentials or the flag --username");
146         }
147 
148         var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
149         if (path.endsWith(".git")) {
150             path = path.substring(0, path.length() - 4);
151         }
152         if (path.startsWith("/")) {
153             path = path.substring(1);
154         }
155 
156         var fork = host.getRepository(path).fork();
157 
158         if (token == null) {
159             GitCredentials.approve(credentials);
160         }
161 
162         var webUrl = fork.getWebUrl();
163         if (isMercurial) {
164             webUrl = URI.create("git+" + webUrl.toString());
165         }
166         if (arguments.at(1).isPresent()) {
167             System.out.println("Fork available at: " + fork.getWebUrl());
168             var dest = arguments.at(1).asString();
169             System.out.println("Cloning " + webUrl + "...");
170             var repo = clone(webUrl, Path.of(dest), isMercurial);
171             var remoteWord = isMercurial ? "path" : "remote";
172             System.out.print("Adding " + remoteWord + " 'upstream' for " + uri.toString() + "...");
173             var upstreamUrl = uri.toString();
174             if (isMercurial) {
175                 upstreamUrl = "git+" + upstreamUrl;
176             }
177             repo.addRemote("upstream", upstreamUrl);
178             var gitConfig = repo.root().resolve(".git").resolve("config");
179             if (!isMercurial && Files.exists(gitConfig)) {
180                 var lines = List.of(
181                     "[sync]",
182                     "        remote = upstream"
183                 );
184                 Files.write(gitConfig, lines, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
185             }
186             System.out.println("done");
187         } else {
188             System.out.println(webUrl);
189         }
190     }
191 }