EA-80250 - ISE: GithubUtil.getErrorTextFromException
[idea/community.git] / plugins / github / src / org / jetbrains / plugins / github / util / GithubUtil.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.plugins.github.util;
17
18 import com.intellij.concurrency.JobScheduler;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.progress.ProgressIndicator;
22 import com.intellij.openapi.progress.ProgressManager;
23 import com.intellij.openapi.progress.Task;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.Messages;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.Ref;
28 import com.intellij.openapi.util.ThrowableComputable;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.util.Consumer;
33 import com.intellij.util.ThrowableConvertor;
34 import com.intellij.util.containers.Convertor;
35 import git4idea.DialogManager;
36 import git4idea.GitUtil;
37 import git4idea.commands.GitCommand;
38 import git4idea.commands.GitSimpleHandler;
39 import git4idea.config.GitVcsApplicationSettings;
40 import git4idea.config.GitVersion;
41 import git4idea.i18n.GitBundle;
42 import git4idea.repo.GitRemote;
43 import git4idea.repo.GitRepository;
44 import git4idea.repo.GitRepositoryManager;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47 import org.jetbrains.plugins.github.api.GithubApiUtil;
48 import org.jetbrains.plugins.github.api.GithubConnection;
49 import org.jetbrains.plugins.github.api.GithubUserDetailed;
50 import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException;
51 import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException;
52 import org.jetbrains.plugins.github.exceptions.GithubTwoFactorAuthenticationException;
53 import org.jetbrains.plugins.github.ui.GithubBasicLoginDialog;
54 import org.jetbrains.plugins.github.ui.GithubLoginDialog;
55
56 import java.io.IOException;
57 import java.net.URI;
58 import java.net.URISyntaxException;
59 import java.net.UnknownHostException;
60 import java.util.List;
61 import java.util.concurrent.ScheduledFuture;
62 import java.util.concurrent.TimeUnit;
63
64 /**
65  * Various utility methods for the GutHub plugin.
66  *
67  * @author oleg
68  * @author Kirill Likhodedov
69  * @author Aleksey Pivovarov
70  */
71 public class GithubUtil {
72
73   public static final Logger LOG = Logger.getInstance("github");
74
75   // TODO: Consider sharing of GithubAuthData between actions (as member of GithubSettings)
76   public static <T> T runTask(@NotNull Project project,
77                               @NotNull GithubAuthDataHolder authHolder,
78                               @NotNull final ProgressIndicator indicator,
79                               @NotNull ThrowableConvertor<GithubConnection, T, IOException> task) throws IOException {
80     GithubAuthData auth = authHolder.getAuthData();
81     try {
82       final GithubConnection connection = new GithubConnection(auth, true);
83       ScheduledFuture<?> future = null;
84
85       try {
86         future = addCancellationListener(indicator, connection);
87         return task.convert(connection);
88       }
89       finally {
90         connection.close();
91         if (future != null) future.cancel(true);
92       }
93     }
94     catch (GithubTwoFactorAuthenticationException e) {
95       getTwoFactorAuthData(project, authHolder, indicator, auth);
96       return runTask(project, authHolder, indicator, task);
97     }
98     catch (GithubAuthenticationException e) {
99       getValidAuthData(project, authHolder, indicator, auth);
100       return runTask(project, authHolder, indicator, task);
101     }
102   }
103
104   public static <T> T runTaskWithBasicAuthForHost(@NotNull Project project,
105                                                   @NotNull GithubAuthDataHolder authHolder,
106                                                   @NotNull final ProgressIndicator indicator,
107                                                   @NotNull String host,
108                                                   @NotNull ThrowableConvertor<GithubConnection, T, IOException> task) throws IOException {
109     GithubAuthData auth = authHolder.getAuthData();
110     try {
111       if (auth.getAuthType() != GithubAuthData.AuthType.BASIC) {
112         throw new GithubAuthenticationException("Expected basic authentication");
113       }
114
115       final GithubConnection connection = new GithubConnection(auth, true);
116       ScheduledFuture<?> future = null;
117
118       try {
119         future = addCancellationListener(indicator, connection);
120         return task.convert(connection);
121       }
122       finally {
123         connection.close();
124         if (future != null) future.cancel(true);
125       }
126     }
127     catch (GithubTwoFactorAuthenticationException e) {
128       getTwoFactorAuthData(project, authHolder, indicator, auth);
129       return runTaskWithBasicAuthForHost(project, authHolder, indicator, host, task);
130     }
131     catch (GithubAuthenticationException e) {
132       getValidBasicAuthDataForHost(project, authHolder, indicator, auth, host);
133       return runTaskWithBasicAuthForHost(project, authHolder, indicator, host, task);
134     }
135   }
136
137   @NotNull
138   private static GithubUserDetailed testConnection(@NotNull Project project,
139                                                    @NotNull GithubAuthDataHolder authHolder,
140                                                    @NotNull final ProgressIndicator indicator) throws IOException {
141     GithubAuthData auth = authHolder.getAuthData();
142     try {
143       final GithubConnection connection = new GithubConnection(auth, true);
144       ScheduledFuture<?> future = null;
145
146       try {
147         future = addCancellationListener(indicator, connection);
148         return GithubApiUtil.getCurrentUserDetailed(connection);
149       }
150       finally {
151         connection.close();
152         if (future != null) future.cancel(true);
153       }
154     }
155     catch (GithubTwoFactorAuthenticationException e) {
156       getTwoFactorAuthData(project, authHolder, indicator, auth);
157       return testConnection(project, authHolder, indicator);
158     }
159   }
160
161   @NotNull
162   private static ScheduledFuture<?> addCancellationListener(@NotNull Runnable run) {
163     return JobScheduler.getScheduler().scheduleWithFixedDelay(run, 1000, 300, TimeUnit.MILLISECONDS);
164   }
165
166   @NotNull
167   private static ScheduledFuture<?> addCancellationListener(@NotNull final ProgressIndicator indicator,
168                                                             @NotNull final GithubConnection connection) {
169     return addCancellationListener(() -> {
170       if (indicator.isCanceled()) connection.abort();
171     });
172   }
173
174   @NotNull
175   private static ScheduledFuture<?> addCancellationListener(@NotNull final ProgressIndicator indicator,
176                                                             @NotNull final Thread thread) {
177     return addCancellationListener(() -> {
178       if (indicator.isCanceled()) thread.interrupt();
179     });
180   }
181
182   public static void getValidAuthData(@NotNull final Project project,
183                                       @NotNull final GithubAuthDataHolder authHolder,
184                                       @NotNull final ProgressIndicator indicator,
185                                       @NotNull final GithubAuthData oldAuth) throws GithubOperationCanceledException {
186     authHolder.runTransaction(oldAuth, () -> {
187       final GithubAuthData[] authData = new GithubAuthData[1];
188       final boolean[] ok = new boolean[1];
189       ApplicationManager.getApplication().invokeAndWait(() -> {
190         final GithubLoginDialog dialog = new GithubLoginDialog(project, oldAuth);
191         DialogManager.show(dialog);
192         ok[0] = dialog.isOK();
193
194         if (ok[0]) {
195           authData[0] = dialog.getAuthData();
196           GithubSettings.getInstance().setAuthData(authData[0], dialog.isSavePasswordSelected());
197         }
198       }, indicator.getModalityState());
199       if (!ok[0]) {
200         throw new GithubOperationCanceledException("Can't get valid credentials");
201       }
202       return authData[0];
203     });
204   }
205
206   public static void getValidBasicAuthDataForHost(@NotNull final Project project,
207                                                   @NotNull final GithubAuthDataHolder authHolder,
208                                                   @NotNull final ProgressIndicator indicator,
209                                                   @NotNull final GithubAuthData oldAuth,
210                                                   @NotNull final String host) throws GithubOperationCanceledException {
211     authHolder.runTransaction(oldAuth, () -> {
212       final GithubAuthData[] authData = new GithubAuthData[1];
213       final boolean[] ok = new boolean[1];
214       ApplicationManager.getApplication().invokeAndWait(() -> {
215         final GithubLoginDialog dialog = new GithubBasicLoginDialog(project, oldAuth, host);
216         DialogManager.show(dialog);
217         ok[0] = dialog.isOK();
218         if (ok[0]) {
219           authData[0] = dialog.getAuthData();
220
221           final GithubSettings settings = GithubSettings.getInstance();
222           if (settings.getAuthType() != GithubAuthData.AuthType.TOKEN) {
223             GithubSettings.getInstance().setAuthData(authData[0], dialog.isSavePasswordSelected());
224           }
225         }
226       }, indicator.getModalityState());
227       if (!ok[0]) {
228         throw new GithubOperationCanceledException("Can't get valid credentials");
229       }
230       return authData[0];
231     });
232   }
233
234   private static void getTwoFactorAuthData(@NotNull final Project project,
235                                            @NotNull final GithubAuthDataHolder authHolder,
236                                            @NotNull final ProgressIndicator indicator,
237                                            @NotNull final GithubAuthData oldAuth) throws GithubOperationCanceledException {
238     authHolder.runTransaction(oldAuth, () -> {
239       if (authHolder.getAuthData().getAuthType() != GithubAuthData.AuthType.BASIC) {
240         throw new GithubOperationCanceledException("Two factor authentication can be used only with Login/Password");
241       }
242
243       GithubApiUtil.askForTwoFactorCodeSMS(new GithubConnection(oldAuth, false));
244
245       final Ref<String> codeRef = new Ref<String>();
246       ApplicationManager.getApplication().invokeAndWait(() -> {
247         codeRef.set(Messages.showInputDialog(project, "Authentication Code", "Github Two-Factor Authentication", null));
248       }, indicator.getModalityState());
249       if (codeRef.isNull()) {
250         throw new GithubOperationCanceledException("Can't get two factor authentication code");
251       }
252
253       GithubSettings settings = GithubSettings.getInstance();
254       if (settings.getAuthType() == GithubAuthData.AuthType.BASIC &&
255           StringUtil.equalsIgnoreCase(settings.getLogin(), oldAuth.getBasicAuth().getLogin())) {
256         settings.setValidGitAuth(false);
257       }
258
259       return oldAuth.copyWithTwoFactorCode(codeRef.get());
260     });
261   }
262
263   @NotNull
264   public static GithubAuthDataHolder getValidAuthDataHolderFromConfig(@NotNull Project project, @NotNull ProgressIndicator indicator)
265     throws IOException {
266     GithubAuthData auth = GithubAuthData.createFromSettings();
267     GithubAuthDataHolder authHolder = new GithubAuthDataHolder(auth);
268     try {
269       checkAuthData(project, authHolder, indicator);
270       return authHolder;
271     }
272     catch (GithubAuthenticationException e) {
273       getValidAuthData(project, authHolder, indicator, auth);
274       return authHolder;
275     }
276   }
277
278   @NotNull
279   public static GithubUserDetailed checkAuthData(@NotNull Project project,
280                                                  @NotNull GithubAuthDataHolder authHolder,
281                                                  @NotNull ProgressIndicator indicator) throws IOException {
282     GithubAuthData auth = authHolder.getAuthData();
283
284     if (StringUtil.isEmptyOrSpaces(auth.getHost())) {
285       throw new GithubAuthenticationException("Target host not defined");
286     }
287
288     try {
289       new URI(auth.getHost());
290     }
291     catch (URISyntaxException e) {
292       throw new GithubAuthenticationException("Invalid host URL");
293     }
294
295     switch (auth.getAuthType()) {
296       case BASIC:
297         GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth();
298         assert basicAuth != null;
299         if (StringUtil.isEmptyOrSpaces(basicAuth.getLogin()) || StringUtil.isEmptyOrSpaces(basicAuth.getPassword())) {
300           throw new GithubAuthenticationException("Empty login or password");
301         }
302         break;
303       case TOKEN:
304         GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth();
305         assert tokenAuth != null;
306         if (StringUtil.isEmptyOrSpaces(tokenAuth.getToken())) {
307           throw new GithubAuthenticationException("Empty token");
308         }
309         break;
310       case ANONYMOUS:
311         throw new GithubAuthenticationException("Anonymous connection not allowed");
312     }
313
314     return testConnection(project, authHolder, indicator);
315   }
316
317   public static <T> T computeValueInModalIO(@NotNull Project project,
318                                             @NotNull String caption,
319                                             @NotNull final ThrowableConvertor<ProgressIndicator, T, IOException> task) throws IOException {
320     return ProgressManager.getInstance().run(new Task.WithResult<T, IOException>(project, caption, true) {
321       @Override
322       protected T compute(@NotNull ProgressIndicator indicator) throws IOException {
323         return task.convert(indicator);
324       }
325     });
326   }
327
328   public static <T> T computeValueInModal(@NotNull Project project,
329                                           @NotNull String caption,
330                                           @NotNull final Convertor<ProgressIndicator, T> task) {
331     return computeValueInModal(project, caption, true, task);
332   }
333
334   public static <T> T computeValueInModal(@NotNull Project project,
335                                           @NotNull String caption,
336                                           boolean canBeCancelled,
337                                           @NotNull final Convertor<ProgressIndicator, T> task) {
338     return ProgressManager.getInstance().run(new Task.WithResult<T, RuntimeException>(project, caption, canBeCancelled) {
339       @Override
340       protected T compute(@NotNull ProgressIndicator indicator) {
341         return task.convert(indicator);
342       }
343     });
344   }
345
346   public static void computeValueInModal(@NotNull Project project,
347                                          @NotNull String caption,
348                                          boolean canBeCancelled,
349                                          @NotNull final Consumer<ProgressIndicator> task) {
350     ProgressManager.getInstance().run(new Task.WithResult<Void, RuntimeException>(project, caption, canBeCancelled) {
351       @Override
352       protected Void compute(@NotNull ProgressIndicator indicator) {
353         task.consume(indicator);
354         return null;
355       }
356     });
357   }
358
359   public static <T> T runInterruptable(@NotNull final ProgressIndicator indicator,
360                                        @NotNull ThrowableComputable<T, IOException> task) throws IOException {
361     ScheduledFuture<?> future = null;
362     try {
363       final Thread thread = Thread.currentThread();
364       future = addCancellationListener(indicator, thread);
365
366       return task.compute();
367     }
368     finally {
369       if (future != null) future.cancel(true);
370       Thread.interrupted();
371     }
372   }
373
374   /*
375   * Git utils
376   */
377
378   @Nullable
379   public static String findGithubRemoteUrl(@NotNull GitRepository repository) {
380     Pair<GitRemote, String> remote = findGithubRemote(repository);
381     if (remote == null) {
382       return null;
383     }
384     return remote.getSecond();
385   }
386
387   @Nullable
388   public static Pair<GitRemote, String> findGithubRemote(@NotNull GitRepository repository) {
389     Pair<GitRemote, String> githubRemote = null;
390     for (GitRemote gitRemote : repository.getRemotes()) {
391       for (String remoteUrl : gitRemote.getUrls()) {
392         if (GithubUrlUtil.isGithubUrl(remoteUrl)) {
393           final String remoteName = gitRemote.getName();
394           if ("github".equals(remoteName) || "origin".equals(remoteName)) {
395             return Pair.create(gitRemote, remoteUrl);
396           }
397           if (githubRemote == null) {
398             githubRemote = Pair.create(gitRemote, remoteUrl);
399           }
400           break;
401         }
402       }
403     }
404     return githubRemote;
405   }
406
407   @Nullable
408   public static String findUpstreamRemote(@NotNull GitRepository repository) {
409     for (GitRemote gitRemote : repository.getRemotes()) {
410       final String remoteName = gitRemote.getName();
411       if ("upstream".equals(remoteName)) {
412         for (String remoteUrl : gitRemote.getUrls()) {
413           if (GithubUrlUtil.isGithubUrl(remoteUrl)) {
414             return remoteUrl;
415           }
416         }
417         return gitRemote.getFirstUrl();
418       }
419     }
420     return null;
421   }
422
423   public static boolean testGitExecutable(final Project project) {
424     final GitVcsApplicationSettings settings = GitVcsApplicationSettings.getInstance();
425     final String executable = settings.getPathToGit();
426     final GitVersion version;
427     try {
428       version = GitVersion.identifyVersion(executable);
429     }
430     catch (Exception e) {
431       GithubNotifications.showErrorDialog(project, GitBundle.getString("find.git.error.title"), e);
432       return false;
433     }
434
435     if (!version.isSupported()) {
436       GithubNotifications.showWarningDialog(project, GitBundle.message("find.git.unsupported.message", version.toString(), GitVersion.MIN),
437                                             GitBundle.getString("find.git.success.title"));
438       return false;
439     }
440     return true;
441   }
442
443   public static boolean isRepositoryOnGitHub(@NotNull GitRepository repository) {
444     return findGithubRemoteUrl(repository) != null;
445   }
446
447   @NotNull
448   public static String getErrorTextFromException(@NotNull Exception e) {
449     if (e instanceof UnknownHostException) {
450       return "Unknown host: " + e.getMessage();
451     }
452     return StringUtil.notNullize(e.getMessage(), "Unknown error");
453   }
454
455   @Nullable
456   public static GitRepository getGitRepository(@NotNull Project project, @Nullable VirtualFile file) {
457     GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
458     List<GitRepository> repositories = manager.getRepositories();
459     if (repositories.size() == 0) {
460       return null;
461     }
462     if (repositories.size() == 1) {
463       return repositories.get(0);
464     }
465     if (file != null) {
466       GitRepository repository = manager.getRepositoryForFile(file);
467       if (repository != null) {
468         return repository;
469       }
470     }
471     return manager.getRepositoryForFile(project.getBaseDir());
472   }
473
474   public static boolean addGithubRemote(@NotNull Project project,
475                                         @NotNull GitRepository repository,
476                                         @NotNull String remote,
477                                         @NotNull String url) {
478     final GitSimpleHandler handler = new GitSimpleHandler(project, repository.getRoot(), GitCommand.REMOTE);
479     handler.setSilent(true);
480
481     try {
482       handler.addParameters("add", remote, url);
483       handler.run();
484       if (handler.getExitCode() != 0) {
485         GithubNotifications.showError(project, "Can't add remote", "Failed to add GitHub remote: '" + url + "'. " + handler.getStderr());
486         return false;
487       }
488       // catch newly added remote
489       repository.update();
490       return true;
491     }
492     catch (VcsException e) {
493       GithubNotifications.showError(project, "Can't add remote", e);
494       return false;
495     }
496   }
497 }