| /* |
| * Copyright 2000-2014 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.util; |
| |
| import com.intellij.concurrency.JobScheduler; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.application.ApplicationManager; |
| 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.Project; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.ThrowableComputable; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.util.ConcurrencyUtil; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.ThrowableConsumer; |
| import com.intellij.util.ThrowableConvertor; |
| import com.intellij.util.containers.Convertor; |
| import git4idea.DialogManager; |
| import git4idea.GitUtil; |
| import git4idea.commands.GitCommand; |
| import git4idea.commands.GitSimpleHandler; |
| import git4idea.config.GitVcsApplicationSettings; |
| import git4idea.config.GitVersion; |
| import git4idea.i18n.GitBundle; |
| import git4idea.repo.GitRemote; |
| import git4idea.repo.GitRepository; |
| import git4idea.repo.GitRepositoryManager; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.plugins.github.api.GithubApiUtil; |
| import org.jetbrains.plugins.github.api.GithubConnection; |
| import org.jetbrains.plugins.github.api.GithubFullPath; |
| import org.jetbrains.plugins.github.api.GithubUserDetailed; |
| import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException; |
| import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; |
| import org.jetbrains.plugins.github.exceptions.GithubTwoFactorAuthenticationException; |
| import org.jetbrains.plugins.github.ui.GithubBasicLoginDialog; |
| import org.jetbrains.plugins.github.ui.GithubLoginDialog; |
| |
| import java.io.IOException; |
| import java.net.UnknownHostException; |
| import java.util.List; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Various utility methods for the GutHub plugin. |
| * |
| * @author oleg |
| * @author Kirill Likhodedov |
| * @author Aleksey Pivovarov |
| */ |
| public class GithubUtil { |
| |
| public static final Logger LOG = Logger.getInstance("github"); |
| |
| // TODO: Consider sharing of GithubAuthData between actions (as member of GithubSettings) |
| public static <T> T runTask(@NotNull Project project, |
| @NotNull GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull ThrowableConvertor<GithubConnection, T, IOException> task) throws IOException { |
| GithubAuthData auth = authHolder.getAuthData(); |
| try { |
| final GithubConnection connection = new GithubConnection(auth, true); |
| ScheduledFuture<?> future = null; |
| |
| try { |
| future = addCancellationListener(indicator, connection); |
| return task.convert(connection); |
| } |
| finally { |
| connection.close(); |
| if (future != null) future.cancel(true); |
| } |
| } |
| catch (GithubTwoFactorAuthenticationException e) { |
| getTwoFactorAuthData(project, authHolder, indicator, auth); |
| return runTask(project, authHolder, indicator, task); |
| } |
| catch (GithubAuthenticationException e) { |
| getValidAuthData(project, authHolder, indicator, auth); |
| return runTask(project, authHolder, indicator, task); |
| } |
| } |
| |
| public static void runTask(@NotNull Project project, |
| @NotNull GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull ThrowableConsumer<GithubConnection, IOException> task) throws IOException { |
| GithubAuthData auth = authHolder.getAuthData(); |
| try { |
| final GithubConnection connection = new GithubConnection(auth, true); |
| ScheduledFuture<?> future = null; |
| |
| try { |
| future = addCancellationListener(indicator, connection); |
| task.consume(connection); |
| } |
| finally { |
| connection.close(); |
| if (future != null) future.cancel(true); |
| } |
| } |
| catch (GithubTwoFactorAuthenticationException e) { |
| getTwoFactorAuthData(project, authHolder, indicator, auth); |
| runTask(project, authHolder, indicator, task); |
| } |
| catch (GithubAuthenticationException e) { |
| getValidAuthData(project, authHolder, indicator, auth); |
| runTask(project, authHolder, indicator, task); |
| } |
| } |
| |
| public static <T> T runTaskWithBasicAuthForHost(@NotNull Project project, |
| @NotNull GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull String host, |
| @NotNull ThrowableConvertor<GithubConnection, T, IOException> task) throws IOException { |
| GithubAuthData auth = authHolder.getAuthData(); |
| try { |
| if (auth.getAuthType() != GithubAuthData.AuthType.BASIC) { |
| throw new GithubAuthenticationException("Expected basic authentication"); |
| } |
| |
| final GithubConnection connection = new GithubConnection(auth, true); |
| ScheduledFuture<?> future = null; |
| |
| try { |
| future = addCancellationListener(indicator, connection); |
| return task.convert(connection); |
| } |
| finally { |
| connection.close(); |
| if (future != null) future.cancel(true); |
| } |
| } |
| catch (GithubTwoFactorAuthenticationException e) { |
| getTwoFactorAuthData(project, authHolder, indicator, auth); |
| return runTaskWithBasicAuthForHost(project, authHolder, indicator, host, task); |
| } |
| catch (GithubAuthenticationException e) { |
| getValidBasicAuthDataForHost(project, authHolder, indicator, auth, host); |
| return runTaskWithBasicAuthForHost(project, authHolder, indicator, host, task); |
| } |
| } |
| |
| @NotNull |
| private static GithubUserDetailed testConnection(@NotNull Project project, |
| @NotNull GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator) throws IOException { |
| GithubAuthData auth = authHolder.getAuthData(); |
| try { |
| final GithubConnection connection = new GithubConnection(auth, true); |
| ScheduledFuture<?> future = null; |
| |
| try { |
| future = addCancellationListener(indicator, connection); |
| return GithubApiUtil.getCurrentUserDetailed(connection); |
| } |
| finally { |
| connection.close(); |
| if (future != null) future.cancel(true); |
| } |
| } |
| catch (GithubTwoFactorAuthenticationException e) { |
| getTwoFactorAuthData(project, authHolder, indicator, auth); |
| return testConnection(project, authHolder, indicator); |
| } |
| } |
| |
| @NotNull |
| private static ScheduledFuture<?> addCancellationListener(@NotNull Runnable run) { |
| return JobScheduler.getScheduler().scheduleWithFixedDelay(run, 1000, 300, TimeUnit.MILLISECONDS); |
| } |
| |
| @NotNull |
| private static ScheduledFuture<?> addCancellationListener(@NotNull final ProgressIndicator indicator, |
| @NotNull final GithubConnection connection) { |
| return addCancellationListener(new Runnable() { |
| @Override |
| public void run() { |
| if (indicator.isCanceled()) connection.abort(); |
| } |
| }); |
| } |
| |
| @NotNull |
| private static ScheduledFuture<?> addCancellationListener(@NotNull final ProgressIndicator indicator, |
| @NotNull final Thread thread) { |
| return addCancellationListener(new Runnable() { |
| @Override |
| public void run() { |
| if (indicator.isCanceled()) thread.interrupt(); |
| } |
| }); |
| } |
| |
| public static void getValidAuthData(@NotNull final Project project, |
| @NotNull final GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull final GithubAuthData oldAuth) throws GithubOperationCanceledException { |
| authHolder.runTransaction(oldAuth, new ThrowableComputable<GithubAuthData, GithubOperationCanceledException>() { |
| @Override |
| @NotNull |
| public GithubAuthData compute() throws GithubOperationCanceledException { |
| final GithubAuthData[] authData = new GithubAuthData[1]; |
| final boolean[] ok = new boolean[1]; |
| ApplicationManager.getApplication().invokeAndWait(new Runnable() { |
| @Override |
| public void run() { |
| final GithubLoginDialog dialog = new GithubLoginDialog(project, oldAuth); |
| DialogManager.show(dialog); |
| ok[0] = dialog.isOK(); |
| |
| if (ok[0]) { |
| authData[0] = dialog.getAuthData(); |
| GithubSettings.getInstance().setAuthData(authData[0], dialog.isSavePasswordSelected()); |
| } |
| } |
| }, indicator.getModalityState()); |
| if (!ok[0]) { |
| throw new GithubOperationCanceledException("Can't get valid credentials"); |
| } |
| return authData[0]; |
| } |
| }); |
| } |
| |
| public static void getValidBasicAuthDataForHost(@NotNull final Project project, |
| @NotNull final GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull final GithubAuthData oldAuth, |
| @NotNull final String host) throws GithubOperationCanceledException { |
| authHolder.runTransaction(oldAuth, new ThrowableComputable<GithubAuthData, GithubOperationCanceledException>() { |
| @Override |
| @NotNull |
| public GithubAuthData compute() throws GithubOperationCanceledException { |
| final GithubAuthData[] authData = new GithubAuthData[1]; |
| final boolean[] ok = new boolean[1]; |
| ApplicationManager.getApplication().invokeAndWait(new Runnable() { |
| @Override |
| public void run() { |
| final GithubLoginDialog dialog = new GithubBasicLoginDialog(project, oldAuth, host); |
| DialogManager.show(dialog); |
| ok[0] = dialog.isOK(); |
| if (ok[0]) { |
| authData[0] = dialog.getAuthData(); |
| |
| final GithubSettings settings = GithubSettings.getInstance(); |
| if (settings.getAuthType() != GithubAuthData.AuthType.TOKEN) { |
| GithubSettings.getInstance().setAuthData(authData[0], dialog.isSavePasswordSelected()); |
| } |
| } |
| } |
| }, indicator.getModalityState()); |
| if (!ok[0]) { |
| throw new GithubOperationCanceledException("Can't get valid credentials"); |
| } |
| return authData[0]; |
| } |
| }); |
| } |
| |
| private static void getTwoFactorAuthData(@NotNull final Project project, |
| @NotNull final GithubAuthDataHolder authHolder, |
| @NotNull final ProgressIndicator indicator, |
| @NotNull final GithubAuthData oldAuth) throws GithubOperationCanceledException { |
| authHolder.runTransaction(oldAuth, new ThrowableComputable<GithubAuthData, GithubOperationCanceledException>() { |
| @Override |
| @NotNull |
| public GithubAuthData compute() throws GithubOperationCanceledException { |
| if (authHolder.getAuthData().getAuthType() != GithubAuthData.AuthType.BASIC) { |
| throw new GithubOperationCanceledException("Two factor authentication can be used only with Login/Password"); |
| } |
| |
| GithubApiUtil.askForTwoFactorCodeSMS(new GithubConnection(oldAuth, false)); |
| |
| final Ref<String> codeRef = new Ref<String>(); |
| ApplicationManager.getApplication().invokeAndWait(new Runnable() { |
| @Override |
| public void run() { |
| codeRef.set(Messages.showInputDialog(project, "Authentication Code", "Github Two-Factor Authentication", null)); |
| } |
| }, indicator.getModalityState()); |
| if (codeRef.isNull()) { |
| throw new GithubOperationCanceledException("Can't get two factor authentication code"); |
| } |
| |
| GithubSettings settings = GithubSettings.getInstance(); |
| if (settings.getAuthType() == GithubAuthData.AuthType.BASIC && |
| StringUtil.equalsIgnoreCase(settings.getLogin(), oldAuth.getBasicAuth().getLogin())) { |
| settings.setValidGitAuth(false); |
| } |
| |
| return oldAuth.copyWithTwoFactorCode(codeRef.get()); |
| } |
| }); |
| } |
| |
| @NotNull |
| public static GithubAuthDataHolder getValidAuthDataHolderFromConfig(@NotNull Project project, @NotNull ProgressIndicator indicator) |
| throws IOException { |
| GithubAuthData auth = GithubAuthData.createFromSettings(); |
| GithubAuthDataHolder authHolder = new GithubAuthDataHolder(auth); |
| try { |
| checkAuthData(project, authHolder, indicator); |
| return authHolder; |
| } |
| catch (GithubAuthenticationException e) { |
| getValidAuthData(project, authHolder, indicator, auth); |
| return authHolder; |
| } |
| } |
| |
| @NotNull |
| public static GithubUserDetailed checkAuthData(@NotNull Project project, |
| @NotNull GithubAuthDataHolder authHolder, |
| @NotNull ProgressIndicator indicator) throws IOException { |
| GithubAuthData auth = authHolder.getAuthData(); |
| |
| if (StringUtil.isEmptyOrSpaces(auth.getHost())) { |
| throw new GithubAuthenticationException("Target host not defined"); |
| } |
| |
| switch (auth.getAuthType()) { |
| case BASIC: |
| GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth(); |
| assert basicAuth != null; |
| if (StringUtil.isEmptyOrSpaces(basicAuth.getLogin()) || StringUtil.isEmptyOrSpaces(basicAuth.getPassword())) { |
| throw new GithubAuthenticationException("Empty login or password"); |
| } |
| break; |
| case TOKEN: |
| GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth(); |
| assert tokenAuth != null; |
| if (StringUtil.isEmptyOrSpaces(tokenAuth.getToken())) { |
| throw new GithubAuthenticationException("Empty token"); |
| } |
| break; |
| case ANONYMOUS: |
| throw new GithubAuthenticationException("Anonymous connection not allowed"); |
| } |
| |
| return testConnection(project, authHolder, indicator); |
| } |
| |
| public static <T> T computeValueInModal(@NotNull Project project, |
| @NotNull String caption, |
| @NotNull final ThrowableConvertor<ProgressIndicator, T, IOException> task) throws IOException { |
| final Ref<T> dataRef = new Ref<T>(); |
| final Ref<Throwable> exceptionRef = new Ref<Throwable>(); |
| ProgressManager.getInstance().run(new Task.Modal(project, caption, true) { |
| public void run(@NotNull ProgressIndicator indicator) { |
| try { |
| dataRef.set(task.convert(indicator)); |
| } |
| catch (Throwable e) { |
| exceptionRef.set(e); |
| } |
| } |
| }); |
| if (!exceptionRef.isNull()) { |
| Throwable e = exceptionRef.get(); |
| if (e instanceof IOException) throw ((IOException)e); |
| if (e instanceof RuntimeException) throw ((RuntimeException)e); |
| if (e instanceof Error) throw ((Error)e); |
| throw new RuntimeException(e); |
| } |
| return dataRef.get(); |
| } |
| |
| public static <T> T computeValueInModal(@NotNull Project project, |
| @NotNull String caption, |
| @NotNull final Convertor<ProgressIndicator, T> task) { |
| return computeValueInModal(project, caption, true, task); |
| } |
| |
| public static <T> T computeValueInModal(@NotNull Project project, |
| @NotNull String caption, |
| boolean canBeCancelled, |
| @NotNull final Convertor<ProgressIndicator, T> task) { |
| final Ref<T> dataRef = new Ref<T>(); |
| final Ref<Throwable> exceptionRef = new Ref<Throwable>(); |
| ProgressManager.getInstance().run(new Task.Modal(project, caption, canBeCancelled) { |
| public void run(@NotNull ProgressIndicator indicator) { |
| try { |
| dataRef.set(task.convert(indicator)); |
| } |
| catch (Throwable e) { |
| exceptionRef.set(e); |
| } |
| } |
| }); |
| if (!exceptionRef.isNull()) { |
| Throwable e = exceptionRef.get(); |
| if (e instanceof RuntimeException) throw ((RuntimeException)e); |
| if (e instanceof Error) throw ((Error)e); |
| throw new RuntimeException(e); |
| } |
| return dataRef.get(); |
| } |
| |
| public static void computeValueInModal(@NotNull Project project, |
| @NotNull String caption, |
| @NotNull final Consumer<ProgressIndicator> task) { |
| computeValueInModal(project, caption, true, task); |
| } |
| |
| public static void computeValueInModal(@NotNull Project project, |
| @NotNull String caption, |
| boolean canBeCancelled, |
| @NotNull final Consumer<ProgressIndicator> task) { |
| final Ref<Throwable> exceptionRef = new Ref<Throwable>(); |
| ProgressManager.getInstance().run(new Task.Modal(project, caption, canBeCancelled) { |
| public void run(@NotNull ProgressIndicator indicator) { |
| try { |
| task.consume(indicator); |
| } |
| catch (Throwable e) { |
| exceptionRef.set(e); |
| } |
| } |
| }); |
| if (!exceptionRef.isNull()) { |
| Throwable e = exceptionRef.get(); |
| if (e instanceof RuntimeException) throw ((RuntimeException)e); |
| if (e instanceof Error) throw ((Error)e); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public static <T> T runInterruptable(@NotNull final ProgressIndicator indicator, |
| @NotNull ThrowableComputable<T, IOException> task) throws IOException { |
| ScheduledFuture<?> future = null; |
| try { |
| final Thread thread = Thread.currentThread(); |
| future = addCancellationListener(indicator, thread); |
| |
| return task.compute(); |
| } |
| finally { |
| if (future != null) future.cancel(true); |
| Thread.interrupted(); |
| } |
| } |
| |
| public static <T> T runInterruptable(@NotNull final ProgressIndicator indicator, @NotNull Computable<T> task) { |
| ScheduledFuture<?> future = null; |
| try { |
| final Thread thread = Thread.currentThread(); |
| future = addCancellationListener(indicator, thread); |
| |
| return task.compute(); |
| } |
| finally { |
| if (future != null) future.cancel(true); |
| Thread.interrupted(); |
| } |
| } |
| |
| public static void runInterruptable(@NotNull final ProgressIndicator indicator, @NotNull Runnable task) { |
| ScheduledFuture<?> future = null; |
| try { |
| final Thread thread = Thread.currentThread(); |
| future = addCancellationListener(indicator, thread); |
| |
| task.run(); |
| } |
| finally { |
| if (future != null) future.cancel(true); |
| Thread.interrupted(); |
| } |
| } |
| |
| /* |
| * Git utils |
| */ |
| |
| @Nullable |
| public static String findGithubRemoteUrl(@NotNull GitRepository repository) { |
| Pair<GitRemote, String> remote = findGithubRemote(repository); |
| if (remote == null) { |
| return null; |
| } |
| return remote.getSecond(); |
| } |
| |
| @Nullable |
| public static Pair<GitRemote, String> findGithubRemote(@NotNull GitRepository repository) { |
| Pair<GitRemote, String> githubRemote = null; |
| for (GitRemote gitRemote : repository.getRemotes()) { |
| for (String remoteUrl : gitRemote.getUrls()) { |
| if (GithubUrlUtil.isGithubUrl(remoteUrl)) { |
| final String remoteName = gitRemote.getName(); |
| if ("github".equals(remoteName) || "origin".equals(remoteName)) { |
| return Pair.create(gitRemote, remoteUrl); |
| } |
| if (githubRemote == null) { |
| githubRemote = Pair.create(gitRemote, remoteUrl); |
| } |
| break; |
| } |
| } |
| } |
| return githubRemote; |
| } |
| |
| @Nullable |
| public static String findUpstreamRemote(@NotNull GitRepository repository) { |
| for (GitRemote gitRemote : repository.getRemotes()) { |
| final String remoteName = gitRemote.getName(); |
| if ("upstream".equals(remoteName)) { |
| for (String remoteUrl : gitRemote.getUrls()) { |
| if (GithubUrlUtil.isGithubUrl(remoteUrl)) { |
| return remoteUrl; |
| } |
| } |
| return gitRemote.getFirstUrl(); |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| public static GitRemote findGithubRemote(@NotNull GitRepository gitRepository, @NotNull GithubFullPath path) { |
| for (GitRemote remote : gitRepository.getRemotes()) { |
| for (String url : remote.getUrls()) { |
| if (path.equals(GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url))) { |
| return remote; |
| } |
| } |
| } |
| return null; |
| } |
| |
| public static boolean testGitExecutable(final Project project) { |
| final GitVcsApplicationSettings settings = GitVcsApplicationSettings.getInstance(); |
| final String executable = settings.getPathToGit(); |
| final GitVersion version; |
| try { |
| version = GitVersion.identifyVersion(executable); |
| } |
| catch (Exception e) { |
| GithubNotifications.showErrorDialog(project, GitBundle.getString("find.git.error.title"), e); |
| return false; |
| } |
| |
| if (!version.isSupported()) { |
| GithubNotifications.showWarningDialog(project, GitBundle.message("find.git.unsupported.message", version.toString(), GitVersion.MIN), |
| GitBundle.getString("find.git.success.title")); |
| return false; |
| } |
| return true; |
| } |
| |
| public static boolean isRepositoryOnGitHub(@NotNull GitRepository repository) { |
| return findGithubRemoteUrl(repository) != null; |
| } |
| |
| public static void setVisibleEnabled(AnActionEvent e, boolean visible, boolean enabled) { |
| e.getPresentation().setVisible(visible); |
| e.getPresentation().setEnabled(enabled); |
| } |
| |
| @NotNull |
| public static String getErrorTextFromException(@NotNull Exception e) { |
| if (e instanceof UnknownHostException) { |
| return "Unknown host: " + e.getMessage(); |
| } |
| return e.getMessage(); |
| } |
| |
| @Nullable |
| public static GitRepository getGitRepository(@NotNull Project project, @Nullable VirtualFile file) { |
| GitRepositoryManager manager = GitUtil.getRepositoryManager(project); |
| List<GitRepository> repositories = manager.getRepositories(); |
| if (repositories.size() == 0) { |
| return null; |
| } |
| if (repositories.size() == 1) { |
| return repositories.get(0); |
| } |
| if (file != null) { |
| GitRepository repository = manager.getRepositoryForFile(file); |
| if (repository != null) { |
| return repository; |
| } |
| } |
| return manager.getRepositoryForFile(project.getBaseDir()); |
| } |
| |
| public static boolean addGithubRemote(@NotNull Project project, |
| @NotNull GitRepository repository, |
| @NotNull String remote, |
| @NotNull String url) { |
| final GitSimpleHandler handler = new GitSimpleHandler(project, repository.getRoot(), GitCommand.REMOTE); |
| handler.setSilent(true); |
| |
| try { |
| handler.addParameters("add", remote, url); |
| handler.run(); |
| if (handler.getExitCode() != 0) { |
| GithubNotifications.showError(project, "Can't add remote", "Failed to add GitHub remote: '" + url + "'. " + handler.getStderr()); |
| return false; |
| } |
| // catch newly added remote |
| repository.update(); |
| return true; |
| } |
| catch (VcsException e) { |
| GithubNotifications.showError(project, "Can't add remote", e); |
| return false; |
| } |
| } |
| } |