From 304e4cca1234da31084df81a433384dcdd214141 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Sat, 16 Jun 2012 12:17:58 +0400 Subject: [PATCH] Move to GitHub API v3, refactor. IDEA-85769 * Introduce getRequest() and postRequest() methods as entry points to do a query to the API instead of doREST, which requires releasing an HttpMethod and so on. * Return parsed JsonElement from these methods, since Github API returns the response in JSON format. * Post request in JSON format as well. Get-methods are not called with parameters yet, so don't bother for them. * Use these methods from GithubCreateGistAction which already uses API v3. * Introduce getApiUrl to return the correct API url for github as well as for enterprise installations. * Introduce GithubUser to encapsulate information about a user. Currently it works only for the current user, and stores only its plan to know if private repositories are allowed for the person. * Rewrite RepositoryInfo to be a structured holder of the information about a repository, without working with DOM elements and so on. Rename some methods to make their return value more obvious. * Add @Nullable, @NotNull, javadocs, code style fixes. * In the GithubShareAction move initial pushing to the background to avoid the modality lock with the dialog asking for the passphrase. This fixes IDEA-86645. --- .../github/GithubCheckoutProvider.java | 4 +- .../github/GithubCreateGistAction.java | 75 ++-- .../plugins/github/GithubRebaseAction.java | 2 +- .../plugins/github/GithubSettings.java | 10 +- .../plugins/github/GithubShareAction.java | 100 ++++-- .../jetbrains/plugins/github/GithubUser.java | 66 ++++ .../jetbrains/plugins/github/GithubUtil.java | 320 ++++++++++++------ .../plugins/github/RepositoryInfo.java | 72 ++-- 8 files changed, 439 insertions(+), 210 deletions(-) create mode 100644 plugins/github/src/org/jetbrains/plugins/github/GithubUser.java diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCheckoutProvider.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCheckoutProvider.java index 792bca17e313..766f268acfc0 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCheckoutProvider.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCheckoutProvider.java @@ -50,7 +50,7 @@ public class GithubCheckoutProvider implements CheckoutProvider { Collections.sort(availableRepos, new Comparator() { @Override public int compare(final RepositoryInfo r1, final RepositoryInfo r2) { - final int comparedOwners = r1.getOwner().compareTo(r2.getOwner()); + final int comparedOwners = r1.getOwnerName().compareTo(r2.getOwnerName()); return comparedOwners != 0 ? comparedOwners : r1.getName().compareTo(r2.getName()); } }); @@ -58,7 +58,7 @@ public class GithubCheckoutProvider implements CheckoutProvider { final GitCloneDialog dialog = new GitCloneDialog(project); // Add predefined repositories to history for (int i = availableRepos.size() - 1; i>=0; i--){ - dialog.prependToHistory(availableRepos.get(i).getUrl()); + dialog.prependToHistory(availableRepos.get(i).getCloneUrl()); } dialog.show(); if (!dialog.isOK()) { diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java index 83412ba7f292..8531a4571217 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java @@ -17,8 +17,6 @@ package org.jetbrains.plugins.github; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; import com.intellij.ide.BrowserUtil; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; @@ -34,9 +32,6 @@ import com.intellij.openapi.util.Ref; import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitVcs; import git4idea.Notificator; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.ui.GitHubCreateGistDialog; @@ -130,41 +125,7 @@ public class GithubCreateGistAction extends DumbAwareAction { ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { @Override public void run() { - // Using GitHub Gist API v3: http://developer.github.com/v3/gists/ - final HttpClient client = anonymous ? GithubUtil.getHttpClient(null, null) : GithubUtil.getHttpClient(settings.getLogin(), password); - final PostMethod method = new PostMethod("https://api.github.com/gists"); - - String request = prepareJsonRequest(description, isPrivate, text, file); - - String response; - try { - method.setRequestEntity(new StringRequestEntity(request, "application/json", "UTF-8")); - client.executeMethod(method); - response = method.getResponseBodyAsString(); - } - catch (IOException e1) { - showError(project, "Failed to create gist", "", null, e1); - return; - } - finally { - method.releaseConnection(); - } - - JsonObject jsonResponse; - try { - jsonResponse = new JsonParser().parse(response).getAsJsonObject(); - } - catch (JsonSyntaxException jse) { - showError(project, "Couldn't parse GitHub response", "", response, jse); - return; - } - - JsonElement htmlUrl = jsonResponse.get("html_url"); - if (htmlUrl == null) { - showError(project, "Invalid GitHub response", "No html_url property", response, null); - return; - } - url.set(htmlUrl.getAsString()); + url.set(createGist(project, settings.getLogin(), password, anonymous, text, isPrivate, file, description)); } }, "Communicating With GitHub", false, project); @@ -188,6 +149,40 @@ public class GithubCreateGistAction extends DumbAwareAction { } } + @Nullable + private static String createGist(@NotNull Project project, @Nullable String login, @Nullable String password, boolean anonymous, + @NotNull String text, boolean isPrivate, @NotNull VirtualFile file, @NotNull String description) { + if (anonymous) { + login = null; + password = null; + } + String requestBody = prepareJsonRequest(description, isPrivate, text, file); + try { + JsonElement jsonElement = GithubUtil.postRequest("https://api.github.com", login, password, "/gists", requestBody); + if (jsonElement == null) { + LOG.info("Null JSON response returned by GitHub"); + showError(project, "Failed to create gist", "Empty JSON response returned by GitHub", null, null); + return null; + } + if (!jsonElement.isJsonObject()) { + LOG.error(String.format("Unexpected JSON result format: %s", jsonElement)); + return null; + } + JsonElement htmlUrl = jsonElement.getAsJsonObject().get("html_url"); + if (htmlUrl == null) { + LOG.info("Invalid JSON response: " + jsonElement); + showError(project, "Invalid GitHub response", "No html_url property", jsonElement.toString(), null); + return null; + } + return htmlUrl.getAsString(); + } + catch (IOException e) { + LOG.info("Exception when creating a Gist", e); + showError(project, "Failed to create gist", "", null, e); + return null; + } + } + private static void showError(@NotNull Project project, @NotNull String title, @NotNull String content, @Nullable String details, @Nullable Exception e) { Notificator.getInstance(project).notifyError(title, content); diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java index 5967e49727e3..bff7f575b574 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java @@ -139,7 +139,7 @@ public class GithubRebaseAction extends DumbAwareAction { return; } - final String parent = repositoryInfo.getParent(); + final String parent = repositoryInfo.getParentName(); LOG.assertTrue(parent != null, "Parent repository not found!"); final String parentRepoSuffix = parent + ".git"; final String parentRepoUrl = "git://github.com/" + parentRepoSuffix; diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubSettings.java b/plugins/github/src/org/jetbrains/plugins/github/GithubSettings.java index 0208f7fdfdfb..ef3857b14f44 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubSettings.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubSettings.java @@ -29,7 +29,6 @@ import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.ui.Messages; import org.jdom.Element; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * @author oleg @@ -43,13 +42,14 @@ import org.jetbrains.annotations.Nullable; ) public class GithubSettings implements PersistentStateComponent { + public static final String DEFAULT_GITHUB_HOST = "github.com"; + private static final String GITHUB_SETTINGS_TAG = "GithubSettings"; private static final String LOGIN = "Login"; private static final String HOST = "Host"; private static final String ANONIMOUS_GIST = "Anonymous"; private static final String OPEN_IN_BROWSER_GIST = "OpenInBrowser"; private static final String PRIVATE_GIST = "Private"; - private static final String GITHUB = "github.com"; private static final String GITHUB_SETTINGS_PASSWORD_KEY = "GITHUB_SETTINGS_PASSWORD_KEY"; private String myLogin; @@ -117,7 +117,7 @@ public class GithubSettings implements PersistentStateComponent { return myLogin != null ? myLogin : ""; } - @Nullable + @NotNull public String getPassword() { LOG.assertTrue(!ProgressManager.getInstance().hasProgressIndicator(), "Password should not be accessed under modal progress"); String password; @@ -154,7 +154,7 @@ public class GithubSettings implements PersistentStateComponent { } public String getHost() { - return myHost != null ? myHost : GITHUB; + return myHost != null ? myHost : DEFAULT_GITHUB_HOST; } public boolean isAnonymous() { @@ -187,7 +187,7 @@ public class GithubSettings implements PersistentStateComponent { } public void setHost(final String host) { - myHost = host != null ? host : GITHUB; + myHost = host != null ? host : DEFAULT_GITHUB_HOST; } public void setAnonymousGist(final boolean anonymousGist) { diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java index 68ddeee657bf..263e038ead8c 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java @@ -15,6 +15,8 @@ */ package org.jetbrains.plugins.github; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; @@ -26,11 +28,11 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.Ref; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.ChangeListManager; @@ -53,6 +55,7 @@ import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryManager; import git4idea.util.GitFileUtils; import git4idea.util.GitUIUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.github.ui.GithubShareDialog; import java.io.IOException; @@ -83,7 +86,7 @@ public class GithubShareAction extends DumbAwareAction { @Override public void actionPerformed(final AnActionEvent e) { final Project project = e.getData(PlatformDataKeys.PROJECT); - if (!GithubUtil.testGitExecutable(project)){ + if (project == null || !GithubUtil.testGitExecutable(project)){ return; } final VirtualFile root = project.getBaseDir(); @@ -136,22 +139,49 @@ public class GithubShareAction extends DumbAwareAction { final String description = shareDialog.getDescription(); try { LOG.info("Creating GitHub repository"); - final String escapedDescription = JDOMUtil.escapeText(description, true, true).replace("&#", "%"); - GithubUtil.doREST(settings.getHost(), settings.getLogin(), settings.getPassword(), - "/repos/create?name=" + name + "&public=" + (isPrivate ? "0" : "1") + "&description=" + escapedDescription, true).releaseConnection(); - LOG.info("Successfully created GitHub repository"); + boolean repositoryCreated = + createGithubRepository(settings.getHost(), settings.getLogin(), settings.getPassword(), name, description, isPrivate); + if (repositoryCreated) { + LOG.info("Successfully created GitHub repository"); + } + else { + Messages.showErrorDialog(project, "Failed to create new GitHub repository", "Create GitHub Repository"); + return; + } } catch (final Exception e1) { Messages.showErrorDialog(e1.getMessage(), "Failed to create new GitHub repository"); return; } - if (bindToGithub(project, root, gitDetected, settings.getLogin(), name)) { - Notifications.Bus.notify(new Notification("github", "Success", "Successfully created project ''" + name + "'' on github", - NotificationType.INFORMATION)); - } + + bindToGithub(project, root, gitDetected, settings.getLogin(), name); } - private boolean bindToGithub(final Project project, final VirtualFile root, final boolean gitDetected, final String login, String name) { + private static boolean createGithubRepository(@NotNull String host, @NotNull String login, @NotNull String password, @NotNull String name, + @NotNull String description, boolean aPrivate) throws IOException { + String path = "/user/repos"; + String requestBody = prepareRequest(name, description, aPrivate); + JsonElement result = GithubUtil.postRequest(host, login, password, path, requestBody); + if (result == null) { + return false; + } + if (!result.isJsonObject()) { + LOG.error(String.format("Unexpected JSON result format: %s", result)); + return false; + } + return result.getAsJsonObject().has("url"); + } + + private static String prepareRequest(String name, String description, boolean isPrivate) { + JsonObject json = new JsonObject(); + json.addProperty("name", name); + json.addProperty("description", description); + json.addProperty("public", Boolean.toString(!isPrivate)); + return json.toString(); + + } + + private void bindToGithub(final Project project, final VirtualFile root, boolean gitDetected, final String login, final String name) { LOG.info("Binding local project with GitHub"); // creating empty git repo if git isnot initialized if (!gitDetected) { @@ -162,7 +192,7 @@ public class GithubShareAction extends DumbAwareAction { if (!h.errors().isEmpty()) { GitUIUtil.showOperationErrors(project, h.errors(), "git init"); LOG.info("Failed to create empty git repo: " + h.errors()); - return false; + return; } final ProgressManager manager = ProgressManager.getInstance(); manager.runProcessWithProgressSynchronously(new Runnable() { @@ -175,7 +205,7 @@ public class GithubShareAction extends DumbAwareAction { // In this case we should create sample commit for binding project if (!performFirstCommitIfRequired(project, root)) { - return false; + return; } //git remote add origin git@github.com:login/name.git @@ -188,36 +218,40 @@ public class GithubShareAction extends DumbAwareAction { addRemoteHandler.run(); if (addRemoteHandler.getExitCode() != 0) { Messages.showErrorDialog("Failed to add GitHub repository as remote", "Failed to add GitHub repository as remote"); - return false; + return; } } catch (VcsException e) { Messages.showErrorDialog(e.getMessage(), "Failed to add GitHub repository as remote"); LOG.info("Failed to add GitHub as remote: " + e.getMessage()); - return false; + return; } //git push origin master - final ProgressManager manager = ProgressManager.getInstance(); + final ArrayList errors = new ArrayList(); - manager.runProcessWithProgressSynchronously(new Runnable() { - public void run() { - final ProgressIndicator progressIndicator = manager.getProgressIndicator(); - if (progressIndicator != null){ - progressIndicator.setText("Pushing to GitHub"); - } - final GitLineHandler gitPushHandler = new GitLineHandler(project, root, GitCommand.PUSH); - gitPushHandler.addParameters("-u", "origin", "master"); - GitPushUtils.trackPushRejectedAsError(gitPushHandler, "Rejected push (" + root.getPresentableUrl() + "): "); - errors.addAll(GitHandlerUtil.doSynchronouslyWithExceptions(gitPushHandler)); + new Task.Backgroundable(project, "Pushing to GitHub", false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + final GitLineHandler gitPushHandler = new GitLineHandler(project, root, GitCommand.PUSH); + gitPushHandler.addParameters("-u", "origin", "master"); + GitPushUtils.trackPushRejectedAsError(gitPushHandler, "Rejected push (" + root.getPresentableUrl() + "): "); + errors.addAll(GitHandlerUtil.doSynchronouslyWithExceptions(gitPushHandler)); + if (!errors.isEmpty()) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + GitUIUtil.showOperationErrors(project, errors, GitBundle.getString("push.active.pushing")); + } + }); } - }, GitBundle.getString("push.active.pushing"), false, project); - if (!errors.isEmpty()) { - GitUIUtil.showOperationErrors(project, errors, GitBundle.getString("push.active.pushing")); - } - // refresh vcs manually - RefreshAction.doRefresh(project); - return true; + else { + RefreshAction.doRefresh(project); + Notifications.Bus.notify(new Notification("github", "Success", "Successfully created project ''" + name + "'' on github", + NotificationType.INFORMATION)); + } + } + }.queue(); } private boolean performFirstCommitIfRequired(final Project project, final VirtualFile root) { diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubUser.java b/plugins/github/src/org/jetbrains/plugins/github/GithubUser.java new file mode 100644 index 000000000000..dbac8d7e6055 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubUser.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github; + +import org.jetbrains.annotations.NotNull; + +/** + * Information about a user on GitHub. + * + * @author Kirill Likhodedov + */ +class GithubUser { + + enum Plan { + FREE, + MICRO, + SMALL, + MEDIUM, + BRONZE, + SILVER, + GOLD, + PLATINUM; + + public boolean isPrivateRepoAllowed() { + return this != FREE; + } + + public static Plan fromString(String name) { + for (Plan plan : values()) { + if (plan.name().equalsIgnoreCase(name)) { + return plan; + } + } + return defaultPlan(); + } + + private static Plan defaultPlan() { + return FREE; + } + } + + @NotNull private final Plan myPlan; + + GithubUser(@NotNull Plan plan) { + myPlan = plan; + } + + @NotNull + Plan getPlan() { + return myPlan; + } + +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java b/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java index 3be5b107733e..6520e1ad2219 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java @@ -15,6 +15,10 @@ */ package org.jetbrains.plugins.github; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; @@ -37,35 +41,41 @@ import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; -import org.jdom.Element; -import org.jdom.input.SAXBuilder; +import org.apache.commons.httpclient.methods.StringRequestEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.ui.GithubLoginDialog; import javax.swing.*; -import java.io.InputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** + * Various utility methods for the GutHub plugin. + * * @author oleg + * @author Kirill Likhodedov */ public class GithubUtil { - private static final String API_URL = "/api/v2/xml"; - private static final Logger LOG = Logger.getInstance(GithubUtil.class.getName()); + public static final Icon GITHUB_ICON = IconLoader.getIcon("/org/jetbrains/plugins/github/github_icon.png"); + private static final Logger LOG = Logger.getInstance(GithubUtil.class.getName()); + /** + * @deprecated The host may be defined in different formats. Use {@link #getApiUrl(String)} instead. + */ + @Deprecated public static String getHttpsUrl() { return "https://" + GithubSettings.getInstance().getHost(); } - public static String getHostByUrl(final String url) { - return url.startsWith("https://") ? url.substring(8) : url.startsWith("http://") ? url.substring(7) : url.startsWith("git@") ? url.substring(4) : url; - } - + /** + * @deprecated TODO Use background progress + */ + @Deprecated public static T accessToGithubWithModalProgress(final Project project, final Computable computable) { final Ref result = new Ref(); ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { @@ -76,6 +86,10 @@ public class GithubUtil { return result.get(); } + /** + * @deprecated TODO Use background progress + */ + @Deprecated public static void accessToGithubWithModalProgress(final Project project, final Runnable runnable) { ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { public void run(@NotNull ProgressIndicator indicator) { @@ -84,38 +98,113 @@ public class GithubUtil { }); } - public static boolean testConnection(final String url, final String login, final String password) { + @Nullable + public static JsonElement getRequest(@NotNull String host, @NotNull String login, @NotNull String password, + @NotNull String path) throws IOException { + return request(host, login, password, path, null, false); + } + + @Nullable + public static JsonElement postRequest(@NotNull String host, @Nullable String login, @Nullable String password, + @NotNull String path, @Nullable String requestBody) throws IOException { + return request(host, login, password, path, requestBody, true); + } + + @Nullable + private static JsonElement request(@NotNull String host, @Nullable String login, @Nullable String password, + @NotNull String path, @Nullable String requestBody, boolean post) throws IOException { HttpMethod method = null; try { - method = doREST(url, login, password, "/user/show/" + login, false); - final InputStream stream = method.getResponseBodyAsStream(); - final Element element = new SAXBuilder(false).build(stream).getRootElement(); - if ("error".equals(element.getName())){ - return false; + method = doREST(host, login, password, path, requestBody, post); + String resp = method.getResponseBodyAsString(); + if (resp == null) { + LOG.info(String.format("Unexpectedly empty response: %s", resp)); + return null; } - // In case if authentification was successful we should see some extra fields - return element.getChild("total-private-repo-count") != null; - } - catch (Exception e) { - // Ignore + return parseResponse(resp); } finally { - if (method!=null) { + if (method != null) { method.releaseConnection(); } } - return false; } - public static HttpMethod doREST(final String url, final String login, final String password, final String request, final boolean post) throws Exception { + @NotNull + private static HttpMethod doREST(@NotNull String host, @Nullable String login, @Nullable String password, @NotNull String path, + @Nullable String requestBody, final boolean post) throws IOException { final HttpClient client = getHttpClient(login, password); - final String uri = "https://" + getHostByUrl(url) + API_URL + request; - final HttpMethod method = post ? new PostMethod(uri) : new GetMethod(uri); + final String uri = getApiUrl(host) + path; + final HttpMethod method; + if (post) { + method = new PostMethod(uri); + if (requestBody != null) { + ((PostMethod)method).setRequestEntity(new StringRequestEntity(requestBody, "application/json", "UTF-8")); + } + } + else { + method = new GetMethod(uri); + } + client.executeMethod(method); return method; } - public static HttpClient getHttpClient(@Nullable final String login, @Nullable final String password) { + @NotNull + private static String removeProtocolPrefix(final String url) { + if (url.startsWith("https://")) { + return url.substring(8); + } + else if (url.startsWith("http://")) { + return url.substring(7); + } + else if (url.startsWith("git@")) { + return url.substring(4); + } + else { + return url; + } + } + + @NotNull + private static String getApiUrl(@NotNull String urlFromSettings) { + return "https://" + getApiUrlWithoutProtocol(urlFromSettings); + } + + /* + All API access is over HTTPS, and accessed from the api.github.com domain + (or through yourdomain.com/api/v3/ for enterprise). + http://developer.github.com/v3/ + */ + @NotNull + private static String getApiUrlWithoutProtocol(String urlFromSettings) { + String url = removeTrailingSlash(removeProtocolPrefix(urlFromSettings)); + final String API_PREFIX = "api."; + final String ENTERPRISE_API_SUFFIX = "/api/v3"; + + if (url.equals(GithubSettings.DEFAULT_GITHUB_HOST)) { + return API_PREFIX + url; + } + else if (url.equals(API_PREFIX + GithubSettings.DEFAULT_GITHUB_HOST)) { + return url; + } + else if (url.endsWith(ENTERPRISE_API_SUFFIX)) { + return url; + } + else { + return url + ENTERPRISE_API_SUFFIX; + } + } + + private static String removeTrailingSlash(String s) { + if (s.endsWith("/")) { + return s.substring(0, s.length() - 1); + } + return s; + } + + @NotNull + private static HttpClient getHttpClient(@Nullable final String login, @Nullable final String password) { final HttpClient client = new HttpClient(); client.getParams().setContentCharset("UTF-8"); // Configure proxySettings if it is required @@ -135,87 +224,127 @@ public class GithubUtil { return client; } - public static List getAvailableRepos(final String url, final String login, final String password, final boolean ownOnly) { - HttpMethod method = null; - try { - final String request = (ownOnly ? "/repos/show/" : "/repos/watched/") + login; - method = doREST(url, login, password, request, false); - final InputStream stream = method.getResponseBodyAsStream(); - final Element element = new SAXBuilder(false).build(stream).getRootElement(); - if ("error".equals(element.getName())){ - LOG.warn("Got error element by request: " + request); - return Collections.emptyList(); - } - final List repositories = element.getChildren(); - final List result = new ArrayList(); - for (int i = 0; i < repositories.size(); i++) { - final Element repo = (Element)repositories.get(i); - result.add(new RepositoryInfo(repo)); - } - return result; - } - catch (Exception e) { - // ignore - } - finally { - if (method != null){ - method.releaseConnection(); - } - } - return Collections.emptyList(); + private static boolean testConnection(final String url, final String login, final String password) { + GithubUser user = retrieveCurrentUserInfo(url, login, password); + return user != null; } + @Nullable + private static GithubUser retrieveCurrentUserInfo(@NotNull String url, @NotNull String login, @NotNull String password) { + try { + JsonElement result = getRequest(url, login, password, "/user"); + return parseUserInfo(result); + } + catch (IOException e) { + LOG.info(e); + return null; + } + } @Nullable - public static RepositoryInfo getDetailedRepoInfo(final String url, final String login, final String password, final String owner, final String name) { - HttpMethod method = null; + private static GithubUser parseUserInfo(@Nullable JsonElement result) { + if (result == null) { + return null; + } + if (!result.isJsonObject()) { + LOG.error(String.format("Unexpected JSON result format: %s", result)); + return null; + } + + JsonObject obj = (JsonObject)result; + if (!obj.has("plan")) { + return null; + } + GithubUser.Plan plan = parsePlan(obj.get("plan")); + return new GithubUser(plan); + } + + @NotNull + private static GithubUser.Plan parsePlan(JsonElement plan) { + if (!plan.isJsonObject()) { + return GithubUser.Plan.FREE; + } + return GithubUser.Plan.fromString(plan.getAsJsonObject().get("name").getAsString()); + } + + @NotNull + private static JsonElement parseResponse(@NotNull String githubResponse) throws IOException { try { - final String request = "/repos/show/" + owner + "/" + name; - method = doREST(url, login, password, request, false); - final InputStream stream = method.getResponseBodyAsStream(); - final Element element = new SAXBuilder(false).build(stream).getRootElement(); - if ("error".equals(element.getName())){ - LOG.warn("Got error element by request: " + request); + return new JsonParser().parse(githubResponse); + } + catch (JsonSyntaxException jse) { + throw new IOException(String.format("Couldn't parse GitHub response:%n%s", githubResponse), jse); + } + } + + @NotNull + private static List getAvailableRepos(@NotNull String url, @NotNull String login, @NotNull String password, + boolean ownOnly) { + final String request = (ownOnly ? "/user/repos" : "/user/watched"); + try { + JsonElement result = getRequest(url, login, password, request); + if (result == null) { + return Collections.emptyList(); + } + return parseRepositoryInfos(result); + } + catch (IOException e) { + LOG.error(e); + return Collections.emptyList(); + } + } + + @NotNull + private static List parseRepositoryInfos(@NotNull JsonElement result) { + if (!result.isJsonArray()) { + LOG.assertTrue(result.isJsonObject(), String.format("Unexpected JSON result format: %s", result)); + return Collections.singletonList(parseSingleRepositoryInfo(result.getAsJsonObject())); + } + + List repositories = new ArrayList(); + for (JsonElement element : result.getAsJsonArray()) { + LOG.assertTrue(element.isJsonObject(), + String.format("This element should be a JsonObject: %s%nTotal JSON response: %n%s", element, result)); + repositories.add(parseSingleRepositoryInfo(element.getAsJsonObject())); + } + return repositories; + } + + @NotNull + private static RepositoryInfo parseSingleRepositoryInfo(@NotNull JsonObject result) { + String name = result.get("name").getAsString(); + String cloneUrl = result.get("clone_url").getAsString(); + String ownerName = result.get("owner").getAsJsonObject().get("login").getAsString(); + String parentName = result.has("parent") ? result.get("parent").getAsJsonObject().get("full_name").getAsString(): null; + boolean fork = result.get("fork").getAsBoolean(); + return new RepositoryInfo(name, cloneUrl, ownerName, parentName, fork); + } + + @Nullable + private static RepositoryInfo getDetailedRepoInfo(@NotNull String url, @NotNull String login, @NotNull String password, + @NotNull String owner, @NotNull String name) { + try { + final String request = "/repos/" + owner + "/" + name; + JsonElement jsonObject = getRequest(url, login, password, request); + if (jsonObject == null) { + LOG.info(String.format("Information about repository is unavailable. Owner: %s, Name: %s", owner, name)); return null; } - return (new RepositoryInfo(element)); + return parseSingleRepositoryInfo(jsonObject.getAsJsonObject()); } - catch (Exception e) { - // ignore + catch (IOException e) { + LOG.info(String.format("Exception was thrown when trying to retrieve information about repository. Owner: %s, Name: %s", + owner, name)); + return null; } - finally { - if (method != null){ - method.releaseConnection(); - } - } - return null; } public static boolean isPrivateRepoAllowed(final String url, final String login, final String password) { - HttpMethod method = null; - try { - final String request = "/user/show/" + login; - method = doREST(url, login, password, request, false); - final InputStream stream = method.getResponseBodyAsStream(); - final Element element = new SAXBuilder(false).build(stream).getRootElement(); - if ("error".equals(element.getName())){ - LOG.warn("Got error element by request: " + request); - return false; - } - final Element plan = element.getChild("plan"); - assert plan != null : "Authentification failed"; - final String privateRepos = plan.getChildText("private-repos"); - return privateRepos != null && Integer.valueOf(privateRepos) > 0; + GithubUser user = retrieveCurrentUserInfo(url, login, password); + if (user == null) { + return false; } - catch (Exception e) { - // ignore - } - finally { - if (method != null){ - method.releaseConnection(); - } - } - return false; + return user.getPlan().isPrivateRepoAllowed(); } public static boolean checkCredentials(final Project project) { @@ -291,6 +420,7 @@ public class GithubUtil { // Otherwise our credentials are valid and they are successfully stored in settings final String validPassword = settings.getPassword(); return accessToGithubWithModalProgress(project, new Computable() { + @Nullable @Override public RepositoryInfo compute() { ProgressManager.getInstance().getProgressIndicator().setText("Extracting detailed info about repository ''" + name + "''"); @@ -327,7 +457,7 @@ public class GithubUtil { } return null; } - + public static boolean testGitExecutable(final Project project) { final GitVcsApplicationSettings settings = GitVcsApplicationSettings.getInstance(); final String executable = settings.getPathToGit(); diff --git a/plugins/github/src/org/jetbrains/plugins/github/RepositoryInfo.java b/plugins/github/src/org/jetbrains/plugins/github/RepositoryInfo.java index f9c6632466f4..b59b0addfea0 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/RepositoryInfo.java +++ b/plugins/github/src/org/jetbrains/plugins/github/RepositoryInfo.java @@ -1,55 +1,59 @@ package org.jetbrains.plugins.github; -import com.intellij.openapi.util.Comparing; -import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** -* @author oleg -* @date 10/21/10 -*/ + * Information about Github repository. + * + * @author oleg + * @author Kirill Likhodedov + */ public class RepositoryInfo { - private final Element myRepository; - public RepositoryInfo(final Element repository) { - myRepository = repository; + @NotNull private final String myName; + @NotNull private final String myCloneUrl; + @NotNull private final String myOwnerName; + @Nullable private final String myParentName; + private final boolean myFork; + + public RepositoryInfo(@NotNull String name, @NotNull String cloneUrl, @NotNull String ownerName, @Nullable String parentName, + boolean fork) { + myName = name; + myParentName = parentName; + myCloneUrl = cloneUrl; + myOwnerName = ownerName; + myFork = fork; } + @NotNull public String getName() { - return myRepository.getChildText("name"); + return myName; } - public String getOwner() { - return myRepository.getChildText("owner"); + @NotNull + public String getOwnerName() { + return myOwnerName; } public boolean isFork() { - return Boolean.valueOf(myRepository.getChildText("fork")); + return myFork; } - public String getParent() { - return myRepository.getChildText("parent"); + /** + * @return The name of the parent of this repository, or null. + * Null is returned if this repository doesn't have a parent, i. e. is not a fork, + * or if the parent information was not retrieved by the time of constructing of this RepositoryInfo object. + * To be sure use {@link #isFork()}. + */ + @Nullable + public String getParentName() { + return myParentName; } - public String getId() { - return getOwner() + "/" + getName(); + @NotNull + public String getCloneUrl() { + return myCloneUrl; } - public String getUrl() { - return myRepository.getChildText("url") + ".git"; - } - - @Override - public int hashCode() { - return myRepository != null ? myRepository.hashCode() : 0; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof RepositoryInfo)){ - return false; - } - final RepositoryInfo repositoryInfo = (RepositoryInfo)obj; - return Comparing.equal(getName(), repositoryInfo.getName()) && - Comparing.equal(getOwner(), repositoryInfo.getOwner()); - } }