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 a21d92426..eebabd020 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitFork.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitFork.java @@ -133,7 +133,7 @@ public static void main(String[] args) throws IOException { final var hostName = uri.getHost(); var path = uri.getPath(); final var protocol = uri.getScheme(); - final var token = System.getenv("GIT_TOKEN"); + final var token = isMercurial ? System.getenv("HG_TOKEN") : System.getenv("GIT_TOKEN"); final var username = arguments.contains("username") ? arguments.get("username").asString() : null; final var credentials = GitCredentials.fill(hostName, path, username, token, protocol); 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 eb7491674..b96eba52f 100644 --- a/cli/src/main/java/org/openjdk/skara/cli/GitPr.java +++ b/cli/src/main/java/org/openjdk/skara/cli/GitPr.java @@ -123,6 +123,9 @@ private static PullRequest getPullRequest(URI uri, GitCredentials credentials, A } private static void show(String ref, Hash hash) throws IOException { + show(ref, hash, null); + } + private static void show(String ref, Hash hash, Path dir) throws IOException { var pb = new ProcessBuilder("git", "diff", "--binary", "--patch", "--find-renames=50%", @@ -130,11 +133,30 @@ private static void show(String ref, Hash hash) throws IOException { "--find-copies-harder", "--abbrev", ref + "..." + hash.hex()); + if (dir != null) { + pb.directory(dir.toFile()); + } + pb.inheritIO(); + await(pb.start()); + } + + private static void gimport() throws IOException { + var pb = new ProcessBuilder("hg", "gimport"); + pb.inheritIO(); + await(pb.start()); + } + + private static void hgImport(Path patch) throws IOException { + var pb = new ProcessBuilder("hg", "import", "--no-commit", patch.toAbsolutePath().toString()); pb.inheritIO(); await(pb.start()); } private static Path diff(String ref, Hash hash) throws IOException { + return diff(ref, hash, null); + } + + private static Path diff(String ref, Hash hash, Path dir) throws IOException { var patch = Files.createTempFile(hash.hex(), ".patch"); var pb = new ProcessBuilder("git", "diff", "--binary", "--patch", @@ -143,6 +165,9 @@ private static Path diff(String ref, Hash hash) throws IOException { "--find-copies-harder", "--abbrev", ref + "..." + hash.hex()); + if (dir != null) { + pb.directory(dir.toFile()); + } pb.redirectOutput(patch.toFile()); pb.redirectError(ProcessBuilder.Redirect.INHERIT); await(pb.start()); @@ -157,14 +182,14 @@ private static void apply(Path patch) throws IOException { private static URI toURI(String remotePath) throws IOException { if (remotePath.startsWith("git+")) { - remotePath = remotePath.substring(4); + remotePath = remotePath.substring("git+".length()); } if (remotePath.startsWith("http")) { return URI.create(remotePath); - } else if (remotePath.startsWith("ssh://")) { - var sshURI = URI.create(remotePath); - return URI.create("https://" + sshURI.getHost() + sshURI.getPath()); } else { + if (remotePath.startsWith("ssh://")) { + remotePath = remotePath.substring("ssh://".length()).replaceFirst("/", ":"); + } var indexOfColon = remotePath.indexOf(':'); var indexOfSlash = remotePath.indexOf('/'); if (indexOfColon != -1) { @@ -289,7 +314,7 @@ public static void main(String[] args) throws IOException { var remote = arguments.get("remote").orString(isMercurial ? "default" : "origin"); var remotePullPath = repo.pullPath(remote); var username = arguments.contains("username") ? arguments.get("username").asString() : null; - var token = System.getenv("GIT_TOKEN"); + var token = isMercurial ? System.getenv("HG_TOKEN") : System.getenv("GIT_TOKEN"); var uri = toURI(remotePullPath); var credentials = GitCredentials.fill(uri.getHost(), uri.getPath(), username, token, uri.getScheme()); var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password())); @@ -547,7 +572,7 @@ public static void main(String[] args) throws IOException { .collect(Collectors.toList()); System.out.format(fmt, (Object[]) row.toArray(new String[0])); } - } else if (action.equals("fetch") || action.equals("checkout") || action.equals("show") || action.equals("apply") || action.equals("close") || action.equals("update")) { + } else if (action.equals("fetch") || action.equals("checkout") || action.equals("show") || action.equals("apply")) { var prId = arguments.at(1); if (!prId.isPresent()) { exit("error: missing pull request identifier"); @@ -555,8 +580,50 @@ public static void main(String[] args) throws IOException { var remoteRepo = getHostedRepositoryFor(uri, credentials); var pr = remoteRepo.getPullRequest(prId.asString()); - var fetchHead = repo.fetch(remoteRepo.getUrl(), pr.getHeadHash().hex()); + var repoUrl = remoteRepo.getWebUrl(); + var prHeadRef = pr.getSourceRef(); + var isHgGit = isMercurial && Repository.exists(repo.root().resolve(".hg").resolve("git")); + if (isHgGit) { + var hgGitRepo = Repository.get(repo.root().resolve(".hg").resolve("git")).get(); + var hgGitFetchHead = hgGitRepo.fetch(repoUrl, prHeadRef); + + if (action.equals("show") || action.equals("apply")) { + var target = hgGitRepo.fetch(repoUrl, pr.getTargetRef()); + var hgGitMergeBase = hgGitRepo.mergeBase(target, hgGitFetchHead); + + if (action.equals("show")) { + show(hgGitMergeBase.hex(), hgGitFetchHead, hgGitRepo.root()); + } else { + var patch = diff(hgGitMergeBase.hex(), hgGitFetchHead, hgGitRepo.root()); + hgImport(patch); + Files.delete(patch); + } + } else if (action.equals("fetch") || action.equals("checkout")) { + var hgGitRef = prHeadRef.endsWith("/head") ? prHeadRef.replace("/head", "") : prHeadRef; + var hgGitBranches = hgGitRepo.branches(); + if (hgGitBranches.contains(new Branch(hgGitRef))) { + hgGitRepo.delete(new Branch(hgGitRef)); + } + hgGitRepo.branch(hgGitFetchHead, hgGitRef); + gimport(); + var hgFetchHead = repo.resolve(hgGitRef).get(); + + if (action.equals("fetch") && arguments.contains("branch")) { + repo.branch(hgFetchHead, arguments.get("branch").asString()); + } else if (action.equals("checkout")) { + repo.checkout(hgFetchHead); + if (arguments.contains("branch")) { + repo.branch(hgFetchHead, arguments.get("branch").asString()); + } + } + } else { + exit("Unexpected action: " + action); + } + + return; + } + var fetchHead = repo.fetch(repoUrl, pr.getSourceRef()); if (action.equals("fetch")) { if (arguments.contains("branch")) { var branchName = arguments.get("branch").asString(); @@ -578,19 +645,33 @@ public static void main(String[] args) throws IOException { var patch = diff(pr.getTargetRef(), fetchHead); apply(patch); Files.deleteIfExists(patch); - } else if (action.equals("close")) { - pr.setState(PullRequest.State.CLOSED); - } else if (action.equals("update")) { - if (arguments.contains("assignees")) { - var usernames = Arrays.asList(arguments.get("assignees").asString().split(",")); - var assignees = usernames.stream() - .map(host::getUserDetails) - .collect(Collectors.toList()); - pr.setAssignees(assignees); - } - } else { - exit("error: unexpected action: " + action); } + } else if (action.equals("close")) { + var prId = arguments.at(1); + if (!prId.isPresent()) { + exit("error: missing pull request identifier"); + } + + var remoteRepo = getHostedRepositoryFor(uri, credentials); + var pr = remoteRepo.getPullRequest(prId.asString()); + pr.setState(PullRequest.State.CLOSED); + } else if (action.equals("update")) { + var prId = arguments.at(1); + if (!prId.isPresent()) { + exit("error: missing pull request identifier"); + } + + var remoteRepo = getHostedRepositoryFor(uri, credentials); + var pr = remoteRepo.getPullRequest(prId.asString()); + if (arguments.contains("assignees")) { + var usernames = Arrays.asList(arguments.get("assignees").asString().split(",")); + var assignees = usernames.stream() + .map(host::getUserDetails) + .collect(Collectors.toList()); + pr.setAssignees(assignees); + } + } else { + exit("error: unexpected action: " + action); } } } diff --git a/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java index 0b2ead73d..f495f5ad0 100644 --- a/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java +++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java @@ -124,9 +124,13 @@ private String getFullName(String userName) { } // Most GitHub API's return user information in this format - HostUserDetails parseUserDetails(JSONValue json) { - return new HostUserDetails(json.get("user").get("id").asInt(), json.get("user").get("login").asString(), - () -> getFullName(json.get("user").get("login").asString())); + HostUserDetails parseUserField(JSONValue json) { + return parseUserObject(json.get("user")); + } + + HostUserDetails parseUserObject(JSONValue json) { + return new HostUserDetails(json.get("id").asInt(), json.get("login").asString(), + () -> getFullName(json.get("login").asString())); } @Override diff --git a/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java index f50be7ea2..45d7fd311 100644 --- a/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java +++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java @@ -60,7 +60,7 @@ public String getId() { @Override public HostUserDetails getAuthor() { - return host.parseUserDetails(json); + return host.parseUserField(json); } @Override @@ -69,7 +69,7 @@ public List getReviews() { .map(JSONValue::asObject) .filter(obj -> !(obj.get("state").asString().equals("COMMENTED") && obj.get("body").asString().isEmpty())) .map(obj -> { - var reviewer = host.parseUserDetails(obj); + var reviewer = host.parseUserField(obj); var hash = new Hash(obj.get("commit_id").asString()); Review.Verdict verdict; switch (obj.get("state").asString()) { @@ -112,7 +112,7 @@ public void addReview(Review.Verdict verdict, String body) { } private ReviewComment parseReviewComment(ReviewComment parent, JSONObject json, PositionMapper diff) { - var author = host.parseUserDetails(json); + var author = host.parseUserField(json); var threadId = parent == null ? json.get("id").toString() : parent.threadId(); var comment = new ReviewComment(parent, threadId, @@ -231,7 +231,7 @@ public void setBody(String body) { private Comment parseComment(JSONValue comment) { var ret = new Comment(Integer.toString(comment.get("id").asInt()), comment.get("body").asString(), - host.parseUserDetails(comment), + host.parseUserField(comment), ZonedDateTime.parse(comment.get("created_at").asString()), ZonedDateTime.parse(comment.get("updated_at").asString())); return ret; @@ -425,7 +425,7 @@ public String toString() { public List getAssignees() { return json.get("assignees").asArray() .stream() - .map(host::parseUserDetails) + .map(host::parseUserObject) .collect(Collectors.toList()); } diff --git a/skara.py b/skara.py index 42de3ce39..3f75b7ebe 100644 --- a/skara.py +++ b/skara.py @@ -65,20 +65,53 @@ def _skara(ui, args, **opts): sys.exit(subprocess.call([git_skara] + args)) -fork_opts = [ - ('u', 'username', '', 'Username on host'), -] -@command('fork', fork_opts, 'hg fork URL [DEST]', norepo=True) -def fork(ui, url, dest=None, **opts): +def _web_url(url): + if url.startswith('git+'): + url = url[len('git+'):] + + if url.startswith('http'): + return url + + if not url.startswith('ssh://'): + raise ValueError('Unexpected url: ' + url) + + without_protocol = url[len('ssh://'):] + first_slash = without_protocol.index('/') + host = without_protocol[:first_slash] + + ssh_config = os.path.join(os.path.expanduser('~'), '.ssh', 'config') + if os.path.exists(ssh_config): + with open(ssh_config) as f: + lines = f.readlines() + current = None + for line in lines: + if line.startswith('Host '): + current = line.split(' ')[1].strip() + if line.strip().lower().startswith('hostname') and host == current: + host = line.strip().split(' ')[1] + break + + return 'https://' + host + without_protocol[first_slash:] + +def _username(ui, opts, url): + web_url = _web_url(url) username = None - if opts['username'] != '' and url.startswith('http'): - username = ui.config('credential "' + url + '"', 'username') + if opts.get('username') == '': + username = ui.config('credential "' + web_url + '"', 'username') if username == None: - protocol, rest = url.split('://') - hostname = rest[:rest.find('/')] + protocol, rest = web_url.split('://') + hostname = rest[:rest.index('/')] username = ui.config('credential "' + protocol + '://' + hostname + '"', 'username') if username == None: username = ui.config('credential', 'username') + return username + +fork_opts = [ + ('u', 'username', '', 'Username on host'), +] +@command('fork', fork_opts, 'hg fork URL [DEST]', norepo=True) +def fork(ui, url, dest=None, **opts): + username = _username(ui, opts, url) args = ['fork', '--mercurial'] if username != None: args.append("--username") @@ -144,14 +177,26 @@ def info(ui, repo, rev, **opts): pr_opts = [ ('u', 'username', '', 'Username on host'), - ('r', 'remote', '', 'Name of remote, defaults to "origin"'), - ('b', 'branch', '', 'Name of target branch, defaults to "master"'), + ('r', 'remote', '', 'Name of path, defaults to "default"'), + ('b', 'branch', '', 'Name of target branch, defaults to "default"'), ('', 'authors', '', 'Comma separated list of authors'), ('', 'assignees', '', 'Comma separated list of assignees'), ('', 'labels', '', 'Comma separated list of labels'), ('', 'columns', '', 'Comma separated list of columns to show'), ('', 'no-decoration', False, 'Do not prefix lines with any decoration') ] -@command('pr', info_opts, 'hg pr ') -def pr(ui, repo, action, **opts): - _skara(ui, ['pr', '--mercurial', action], **opts) +@command('pr', pr_opts, 'hg pr ') +def pr(ui, repo, action, n=None, **opts): + path = opts.get('remote') + if path == '': + path = 'default' + url = ui.config('paths', path) + username = _username(ui, opts, url) + args = ['pr', '--mercurial'] + if username != None: + args.append('--username') + args.append(username) + args.append(action) + if n != None: + args.append(n) + _skara(ui, args, **opts) 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 4bd46a098..7205b6ebd 100644 --- a/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java +++ b/vcs/src/main/java/org/openjdk/skara/vcs/ReadOnlyRepository.java @@ -55,6 +55,12 @@ public interface ReadOnlyRepository { Hash mergeBase(Hash first, Hash second) throws IOException; boolean isAncestor(Hash ancestor, Hash descendant) throws IOException; Optional resolve(String ref) throws IOException; + default Optional resolve(Tag t) throws IOException { + return resolve(t.name()); + } + default Optional resolve(Branch b) throws IOException { + return resolve(b.name()); + } boolean contains(Branch b, Hash h) throws IOException; Optional username() throws IOException; Optional show(Path p, Hash h) throws IOException;