diff options
Diffstat (limited to 'plugins/github/src/org/jetbrains')
26 files changed, 1910 insertions, 1218 deletions
diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java index 43753142a5b2..ae7b271ce3d6 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCreateGistAction.java @@ -38,6 +38,7 @@ import icons.GithubIcons; 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.GithubGist; import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; import org.jetbrains.plugins.github.ui.GithubCreateGistDialog; @@ -219,11 +220,11 @@ public class GithubCreateGistAction extends DumbAwareAction { } try { final List<FileContent> finalContents = contents; - return GithubUtil.runTask(project, auth, indicator, new ThrowableConvertor<GithubAuthData, GithubGist, IOException>() { + return GithubUtil.runTask(project, auth, indicator, new ThrowableConvertor<GithubConnection, GithubGist, IOException>() { @NotNull @Override - public GithubGist convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.createGist(auth, finalContents, description, isPrivate); + public GithubGist convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.createGist(connection, finalContents, description, isPrivate); } }).getHtmlUrl(); } diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestAction.java index 636415e5fcb6..d3fa265dc389 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestAction.java @@ -26,7 +26,7 @@ import icons.GithubIcons; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.ui.GithubCreatePullRequestDialog; -import org.jetbrains.plugins.github.util.*; +import org.jetbrains.plugins.github.util.GithubUtil; import static org.jetbrains.plugins.github.util.GithubUtil.setVisibleEnabled; @@ -73,12 +73,12 @@ public class GithubCreatePullRequestAction extends DumbAwareAction { } static void createPullRequest(@NotNull Project project, @Nullable VirtualFile file) { - GithubCreatePullRequestWorker worker = GithubCreatePullRequestWorker.createPullRequestWorker(project, file); + GithubCreatePullRequestWorker worker = GithubCreatePullRequestWorker.create(project, file); if (worker == null) { return; } - GithubCreatePullRequestDialog dialog = new GithubCreatePullRequestDialog(worker); + GithubCreatePullRequestDialog dialog = new GithubCreatePullRequestDialog(project, worker); DialogManager.show(dialog); } }
\ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java index bf9803b6f46f..07ed51bfca7d 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java @@ -1,30 +1,16 @@ -/* - * 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; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Couple; 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.vcs.changes.Change; @@ -34,11 +20,10 @@ import com.intellij.util.Function; import com.intellij.util.ThrowableConvertor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Convertor; -import com.intellij.util.containers.HashMap; +import com.intellij.vcs.log.VcsCommitMetadata; import git4idea.DialogManager; import git4idea.GitCommit; import git4idea.GitLocalBranch; -import git4idea.GitRemoteBranch; import git4idea.changes.GitChangeUtils; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; @@ -52,18 +37,19 @@ import git4idea.util.GitCommitCompareInfo; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.api.*; +import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; import org.jetbrains.plugins.github.ui.GithubSelectForkDialog; import org.jetbrains.plugins.github.util.*; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; -/** - * @author Aleksey Pivovarov - */ public class GithubCreatePullRequestWorker { private static final Logger LOG = GithubUtil.LOG; private static final String CANNOT_CREATE_PULL_REQUEST = "Can't Create Pull Request"; @@ -71,40 +57,36 @@ public class GithubCreatePullRequestWorker { @NotNull private final Project myProject; @NotNull private final Git myGit; @NotNull private final GitRepository myGitRepository; + @NotNull private final GithubAuthDataHolder myAuthHolder; + @NotNull private final GithubFullPath myPath; @NotNull private final String myRemoteName; @NotNull private final String myRemoteUrl; @NotNull private final String myCurrentBranch; - @NotNull private final GithubAuthDataHolder myAuthHolder; - @NotNull private final Map<String, FutureTask<DiffInfo>> myDiffInfos; + @NotNull private GithubFullPath mySource; - private volatile GithubFullPath myForkPath; - private volatile String myTargetRemote; + @NotNull private final List<ForkInfo> myForks; + @Nullable private List<GithubFullPath> myAvailableForks; private GithubCreatePullRequestWorker(@NotNull Project project, @NotNull Git git, @NotNull GitRepository gitRepository, + @NotNull GithubAuthDataHolder authHolder, @NotNull GithubFullPath path, @NotNull String remoteName, @NotNull String remoteUrl, - @NotNull String currentBranch, - @NotNull GithubAuthDataHolder authHolder) { + @NotNull String currentBranch) { myProject = project; myGit = git; myGitRepository = gitRepository; + myAuthHolder = authHolder; myPath = path; myRemoteName = remoteName; myRemoteUrl = remoteUrl; myCurrentBranch = currentBranch; - myAuthHolder = authHolder; - - myDiffInfos = new HashMap<String, FutureTask<DiffInfo>>(); - } - @NotNull - public Project getProject() { - return myProject; + myForks = new ArrayList<ForkInfo>(); } @NotNull @@ -112,231 +94,416 @@ public class GithubCreatePullRequestWorker { return myCurrentBranch; } - public boolean canShowDiff() { - return myTargetRemote != null; + @NotNull + public List<ForkInfo> getForks() { + return myForks; } @Nullable - public static GithubCreatePullRequestWorker createPullRequestWorker(@NotNull final Project project, @Nullable final VirtualFile file) { - Git git = ServiceManager.getService(Git.class); + public static GithubCreatePullRequestWorker create(@NotNull final Project project, @Nullable final VirtualFile file) { + return GithubUtil.computeValueInModal(project, "Loading data...", new Convertor<ProgressIndicator, GithubCreatePullRequestWorker>() { + @Override + public GithubCreatePullRequestWorker convert(ProgressIndicator indicator) { + Git git = ServiceManager.getService(Git.class); - GitRepository gitRepository = GithubUtil.getGitRepository(project, file); - if (gitRepository == null) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find git repository"); - return null; + GitRepository gitRepository = GithubUtil.getGitRepository(project, file); + if (gitRepository == null) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find git repository"); + return null; + } + gitRepository.update(); + + Pair<GitRemote, String> remote = GithubUtil.findGithubRemote(gitRepository); + if (remote == null) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find GitHub remote"); + return null; + } + String remoteName = remote.getFirst().getName(); + String remoteUrl = remote.getSecond(); + + GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(remoteUrl); + if (path == null) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't process remote: " + remoteUrl); + return null; + } + + GitLocalBranch currentBranch = gitRepository.getCurrentBranch(); + if (currentBranch == null) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "No current branch"); + return null; + } + + GithubAuthDataHolder authHolder; + try { + authHolder = GithubUtil.getValidAuthDataHolderFromConfig(project, indicator); + } + catch (IOException e) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e); + return null; + } + + GithubCreatePullRequestWorker worker = + new GithubCreatePullRequestWorker(project, git, gitRepository, authHolder, path, remoteName, remoteUrl, currentBranch.getName()); + + try { + worker.initForks(indicator); + } + catch (IOException e) { + GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e); + return null; + } + + return worker; + } + }); + } + + private void initForks(@NotNull ProgressIndicator indicator) throws IOException { + doLoadForksFromGithub(indicator); + doLoadForksFromGit(indicator); + doLoadForksFromSettings(indicator); + } + + @Nullable + private ForkInfo doAddFork(@NotNull GithubFullPath path, + @Nullable String remoteName, + @NotNull ProgressIndicator indicator) { + for (ForkInfo fork : myForks) { + if (fork.getPath().equals(path)) { + if (fork.getRemoteName() == null && remoteName != null) { + fork.setRemoteName(remoteName); + } + return fork; + } } - gitRepository.update(); - Pair<GitRemote, String> remote = GithubUtil.findGithubRemote(gitRepository); - if (remote == null) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find GitHub remote"); - return null; + try { + List<String> branches = loadBranches(path, indicator); + String defaultBranch = doLoadDefaultBranch(path, indicator); + + ForkInfo fork = new ForkInfo(path, branches, defaultBranch); + myForks.add(fork); + if (remoteName != null) { + fork.setRemoteName(remoteName); + } + return fork; } - String remoteName = remote.getFirst().getName(); - String remoteUrl = remote.getSecond(); - GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(remoteUrl); - if (path == null) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't process remote: " + remoteUrl); + catch (IOException e) { + GithubNotifications.showWarning(myProject, "Can't load branches for " + path.getFullName(), e); return null; } + } - GitLocalBranch currentBranch = gitRepository.getCurrentBranch(); - if (currentBranch == null) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "No current branch"); - return null; + @Nullable + private ForkInfo doAddFork(@NotNull GithubRepo repo, @NotNull ProgressIndicator indicator) { + GithubFullPath path = repo.getFullPath(); + for (ForkInfo fork : myForks) { + if (fork.getPath().equals(path)) { + return fork; + } } - GithubAuthDataHolder authHolder; try { - authHolder = GithubUtil - .computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubAuthDataHolder, IOException>() { - @NotNull - @Override - public GithubAuthDataHolder convert(ProgressIndicator indicator) throws IOException { - return GithubUtil.getValidAuthDataHolderFromConfig(project, indicator); - } - }); + List<String> branches = loadBranches(path, indicator); + String defaultBranch = repo.getDefaultBranch(); + + ForkInfo fork = new ForkInfo(path, branches, defaultBranch); + myForks.add(fork); + return fork; } catch (IOException e) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e); + GithubNotifications.showWarning(myProject, "Can't load branches for " + path.getFullName(), e); return null; } + } - return new GithubCreatePullRequestWorker(project, git, gitRepository, path, remoteName, remoteUrl, currentBranch.getName(), authHolder); + private void doLoadForksFromSettings(@NotNull ProgressIndicator indicator) throws IOException { + GithubFullPath savedRepo = GithubProjectSettings.getInstance(myProject).getCreatePullRequestDefaultRepo(); + if (savedRepo != null) { + doAddFork(savedRepo, null, indicator); + } } - @Nullable - public GithubTargetInfo setTarget(@NotNull final GithubFullPath forkPath) { - try { - GithubInfo info = - GithubUtil.computeValueInModal(myProject, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo, IOException>() { - @NotNull - @Override - public GithubInfo convert(ProgressIndicator indicator) throws IOException { - // configure remote - GitRemote targetRemote = GithubUtil.findGithubRemote(myGitRepository, forkPath); - String targetRemoteName = targetRemote == null ? null : targetRemote.getName(); - if (targetRemoteName == null) { - final Ref<Integer> responseRef = new Ref<Integer>(); - ApplicationManager.getApplication().invokeAndWait(new Runnable() { - @Override - public void run() { - responseRef.set(GithubNotifications - .showYesNoDialog(myProject, "Can't Find Remote", "Configure remote for '" + forkPath.getUser() + "'?")); - } - }, indicator.getModalityState()); - if (responseRef.get() == Messages.YES) { - targetRemoteName = configureRemote(myProject, myGitRepository, forkPath); - } - } + private void doLoadForksFromGit(@NotNull ProgressIndicator indicator) { + for (GitRemote remote : myGitRepository.getRemotes()) { + for (String url : remote.getUrls()) { + if (GithubUrlUtil.isGithubUrl(url)) { + GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url); + if (path != null) { + doAddFork(path, remote.getName(), indicator); + break; + } + } + } + } + } - // load available branches - List<String> branches = ContainerUtil.map(GithubUtil.runTask(myProject, myAuthHolder, indicator, - new ThrowableConvertor<GithubAuthData, List<GithubBranch>, IOException>() { - @Override - public List<GithubBranch> convert(@NotNull GithubAuthData auth) - throws IOException { - return GithubApiUtil.getRepoBranches(auth, forkPath.getUser(), - forkPath.getRepository()); - } - } - ), new Function<GithubBranch, String>() { - @Override - public String fun(GithubBranch githubBranch) { - return githubBranch.getName(); - } - }); + private void doLoadForksFromGithub(@NotNull ProgressIndicator indicator) throws IOException { + GithubRepoDetailed repo = + GithubUtil.runTask(myProject, myAuthHolder, indicator, new ThrowableConvertor<GithubConnection, GithubRepoDetailed, IOException>() { + @NotNull + @Override + public GithubRepoDetailed convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.getDetailedRepoInfo(connection, myPath.getUser(), myPath.getRepository()); + } + }); - // fetch - if (targetRemoteName != null) { - GitFetchResult result = new GitFetcher(myProject, indicator, false).fetch(myGitRepository.getRoot(), targetRemoteName, null); - if (!result.isSuccess()) { - GitFetcher.displayFetchResult(myProject, result, null, result.getErrors()); - targetRemoteName = null; - } - } + doAddFork(repo, indicator); + if (repo.getParent() != null) { + doAddFork(repo.getParent(), indicator); + } + if (repo.getSource() != null) { + doAddFork(repo.getSource(), indicator); + } - return new GithubInfo(branches, targetRemoteName); - } - }); + mySource = repo.getSource() == null ? repo.getFullPath() : repo.getSource().getFullPath(); + } - myForkPath = forkPath; - myTargetRemote = info.getTargetRemote(); - - myDiffInfos.clear(); - if (canShowDiff()) { - for (final String branch : info.getBranches()) { - myDiffInfos.put(branch, new FutureTask<DiffInfo>(new Callable<DiffInfo>() { - @Nullable - @Override - public DiffInfo call() throws Exception { - return loadDiffInfo(myProject, myGitRepository, myCurrentBranch, myTargetRemote + "/" + branch); - } - })); + @NotNull + private List<String> loadBranches(@NotNull final GithubFullPath fork, @NotNull ProgressIndicator indicator) throws IOException { + return ContainerUtil.map( + GithubUtil.runTask(myProject, myAuthHolder, indicator, new ThrowableConvertor<GithubConnection, List<GithubBranch>, IOException>() { + @Override + public List<GithubBranch> convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.getRepoBranches(connection, fork.getUser(), fork.getRepository()); + } + }), + new Function<GithubBranch, String>() { + @Override + public String fun(@NotNull GithubBranch branch) { + return branch.getName(); } } + ); + } + + @Nullable + private String doLoadDefaultBranch(@NotNull final GithubFullPath fork, @NotNull ProgressIndicator indicator) throws IOException { + GithubRepo repo = + GithubUtil.runTask(myProject, myAuthHolder, indicator, new ThrowableConvertor<GithubConnection, GithubRepo, IOException>() { + @Override + public GithubRepo convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.getDetailedRepoInfo(connection, fork.getUser(), fork.getRepository()); + } + }); + return repo.getDefaultBranch(); + } + + public void launchFetchRemote(@NotNull final ForkInfo fork) { + if (fork.getRemoteName() == null) return; + + if (fork.getFetchTask() != null) return; + synchronized (fork.LOCK) { + if (fork.getFetchTask() != null) return; - return new GithubTargetInfo(info.getBranches()); + final MasterFutureTask<Void> task = new MasterFutureTask<Void>(new Callable<Void>() { + @Override + public Void call() throws Exception { + doFetchRemote(fork); + return null; + } + }); + fork.setFetchTask(task); + + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + task.run(); + } + }); } - catch (IOException e) { - GithubNotifications.showErrorDialog(myProject, CANNOT_CREATE_PULL_REQUEST, e); + } + + public void launchLoadDiffInfo(@NotNull final BranchInfo branch) { + if (branch.getForkInfo().getRemoteName() == null) return; + + if (branch.getDiffInfoTask() != null) return; + synchronized (branch.LOCK) { + if (branch.getDiffInfoTask() != null) return; + + launchFetchRemote(branch.getForkInfo()); + MasterFutureTask<Void> masterTask = branch.getForkInfo().getFetchTask(); + assert masterTask != null; + + final SlaveFutureTask<DiffInfo> task = new SlaveFutureTask<DiffInfo>(masterTask, new Callable<DiffInfo>() { + @Override + public DiffInfo call() throws VcsException { + return doLoadDiffInfo(branch); + } + }); + branch.setDiffInfoTask(task); + + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + task.run(); + } + }); + } + } + + @Nullable + public DiffInfo getDiffInfo(@NotNull final BranchInfo branch) throws IOException { + if (branch.getForkInfo().getRemoteName() == null) return null; + + launchLoadDiffInfo(branch); + + assert branch.getDiffInfoTask() != null; + try { + return branch.getDiffInfoTask().get(); + } + catch (InterruptedException e) { + throw new GithubOperationCanceledException(e); + } + catch (ExecutionException e) { + Throwable ex = e.getCause(); + if (ex instanceof VcsException) throw new IOException(ex); + LOG.error(ex); return null; } } - public void showDiffDialog(@NotNull String branch) { - if (canShowDiff()) { - DiffInfo info = getDiffInfoWithModal(branch); - if (info == null) { - GithubNotifications.showErrorDialog(myProject, "Can't Show Diff", "Can't get diff info"); - return; - } + private boolean doFetchRemote(@NotNull ForkInfo fork) { + if (fork.getRemoteName() == null) return false; - GitCompareBranchesDialog dialog = - new GitCompareBranchesDialog(myProject, info.getTo(), info.getFrom(), info.getInfo(), myGitRepository); - dialog.show(); + GitFetchResult result = + new GitFetcher(myProject, new EmptyProgressIndicator(), false).fetch(myGitRepository.getRoot(), fork.getRemoteName(), null); + if (!result.isSuccess()) { + GitFetcher.displayFetchResult(myProject, result, null, result.getErrors()); + return false; } + return true; } - @Nullable - public GithubFullPath showTargetDialog() { - return showTargetDialog(false); + @NotNull + private DiffInfo doLoadDiffInfo(@NotNull final BranchInfo branch) throws VcsException { + // TODO: make cancelable and abort old speculative requests (when git4idea will allow to do so) + String currentBranch = myCurrentBranch; + String targetBranch = branch.getForkInfo().getRemoteName() + "/" + branch.getRemoteName(); + + List<GitCommit> commits1 = GitHistoryUtils.history(myProject, myGitRepository.getRoot(), ".." + targetBranch); + List<GitCommit> commits2 = GitHistoryUtils.history(myProject, myGitRepository.getRoot(), targetBranch + ".."); + Collection<Change> diff = GitChangeUtils.getDiff(myProject, myGitRepository.getRoot(), targetBranch, myCurrentBranch, null); + GitCommitCompareInfo info = new GitCommitCompareInfo(GitCommitCompareInfo.InfoType.BRANCH_TO_HEAD); + info.put(myGitRepository, diff); + info.put(myGitRepository, Couple.of(commits1, commits2)); + + return new DiffInfo(info, currentBranch, targetBranch); } - @Nullable - public GithubFullPath showTargetDialog(boolean firstTime) { - final GithubInfo2 info = getAvailableForksInModal(myProject, myGitRepository, myAuthHolder, myPath); - if (info == null) { - return null; + private void doConfigureRemote(@NotNull ForkInfo fork) { + if (fork.getRemoteName() != null) return; + + GithubFullPath path = fork.getPath(); + String url = GithubUrlUtil.getCloneUrl(path); + + if (GithubUtil.addGithubRemote(myProject, myGitRepository, path.getUser(), url)) { + fork.setRemoteName(path.getUser()); } + } - if (firstTime) { - if (info.getForks().size() == 1) { - return info.getForks().iterator().next(); + public void configureRemote(@NotNull final ForkInfo fork) { + GithubUtil.computeValueInModal(myProject, "Creating remote..", false, new Consumer<ProgressIndicator>() { + @Override + public void consume(ProgressIndicator indicator) { + doConfigureRemote(fork); } - if (info.getForks().size() == 2) { - Iterator<GithubFullPath> it = info.getForks().iterator(); - GithubFullPath path1 = it.next(); - GithubFullPath path2 = it.next(); + }); + } - if (myPath.equals(path1)) { - return path2; - } - if (myPath.equals(path2)) { - return path1; + @NotNull + public Couple<String> getDefaultDescriptionMessage(@NotNull final BranchInfo branch) { + Couple<String> message = branch.getDefaultMessage(); + if (message != null) return message; + + if (branch.getForkInfo().getRemoteName() == null) { + return getSimpleDefaultDescriptionMessage(branch); + } + + return GithubUtil + .computeValueInModal(myProject, "Collecting additional data...", false, new Convertor<ProgressIndicator, Couple<String>>() { + @Override + public Couple<String> convert(ProgressIndicator o) { + String localBranch = myCurrentBranch; + String targetBranch = branch.getForkInfo().getRemoteName() + "/" + branch.getRemoteName(); + try { + List<VcsCommitMetadata> commits = + GitHistoryUtils.readLastCommits(myProject, myGitRepository.getRoot(), localBranch, targetBranch); + if (commits == null) return getSimpleDefaultDescriptionMessage(branch); + + VcsCommitMetadata localCommit = commits.get(0); + VcsCommitMetadata targetCommit = commits.get(1); + + if (localCommit.getParents().contains(targetCommit.getId())) { + return Couple.of(localCommit.getSubject(), localCommit.getFullMessage()); + } + return getSimpleDefaultDescriptionMessage(branch); + } + catch (VcsException e) { + GithubNotifications.showWarning(myProject, "Can't collect additional data", e); + return getSimpleDefaultDescriptionMessage(branch); + } } - } + }); + } + + @NotNull + public Couple<String> getSimpleDefaultDescriptionMessage(@NotNull final BranchInfo branch) { + Couple<String> message = Couple.of(myCurrentBranch, ""); + branch.setDefaultMessage(message); + return message; + } + + public boolean checkAction(@Nullable final BranchInfo branch) { + if (branch == null) { + GithubNotifications.showWarningDialog(myProject, CANNOT_CREATE_PULL_REQUEST, "Target branch is not selected"); + return false; } - Convertor<String, GithubFullPath> getForkPath = new Convertor<String, GithubFullPath>() { - @Nullable - @Override - public GithubFullPath convert(@NotNull final String user) { - return GithubUtil.computeValueInModal(myProject, "Access to GitHub", new Convertor<ProgressIndicator, GithubFullPath>() { - @Nullable + DiffInfo info; + try { + info = GithubUtil + .computeValueInModal(myProject, "Collecting diff data...", new ThrowableConvertor<ProgressIndicator, DiffInfo, IOException>() { @Override - public GithubFullPath convert(ProgressIndicator indicator) { - return findRepositoryByUser(myProject, myAuthHolder, indicator, user, info.getForks(), info.getSource()); + public DiffInfo convert(ProgressIndicator indicator) throws IOException { + return GithubUtil.runInterruptable(indicator, new ThrowableComputable<DiffInfo, IOException>() { + @Override + public DiffInfo compute() throws IOException { + return getDiffInfo(branch); + } + }); } }); - } - }; - GithubSelectForkDialog dialog = new GithubSelectForkDialog(myProject, info.getForks(), getForkPath); - DialogManager.show(dialog); - if (!dialog.isOK()) { - return null; } - return dialog.getPath(); - } - - public boolean checkAction(@NotNull String targetBranch) { - DiffInfo info = getDiffInfoWithModal(targetBranch); + catch (IOException e) { + GithubNotifications.showError(myProject, "Can't collect diff data", e); + return true; + } if (info == null) { return true; } - String localBranchName = "'" + getCurrentBranch() + "'"; - String targetBranchName = "'" + myTargetRemote + ":" + targetBranch + "'"; + ForkInfo fork = branch.getForkInfo(); + + String localBranchName = "'" + myCurrentBranch + "'"; + String targetBranchName = "'" + fork.getRemoteName() + "/" + branch.getRemoteName() + "'"; if (info.getInfo().getBranchToHeadCommits(myGitRepository).isEmpty()) { - GithubNotifications - .showWarningDialog(myProject, CANNOT_CREATE_PULL_REQUEST, - "Can't create empty pull request: the branch " + localBranchName + - " is fully merged to the branch " + targetBranchName - ); - return false; + return Messages.YES == GithubNotifications + .showYesNoDialog(myProject, "Do you want to proceed anyway?", + "Empty pull request: the branch " + localBranchName + " is fully merged to the branch " + targetBranchName); } if (!info.getInfo().getHeadToBranchCommits(myGitRepository).isEmpty()) { - return GithubNotifications - .showYesNoDialog(myProject, "Do you want to proceed anyway?", - "The branch " + targetBranchName + " is not fully merged to the branch " + localBranchName) == Messages.YES; + return Messages.YES == GithubNotifications + .showYesNoDialog(myProject, "Do you want to proceed anyway?", + "The branch " + targetBranchName + " is not fully merged to the branch " + localBranchName); } return true; } - public void performAction(@NotNull final String title, @NotNull final String description, @NotNull final String targetBranch) { - @NotNull final Project project = myProject; - + public void createPullRequest(@NotNull final BranchInfo branch, + @NotNull final String title, + @NotNull final String description) { new Task.Backgroundable(myProject, "Creating pull request...") { @Override public void run(@NotNull ProgressIndicator indicator) { @@ -344,298 +511,322 @@ public class GithubCreatePullRequestWorker { indicator.setText("Pushing current branch..."); GitCommandResult result = myGit.push(myGitRepository, myRemoteName, myRemoteUrl, myCurrentBranch, true); if (!result.success()) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Push failed:<br/>" + result.getErrorOutputAsHtmlString()); + GithubNotifications.showError(myProject, CANNOT_CREATE_PULL_REQUEST, "Push failed:<br/>" + result.getErrorOutputAsHtmlString()); return; } - String headBranch = myPath.getUser() + ":" + myCurrentBranch; - LOG.info("Creating pull request"); indicator.setText("Creating pull request..."); - GithubPullRequest request = - createPullRequest(project, myAuthHolder, indicator, myForkPath, title, description, headBranch, targetBranch); + GithubPullRequest request = doCreatePullRequest(indicator, branch, title, description); if (request == null) { return; } - GithubNotifications - .showInfoURL(project, "Successfully created pull request", "Pull request #" + request.getNumber(), request.getHtmlUrl()); + GithubNotifications.showInfoURL(myProject, "Successfully created pull request", + "Pull request #" + request.getNumber(), request.getHtmlUrl()); } }.queue(); } @Nullable - private static String configureRemote(@NotNull Project project, @NotNull GitRepository gitRepository, @NotNull GithubFullPath forkPath) { - String url = GithubUrlUtil.getCloneUrl(forkPath); + private GithubPullRequest doCreatePullRequest(@NotNull ProgressIndicator indicator, + @NotNull final BranchInfo branch, + @NotNull final String title, + @NotNull final String description) { + final ForkInfo fork = branch.getForkInfo(); - if (GithubUtil.addGithubRemote(project, gitRepository, forkPath.getUser(), url)) { - return forkPath.getUser(); - } - else { - return null; - } - } + final String head = myPath.getUser() + ":" + myCurrentBranch; + final String base = branch.getRemoteName(); - @Nullable - private static GithubPullRequest createPullRequest(@NotNull Project project, - @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator, - @NotNull final GithubFullPath targetRepo, - @NotNull final String title, - @NotNull final String description, - @NotNull final String head, - @NotNull final String base) { try { - return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubPullRequest, IOException>() { - @NotNull - @Override - public GithubPullRequest convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.createPullRequest(auth, targetRepo.getUser(), targetRepo.getRepository(), title, description, head, base); - } - }); + return GithubUtil + .runTask(myProject, myAuthHolder, indicator, new ThrowableConvertor<GithubConnection, GithubPullRequest, IOException>() { + @NotNull + @Override + public GithubPullRequest convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil + .createPullRequest(connection, fork.getPath().getUser(), fork.getPath().getRepository(), title, description, head, base); + } + }); } catch (IOException e) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e); + GithubNotifications.showError(myProject, CANNOT_CREATE_PULL_REQUEST, e); return null; } } - @Nullable - private DiffInfo getDiffInfo(@NotNull String branch) { + public void showDiffDialog(@Nullable final BranchInfo branch) { + if (branch == null) { + GithubNotifications.showWarningDialog(myProject, "Can't Show Diff", "Target branch is not selected"); + return; + } + + DiffInfo info; try { - FutureTask<DiffInfo> future = myDiffInfos.get(branch); - if (future == null) { - return null; - } - future.run(); - return future.get(); + info = GithubUtil + .computeValueInModal(myProject, "Collecting diff data...", new ThrowableConvertor<ProgressIndicator, DiffInfo, IOException>() { + @Override + public DiffInfo convert(ProgressIndicator indicator) throws IOException { + return GithubUtil.runInterruptable(indicator, new ThrowableComputable<DiffInfo, IOException>() { + @Override + public DiffInfo compute() throws IOException { + return getDiffInfo(branch); + } + }); + } + }); } - catch (InterruptedException e) { - LOG.error(e); - return null; + catch (IOException e) { + GithubNotifications.showError(myProject, "Can't collect diff data", e); + return; } - catch (ExecutionException e) { - LOG.error(e); - return null; + if (info == null) { + GithubNotifications.showErrorDialog(myProject, "Can't Show Diff", "Can't collect diff data"); + return; } + + GitCompareBranchesDialog dialog = + new GitCompareBranchesDialog(myProject, info.getTo(), info.getFrom(), info.getInfo(), myGitRepository, true); + dialog.show(); } @Nullable - private DiffInfo getDiffInfoWithModal(@NotNull final String branch) { - return GithubUtil.computeValueInModal(myProject, "Collecting diff data...", new Convertor<ProgressIndicator, DiffInfo>() { - @Override - @Nullable - public DiffInfo convert(ProgressIndicator indicator) { - return getDiffInfo(branch); - } - }); - } + public ForkInfo showTargetDialog() { + if (myAvailableForks == null) { + myAvailableForks = GithubUtil + .computeValueInModal(myProject, myCurrentBranch, new Convertor<ProgressIndicator, List<GithubFullPath>>() { + @Override + public List<GithubFullPath> convert(ProgressIndicator indicator) { + return getAvailableForks(indicator); + } + }); + } - public void getDiffDescriptionInPooledThread(@NotNull final String branch, @NotNull final Consumer<DiffDescription> after) { - ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + Convertor<String, ForkInfo> getForkPath = new Convertor<String, ForkInfo>() { + @Nullable @Override - public void run() { - after.consume(getDefaultDescriptionMessage(branch, getDiffInfo(branch), myGitRepository)); + public ForkInfo convert(@NotNull final String user) { + return GithubUtil.computeValueInModal(myProject, "Access to GitHub", new Convertor<ProgressIndicator, ForkInfo>() { + @Nullable + @Override + public ForkInfo convert(ProgressIndicator indicator) { + return findRepositoryByUser(indicator, user); + } + }); } - }); + }; + GithubSelectForkDialog dialog = new GithubSelectForkDialog(myProject, myAvailableForks, getForkPath); + DialogManager.show(dialog); + if (!dialog.isOK()) { + return null; + } + return dialog.getPath(); } @Nullable - private static DiffInfo loadDiffInfo(@NotNull final Project project, - @NotNull final GitRepository repository, - @NotNull final String currentBranch, - @NotNull final String targetBranch) { + private List<GithubFullPath> getAvailableForks(@NotNull ProgressIndicator indicator) { try { - List<GitCommit> commits1 = GitHistoryUtils.history(project, repository.getRoot(), ".." + targetBranch); - List<GitCommit> commits2 = GitHistoryUtils.history(project, repository.getRoot(), targetBranch + ".."); - Collection<Change> diff = GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), targetBranch, currentBranch, null); - GitCommitCompareInfo info = new GitCommitCompareInfo(GitCommitCompareInfo.InfoType.BRANCH_TO_HEAD); - info.put(repository, diff); - info.put(repository, Couple.of(commits1, commits2)); - return new DiffInfo(info, currentBranch, targetBranch); - } - catch (VcsException e) { - LOG.info(e); + List<GithubFullPath> forks = ContainerUtil.map( + GithubUtil.runTask(myProject, myAuthHolder, indicator, + new ThrowableConvertor<GithubConnection, List<GithubRepo>, IOException>() { + @NotNull + @Override + public List<GithubRepo> convert(@NotNull GithubConnection connection) + throws IOException { + return GithubApiUtil.getForks(connection, mySource.getUser(), mySource.getRepository()); + } + } + ), + new Function<GithubRepo, GithubFullPath>() { + @Override + public GithubFullPath fun(GithubRepo repo) { + return repo.getFullPath(); + } + } + ); + if (!forks.contains(mySource)) forks.add(mySource); + return forks; + } + catch (IOException e) { + GithubNotifications.showWarning(myProject, "Can't load available forks", e); return null; } } - @NotNull - private static DiffDescription getDefaultDescriptionMessage(@NotNull String branch, - @Nullable DiffInfo info, - @NotNull GitRepository gitRepository) { - if (info == null) { - return new DiffDescription(branch, null, null); - } - - if (info.getInfo().getBranchToHeadCommits(gitRepository).size() != 1) { - return new DiffDescription(branch, info.getFrom(), null); + @Nullable + private ForkInfo findRepositoryByUser(@NotNull final ProgressIndicator indicator, @NotNull final String user) { + for (ForkInfo fork : myForks) { + if (StringUtil.equalsIgnoreCase(user, fork.getPath().getUser())) { + return fork; + } } - GitCommit commit = info.getInfo().getBranchToHeadCommits(gitRepository).get(0); - return new DiffDescription(branch, commit.getSubject(), commit.getFullMessage()); - } - - @Nullable - private static GithubInfo2 getAvailableForksInModal(@NotNull final Project project, - @NotNull final GitRepository gitRepository, - @NotNull final GithubAuthDataHolder authHolder, - @NotNull final GithubFullPath path) { try { - return GithubUtil - .computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo2, IOException>() { - @NotNull + GithubRepo repo = + GithubUtil.runTask(myProject, myAuthHolder, indicator, new ThrowableConvertor<GithubConnection, GithubRepo, IOException>() { + @Nullable @Override - public GithubInfo2 convert(ProgressIndicator indicator) throws IOException { - final Set<GithubFullPath> forks = new HashSet<GithubFullPath>(); - - // GitHub - GithubRepoDetailed repo = - GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubRepoDetailed, IOException>() { - @NotNull - @Override - public GithubRepoDetailed convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.getDetailedRepoInfo(auth, path.getUser(), path.getRepository()); - } - }); - - forks.add(path); - if (repo.getParent() != null) { - forks.add(repo.getParent().getFullPath()); + public GithubRepo convert(@NotNull GithubConnection connection) throws IOException { + try { + GithubRepoDetailed target = GithubApiUtil.getDetailedRepoInfo(connection, user, mySource.getRepository()); + if (target.getSource() != null && StringUtil.equals(target.getSource().getUserName(), mySource.getUser())) { + return target; + } } - if (repo.getSource() != null) { - forks.add(repo.getSource().getFullPath()); + catch (IOException ignore) { + // such repo may not exist } - // Git - forks.addAll(getAvailableForksFromGit(gitRepository)); - - GithubRepo forkTreeRoot = repo.getSource() == null ? repo : repo.getSource(); - return new GithubInfo2(forks, forkTreeRoot); + return GithubApiUtil.findForkByUser(connection, mySource.getUser(), mySource.getRepository(), user); } }); + + if (repo == null) return null; + return doAddFork(repo, indicator); } catch (IOException e) { - GithubNotifications.showErrorDialog(project, CANNOT_CREATE_PULL_REQUEST, e); + GithubNotifications.showError(myProject, "Can't find repository", e); return null; } } - @NotNull - private static List<GithubFullPath> getAvailableForksFromGit(@NotNull GitRepository gitRepository) { - List<GithubFullPath> forks = new ArrayList<GithubFullPath>(); - for (GitRemoteBranch remoteBranch : gitRepository.getBranches().getRemoteBranches()) { - for (String url : remoteBranch.getRemote().getUrls()) { - if (GithubUrlUtil.isGithubUrl(url)) { - GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url); - if (path != null) { - forks.add(path); - break; - } - } + public static class ForkInfo { + @NotNull public final Object LOCK = new Object(); + + // initial loading + @NotNull private final GithubFullPath myPath; + + @NotNull private final String myDefaultBranch; + @NotNull private final List<BranchInfo> myBranches; + + @Nullable private String myRemoteName; + private boolean myProposedToCreateRemote; + + @Nullable private MasterFutureTask<Void> myFetchTask; + + public ForkInfo(@NotNull GithubFullPath path, @NotNull List<String> branches, @Nullable String defaultBranch) { + myPath = path; + myDefaultBranch = defaultBranch == null ? "master" : defaultBranch; + myBranches = new ArrayList<BranchInfo>(); + for (String branchName : branches) { + myBranches.add(new BranchInfo(branchName, this)); } } - return forks; - } - @Nullable - private static GithubFullPath findRepositoryByUser(@NotNull Project project, - @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator, - @NotNull final String user, - @NotNull Set<GithubFullPath> forks, - @NotNull final GithubRepo source) { - for (GithubFullPath path : forks) { - if (StringUtil.equalsIgnoreCase(user, path.getUser())) { - return path; - } + @NotNull + public GithubFullPath getPath() { + return myPath; } - try { - return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubFullPath, IOException>() { - @Nullable - @Override - public GithubFullPath convert(@NotNull GithubAuthData auth) throws IOException { - try { - GithubRepoDetailed target = GithubApiUtil.getDetailedRepoInfo(auth, user, source.getName()); - if (target.getSource() != null && StringUtil.equals(target.getSource().getUserName(), source.getUserName())) { - return target.getFullPath(); - } - } - catch (IOException ignore) { - // such repo may not exist - } + @Nullable + public String getRemoteName() { + return myRemoteName; + } - GithubRepo fork = GithubApiUtil.findForkByUser(auth, source.getUserName(), source.getName(), user); - if (fork != null) { - return fork.getFullPath(); - } + @NotNull + public String getDefaultBranch() { + return myDefaultBranch; + } - return null; - } - }); + @NotNull + public List<BranchInfo> getBranches() { + return myBranches; } - catch (IOException e) { - GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e); + + public void setRemoteName(@NotNull String remoteName) { + myRemoteName = remoteName; } - return null; - } + public boolean isProposedToCreateRemote() { + return myProposedToCreateRemote; + } + + public void setProposedToCreateRemote(boolean proposedToCreateRemote) { + myProposedToCreateRemote = proposedToCreateRemote; + } - private static class GithubInfo { - @NotNull private final List<String> myBranches; - @Nullable private final String myTargetRemote; + @Nullable + public MasterFutureTask<Void> getFetchTask() { + return myFetchTask; + } - private GithubInfo(@NotNull List<String> repo, @Nullable String targetRemote) { - myBranches = repo; - myTargetRemote = targetRemote; + public void setFetchTask(@NotNull MasterFutureTask<Void> fetchTask) { + myFetchTask = fetchTask; } - @NotNull - public List<String> getBranches() { - return myBranches; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ForkInfo info = (ForkInfo)o; + + if (!myPath.equals(info.myPath)) return false; + + return true; } - @Nullable - public String getTargetRemote() { - return myTargetRemote; + @Override + public int hashCode() { + return myPath.hashCode(); + } + + @Override + public String toString() { + return myPath.getUser() + ":" + myPath.getRepository(); } } - private static class GithubInfo2 { - @NotNull private final Set<GithubFullPath> myForks; - @NotNull private final GithubRepo mySource; + public static class BranchInfo { + @NotNull public final Object LOCK = new Object(); - private GithubInfo2(@NotNull Set<GithubFullPath> forks, @NotNull GithubRepo source) { - myForks = forks; - mySource = source; + @NotNull private final ForkInfo myForkInfo; + @NotNull private final String myRemoteName; + + @Nullable private SlaveFutureTask<DiffInfo> myDiffInfoTask; + + @Nullable private Couple<String> myDefaultMessage; + + public BranchInfo(@NotNull String remoteName, @NotNull ForkInfo fork) { + myRemoteName = remoteName; + myForkInfo = fork; } @NotNull - public Set<GithubFullPath> getForks() { - return myForks; + public ForkInfo getForkInfo() { + return myForkInfo; } @NotNull - public GithubRepo getSource() { - return mySource; + public String getRemoteName() { + return myRemoteName; } - } - public static class GithubTargetInfo { - @NotNull private final List<String> myBranches; + @Nullable + public SlaveFutureTask<DiffInfo> getDiffInfoTask() { + return myDiffInfoTask; + } - private GithubTargetInfo(@NotNull List<String> branches) { - myBranches = branches; + public void setDiffInfoTask(@NotNull SlaveFutureTask<DiffInfo> diffInfoTask) { + myDiffInfoTask = diffInfoTask; } - @NotNull - public List<String> getBranches() { - return myBranches; + @Nullable + public Couple<String> getDefaultMessage() { + return myDefaultMessage; + } + + public void setDefaultMessage(@NotNull Couple<String> message) { + myDefaultMessage = message; + } + + @Override + public String toString() { + return myRemoteName; } } - private static class DiffInfo { + public static class DiffInfo { @NotNull private final GitCommitCompareInfo myInfo; @NotNull private final String myFrom; @NotNull private final String myTo; @@ -662,30 +853,86 @@ public class GithubCreatePullRequestWorker { } } - public static class DiffDescription { - @NotNull private final String myBranch; - @Nullable private final String myTitle; - @Nullable private final String myDescription; + public static class SlaveFutureTask<T> extends FutureTask<T> { + @NotNull private final MasterFutureTask myMaster; - public DiffDescription(@NotNull String branch, @Nullable String title, @Nullable String description) { - myBranch = branch; - myTitle = title; - myDescription = description; + public SlaveFutureTask(@NotNull MasterFutureTask master, @NotNull Callable<T> callable) { + super(callable); + myMaster = master; } - @NotNull - public String getBranch() { - return myBranch; + @Override + public void run() { + if (myMaster.isDone()) { + super.run(); + } + else { + if (!myMaster.addSlave(this)) { + super.run(); + } + } } - @Nullable - public String getTitle() { - return myTitle; + public T safeGet() { + try { + return super.get(); + } + catch (InterruptedException e) { + return null; + } + catch (CancellationException e) { + return null; + } + catch (ExecutionException e) { + return null; + } } + } - @Nullable - public String getDescription() { - return myDescription; + public static class MasterFutureTask<T> extends FutureTask<T> { + @NotNull private final Object LOCK = new Object(); + private boolean myDone = false; + + @Nullable private List<SlaveFutureTask> mySlaves; + + public MasterFutureTask(@NotNull Callable<T> callable) { + super(callable); + } + + boolean addSlave(@NotNull SlaveFutureTask slave) { + if (isDone()) { + return false; + } + else { + synchronized (LOCK) { + if (myDone) return false; + if (mySlaves == null) mySlaves = new ArrayList<SlaveFutureTask>(); + mySlaves.add(slave); + return true; + } + } + } + + @Override + protected void done() { + synchronized (LOCK) { + myDone = true; + if (mySlaves != null) { + for (final SlaveFutureTask slave : mySlaves) { + runSlave(slave); + } + mySlaves = null; + } + } + } + + protected void runSlave(@NotNull final SlaveFutureTask slave) { + ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { + @Override + public void run() { + slave.run(); + } + }); } } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java index 719f8b956432..23d34e3178d3 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubRebaseAction.java @@ -41,6 +41,7 @@ import icons.GithubIcons; 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.GithubRepoDetailed; import org.jetbrains.plugins.github.util.*; @@ -197,11 +198,11 @@ public class GithubRebaseAction extends DumbAwareAction { try { return GithubUtil.runTask(project, GithubAuthDataHolder.createFromSettings(), indicator, - new ThrowableConvertor<GithubAuthData, GithubRepoDetailed, IOException>() { + new ThrowableConvertor<GithubConnection, GithubRepoDetailed, IOException>() { @NotNull @Override - public GithubRepoDetailed convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.getDetailedRepoInfo(auth, userAndRepo.getUser(), userAndRepo.getRepository()); + public GithubRepoDetailed convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.getDetailedRepoInfo(connection, userAndRepo.getUser(), userAndRepo.getRepository()); } }); } diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java index cca5afa0f5e5..c372b09805a1 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShareAction.java @@ -52,6 +52,7 @@ import icons.GithubIcons; 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.GithubRepo; import org.jetbrains.plugins.github.api.GithubUserDetailed; import org.jetbrains.plugins.github.ui.GithubShareDialog; @@ -211,15 +212,15 @@ public class GithubShareAction extends DumbAwareAction { @Override public GithubInfo convert(ProgressIndicator indicator) throws IOException { // get existing github repos (network) and validate auth data - return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubInfo, IOException>() { + return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubConnection, GithubInfo, IOException>() { @NotNull @Override - public GithubInfo convert(@NotNull GithubAuthData auth) throws IOException { + public GithubInfo convert(@NotNull GithubConnection connection) throws IOException { // check access to private repos (network) - GithubUserDetailed userInfo = GithubApiUtil.getCurrentUserDetailed(auth); + GithubUserDetailed userInfo = GithubApiUtil.getCurrentUserDetailed(connection); HashSet<String> names = new HashSet<String>(); - for (GithubRepo info : GithubApiUtil.getUserRepos(auth)) { + for (GithubRepo info : GithubApiUtil.getUserRepos(connection)) { names.add(info.getName()); } return new GithubInfo(userInfo, names); @@ -243,11 +244,11 @@ public class GithubShareAction extends DumbAwareAction { final boolean isPrivate) { try { - return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubRepo, IOException>() { + return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubConnection, GithubRepo, IOException>() { @NotNull @Override - public GithubRepo convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.createRepo(auth, name, description, isPrivate); + public GithubRepo convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.createRepo(connection, name, description, isPrivate); } }).getHtmlUrl(); } diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiUtil.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiUtil.java index b8cf537f7709..063ad3bc7bb6 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiUtil.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiUtil.java @@ -1,60 +1,32 @@ -/* - * 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.api; import com.google.gson.*; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.net.HttpConfigurable; -import org.apache.commons.httpclient.*; -import org.apache.commons.httpclient.auth.AuthScope; -import org.apache.commons.httpclient.methods.*; -import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.message.BasicHeader; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.plugins.github.exceptions.*; -import org.jetbrains.plugins.github.util.GithubAuthData; -import org.jetbrains.plugins.github.util.GithubSettings; -import org.jetbrains.plugins.github.util.GithubUrlUtil; +import org.jetbrains.plugins.github.api.GithubConnection.PagedRequest; +import org.jetbrains.plugins.github.exceptions.GithubConfusingException; +import org.jetbrains.plugins.github.exceptions.GithubJsonException; +import org.jetbrains.plugins.github.exceptions.GithubStatusCodeException; import org.jetbrains.plugins.github.util.GithubUtil; -import sun.security.validator.ValidatorException; -import javax.net.ssl.SSLHandshakeException; -import java.awt.*; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; import java.net.URLEncoder; import java.util.*; -import java.util.List; -/** - * @author Kirill Likhodedov - */ public class GithubApiUtil { + private static final Logger LOG = GithubUtil.LOG; public static final String DEFAULT_GITHUB_HOST = "github.com"; private static final String PER_PAGE = "per_page=100"; - private static final Logger LOG = GithubUtil.LOG; - private static final Header ACCEPT_V3_JSON_HTML_MARKUP = new Header("Accept", "application/vnd.github.v3.html+json"); - private static final Header ACCEPT_V3_JSON = new Header("Accept", "application/vnd.github.v3+json"); + private static final Header ACCEPT_V3_JSON_HTML_MARKUP = new BasicHeader("Accept", "application/vnd.github.v3.html+json"); + private static final Header ACCEPT_V3_JSON = new BasicHeader("Accept", "application/vnd.github.v3+json"); @NotNull private static final Gson gson = initGson(); @@ -65,344 +37,8 @@ public class GithubApiUtil { return builder.create(); } - private enum HttpVerb { - GET, POST, DELETE, HEAD, PATCH - } - - @Nullable - private static JsonElement postRequest(@NotNull GithubAuthData auth, - @NotNull String path, - @Nullable String requestBody, - @NotNull Header... headers) throws IOException { - return request(auth, path, requestBody, Arrays.asList(headers), HttpVerb.POST).getJsonElement(); - } - - @Nullable - private static JsonElement patchRequest(@NotNull GithubAuthData auth, - @NotNull String path, - @Nullable String requestBody, - @NotNull Header... headers) throws IOException { - return request(auth, path, requestBody, Arrays.asList(headers), HttpVerb.PATCH).getJsonElement(); - } - - @Nullable - private static JsonElement deleteRequest(@NotNull GithubAuthData auth, @NotNull String path, @NotNull Header... headers) - throws IOException { - return request(auth, path, null, Arrays.asList(headers), HttpVerb.DELETE).getJsonElement(); - } - - @Nullable - private static JsonElement getRequest(@NotNull GithubAuthData auth, @NotNull String path, @NotNull Header... headers) throws IOException { - return request(auth, path, null, Arrays.asList(headers), HttpVerb.GET).getJsonElement(); - } - - @NotNull - private static ResponsePage request(@NotNull GithubAuthData auth, - @NotNull String path, - @Nullable String requestBody, - @NotNull Collection<Header> headers, - @NotNull HttpVerb verb) throws IOException { - if (EventQueue.isDispatchThread() && !ApplicationManager.getApplication().isUnitTestMode()) { - LOG.warn("Network operation in EDT"); // TODO: fix - } - - HttpMethod method = null; - try { - String uri = GithubUrlUtil.getApiUrl(auth.getHost()) + path; - method = doREST(auth, uri, requestBody, headers, verb); - - checkStatusCode(method, requestBody); - - InputStream resp = method.getResponseBodyAsStream(); - if (resp == null) { - return new ResponsePage(); - } - - JsonElement ret = parseResponse(resp); - if (ret.isJsonNull()) { - return new ResponsePage(); - } - - Header header = method.getResponseHeader("Link"); - if (header != null) { - String value = header.getValue(); - int end = value.indexOf(">; rel=\"next\""); - int begin = value.lastIndexOf('<', end); - if (begin >= 0 && end >= 0) { - String newPath = GithubUrlUtil.removeProtocolPrefix(value.substring(begin + 1, end)); - int index = newPath.indexOf('/'); - - return new ResponsePage(ret, newPath.substring(index)); - } - } - - return new ResponsePage(ret); - } - finally { - if (method != null) { - method.releaseConnection(); - } - } - } - - @NotNull - private static HttpMethod doREST(@NotNull final GithubAuthData auth, - @NotNull final String uri, - @Nullable final String requestBody, - @NotNull final Collection<Header> headers, - @NotNull final HttpVerb verb) throws IOException { - HttpClient client = getHttpClient(auth.getBasicAuth(), auth.isUseProxy()); - HttpMethod method; - switch (verb) { - case POST: - method = new PostMethod(uri); - if (requestBody != null) { - ((PostMethod)method).setRequestEntity(new StringRequestEntity(requestBody, "application/json", "UTF-8")); - } - break; - case PATCH: - method = new PostMethod(uri) { // TODO: httpclient 4.x - @Override - public String getName() { - return "PATCH"; - } - }; - if (requestBody != null) { - ((PostMethod)method).setRequestEntity(new StringRequestEntity(requestBody, "application/json", "UTF-8")); - } - break; - case GET: - method = new GetMethod(uri); - break; - case DELETE: - method = new DeleteMethod(uri); - break; - case HEAD: - method = new HeadMethod(uri); - break; - default: - throw new IllegalStateException("Wrong HttpVerb: unknown method: " + verb.toString()); - } - - GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth(); - if (tokenAuth != null) { - method.addRequestHeader("Authorization", "token " + tokenAuth.getToken()); - } - GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth(); - if (basicAuth != null && basicAuth.getCode() != null) { - method.addRequestHeader("X-GitHub-OTP", basicAuth.getCode()); - } - for (Header header : headers) { - method.addRequestHeader(header); - } - - try { - client.executeMethod(method); - } - catch (SSLHandshakeException e) { // User canceled operation from CertificateManager - if (e.getCause() instanceof ValidatorException) { - LOG.info("Host SSL certificate is not trusted", e); - throw new GithubOperationCanceledException("Host SSL certificate is not trusted", e); - } - throw e; - } - return method; - } - - @NotNull - private static HttpClient getHttpClient(@Nullable GithubAuthData.BasicAuth basicAuth, boolean useProxy) { - int timeout = GithubSettings.getInstance().getConnectionTimeout(); - final HttpClient client = new HttpClient(); - HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams(); - params.setConnectionTimeout(timeout); //set connection timeout (how long it takes to connect to remote host) - params.setSoTimeout(timeout); //set socket timeout (how long it takes to retrieve data from remote host) - - client.getParams().setContentCharset("UTF-8"); - // Configure proxySettings if it is required - final HttpConfigurable proxySettings = HttpConfigurable.getInstance(); - if (useProxy && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) { - client.getHostConfiguration().setProxy(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT); - if (proxySettings.PROXY_AUTHENTICATION) { - client.getState().setProxyCredentials(AuthScope.ANY, new UsernamePasswordCredentials(proxySettings.PROXY_LOGIN, - proxySettings.getPlainProxyPassword())); - } - } - if (basicAuth != null) { - client.getParams().setCredentialCharset("UTF-8"); - client.getParams().setAuthenticationPreemptive(true); - client.getState().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(basicAuth.getLogin(), basicAuth.getPassword())); - } - return client; - } - - private static void checkStatusCode(@NotNull HttpMethod method, @Nullable String body) throws IOException { - int code = method.getStatusCode(); - switch (code) { - case HttpStatus.SC_OK: - case HttpStatus.SC_CREATED: - case HttpStatus.SC_ACCEPTED: - case HttpStatus.SC_NO_CONTENT: - return; - case HttpStatus.SC_UNAUTHORIZED: - case HttpStatus.SC_PAYMENT_REQUIRED: - case HttpStatus.SC_FORBIDDEN: - String message = getErrorMessage(method); - - Header headerOTP = method.getResponseHeader("X-GitHub-OTP"); - if (headerOTP != null) { - if (headerOTP.getValue().startsWith("required")) { - throw new GithubTwoFactorAuthenticationException(message); - } - } - - if (message.contains("API rate limit exceeded")) { - throw new GithubRateLimitExceededException(message); - } - - throw new GithubAuthenticationException("Request response: " + message); - case HttpStatus.SC_BAD_REQUEST: - case HttpStatus.SC_UNPROCESSABLE_ENTITY: - if (body != null) { - LOG.info(body); - } - throw new GithubStatusCodeException(code + ": " + getErrorMessage(method), code); - default: - throw new GithubStatusCodeException(code + ": " + getErrorMessage(method), code); - } - } - - @NotNull - private static String getErrorMessage(@NotNull HttpMethod method) { - try { - InputStream resp = method.getResponseBodyAsStream(); - if (resp != null) { - GithubErrorMessageRaw error = fromJson(parseResponse(resp), GithubErrorMessageRaw.class); - return method.getStatusText() + " - " + error.getMessage(); - } - } - catch (IOException e) { - LOG.info(e); - } - return method.getStatusText(); - } - - @NotNull - private static JsonElement parseResponse(@NotNull InputStream githubResponse) throws IOException { - Reader reader = new InputStreamReader(githubResponse, "UTF-8"); - try { - return new JsonParser().parse(reader); - } - catch (JsonParseException jse) { - throw new GithubJsonException("Couldn't parse GitHub response", jse); - } - finally { - reader.close(); - } - } - - private static class ResponsePage { - @Nullable private final JsonElement response; - @Nullable private final String nextPage; - - public ResponsePage() { - this(null, null); - } - - public ResponsePage(@Nullable JsonElement response) { - this(response, null); - } - - public ResponsePage(@Nullable JsonElement response, @Nullable String next) { - this.response = response; - this.nextPage = next; - } - - @Nullable - public JsonElement getJsonElement() { - return response; - } - - @Nullable - public String getNextPage() { - return nextPage; - } - } - - /* - * Json API - */ - - static <Raw extends DataConstructor, Result> Result createDataFromRaw(@NotNull Raw rawObject, @NotNull Class<Result> resultClass) - throws GithubJsonException { - try { - return rawObject.create(resultClass); - } - catch (Exception e) { - throw new GithubJsonException("Json parse error", e); - } - } - - public static class PagedRequest<T> { - @Nullable private String myNextPage; - @NotNull private final Collection<Header> myHeaders; - @NotNull private final Class<T> myResult; - @NotNull private final Class<? extends DataConstructor[]> myRawArray; - - @SuppressWarnings("NullableProblems") - public PagedRequest(@NotNull String path, - @NotNull Class<T> result, - @NotNull Class<? extends DataConstructor[]> rawArray, - @NotNull Header... headers) { - myNextPage = path; - myResult = result; - myRawArray = rawArray; - myHeaders = Arrays.asList(headers); - } - - @NotNull - public List<T> next(@NotNull GithubAuthData auth) throws IOException { - if (myNextPage == null) { - throw new NoSuchElementException(); - } - - String page = myNextPage; - myNextPage = null; - - ResponsePage response = request(auth, page, null, myHeaders, HttpVerb.GET); - - if (response.getJsonElement() == null) { - throw new HttpException("Empty response"); - } - - if (!response.getJsonElement().isJsonArray()) { - throw new GithubJsonException("Wrong json type: expected JsonArray", new Exception(response.getJsonElement().toString())); - } - - myNextPage = response.getNextPage(); - - List<T> result = new ArrayList<T>(); - for (DataConstructor raw : fromJson(response.getJsonElement().getAsJsonArray(), myRawArray)) { - result.add(createDataFromRaw(raw, myResult)); - } - return result; - } - - public boolean hasNext() { - return myNextPage != null; - } - - @NotNull - public List<T> getAll(@NotNull GithubAuthData auth) throws IOException { - List<T> result = new ArrayList<T>(); - while (hasNext()) { - result.addAll(next(auth)); - } - return result; - } - } - @NotNull - private static <T> T fromJson(@Nullable JsonElement json, @NotNull Class<T> classT) throws IOException { + public static <T> T fromJson(@Nullable JsonElement json, @NotNull Class<T> classT) throws IOException { if (json == null) { throw new GithubJsonException("Unexpected empty response"); } @@ -425,13 +61,24 @@ public class GithubApiUtil { return res; } - /* - * Github API + @NotNull + public static <Raw extends DataConstructor, Result> Result createDataFromRaw(@NotNull Raw rawObject, @NotNull Class<Result> resultClass) + throws GithubJsonException { + try { + return rawObject.create(resultClass); + } + catch (Exception e) { + throw new GithubJsonException("Json parse error", e); + } + } + + /* + * Operations */ - public static void askForTwoFactorCodeSMS(@NotNull GithubAuthData auth) { + public static void askForTwoFactorCodeSMS(@NotNull GithubConnection connection) { try { - postRequest(auth, "/authorizations", null, ACCEPT_V3_JSON); + connection.postRequest("/authorizations", null, ACCEPT_V3_JSON); } catch (IOException e) { LOG.info(e); @@ -439,47 +86,42 @@ public class GithubApiUtil { } @NotNull - public static Collection<String> getTokenScopes(@NotNull GithubAuthData auth) throws IOException { - HttpMethod method = null; - try { - String uri = GithubUrlUtil.getApiUrl(auth.getHost()) + "/user"; - method = doREST(auth, uri, null, Collections.<Header>emptyList(), HttpVerb.HEAD); - - checkStatusCode(method, null); + public static Collection<String> getTokenScopes(@NotNull GithubConnection connection) throws IOException { + Header[] headers = connection.headRequest("/user", ACCEPT_V3_JSON); - Header header = method.getResponseHeader("X-OAuth-Scopes"); - if (header == null) { - throw new HttpException("No scopes header"); - } - - Collection<String> scopes = new ArrayList<String>(); - for (HeaderElement elem : header.getElements()) { - scopes.add(elem.getName()); + Header scopesHeader = null; + for (Header header : headers) { + if (header.getName().equals("X-OAuth-Scopes")) { + scopesHeader = header; + break; } - return scopes; } - finally { - if (method != null) { - method.releaseConnection(); - } + if (scopesHeader == null) { + throw new GithubConfusingException("No scopes header"); } + + Collection<String> scopes = new ArrayList<String>(); + for (HeaderElement elem : scopesHeader.getElements()) { + scopes.add(elem.getName()); + } + return scopes; } @NotNull - public static String getScopedToken(@NotNull GithubAuthData auth, @NotNull Collection<String> scopes, @NotNull String note) + public static String getScopedToken(@NotNull GithubConnection connection, @NotNull Collection<String> scopes, @NotNull String note) throws IOException { - GithubAuthorization token = findToken(auth, note); + GithubAuthorization token = findToken(connection, note); if (token == null) { - return getNewScopedToken(auth, scopes, note).getToken(); + return getNewScopedToken(connection, scopes, note).getToken(); } if (token.getScopes().containsAll(scopes)) { return token.getToken(); } - return updateTokenScopes(auth, token, scopes).getToken(); + return updateTokenScopes(connection, token, scopes).getToken(); } @NotNull - private static GithubAuthorization updateTokenScopes(@NotNull GithubAuthData auth, + private static GithubAuthorization updateTokenScopes(@NotNull GithubConnection connection, @NotNull GithubAuthorization token, @NotNull Collection<String> scopes) throws IOException { try { @@ -487,7 +129,7 @@ public class GithubApiUtil { GithubAuthorizationUpdateRequest request = new GithubAuthorizationUpdateRequest(new ArrayList<String>(scopes)); - return createDataFromRaw(fromJson(patchRequest(auth, path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class), + return createDataFromRaw(fromJson(connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class), GithubAuthorization.class); } catch (GithubConfusingException e) { @@ -497,7 +139,7 @@ public class GithubApiUtil { } @NotNull - private static GithubAuthorization getNewScopedToken(@NotNull GithubAuthData auth, + private static GithubAuthorization getNewScopedToken(@NotNull GithubConnection connection, @NotNull Collection<String> scopes, @NotNull String note) throws IOException { @@ -506,7 +148,7 @@ public class GithubApiUtil { GithubAuthorizationCreateRequest request = new GithubAuthorizationCreateRequest(new ArrayList<String>(scopes), note, null); - return createDataFromRaw(fromJson(postRequest(auth, path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class), + return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class), GithubAuthorization.class); } catch (GithubConfusingException e) { @@ -516,14 +158,14 @@ public class GithubApiUtil { } @Nullable - private static GithubAuthorization findToken(@NotNull GithubAuthData auth, @NotNull String note) throws IOException { + private static GithubAuthorization findToken(@NotNull GithubConnection connection, @NotNull String note) throws IOException { try { String path = "/authorizations"; PagedRequest<GithubAuthorization> request = new PagedRequest<GithubAuthorization>(path, GithubAuthorization.class, GithubAuthorizationRaw[].class, ACCEPT_V3_JSON); - List<GithubAuthorization> tokens = request.getAll(auth); + List<GithubAuthorization> tokens = request.getAll(connection); for (GithubAuthorization token : tokens) { if (note.equals(token.getNote())) return token; @@ -537,29 +179,32 @@ public class GithubApiUtil { } @NotNull - public static String getMasterToken(@NotNull GithubAuthData auth, @NotNull String note) throws IOException { + public static String getMasterToken(@NotNull GithubConnection connection, @NotNull String note) throws IOException { // "repo" - read/write access to public/private repositories // "gist" - create/delete gists List<String> scopes = Arrays.asList("repo", "gist"); - return getScopedToken(auth, scopes, note); + return getScopedToken(connection, scopes, note); } @NotNull - public static String getReadOnlyToken(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, @NotNull String note) + public static String getReadOnlyToken(@NotNull GithubConnection connection, + @NotNull String user, + @NotNull String repo, + @NotNull String note) throws IOException { - GithubRepo repository = getDetailedRepoInfo(auth, user, repo); + GithubRepo repository = getDetailedRepoInfo(connection, user, repo); // TODO: use read-only token for private repos when it will be available List<String> scopes = repository.isPrivate() ? Collections.singletonList("repo") : Collections.<String>emptyList(); - return getScopedToken(auth, scopes, note); + return getScopedToken(connection, scopes, note); } @NotNull - public static GithubUser getCurrentUser(@NotNull GithubAuthData auth) throws IOException { + public static GithubUser getCurrentUser(@NotNull GithubConnection connection) throws IOException { try { - JsonElement result = getRequest(auth, "/user", ACCEPT_V3_JSON); + JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUser.class); } catch (GithubConfusingException e) { @@ -569,9 +214,9 @@ public class GithubApiUtil { } @NotNull - public static GithubUserDetailed getCurrentUserDetailed(@NotNull GithubAuthData auth) throws IOException { + public static GithubUserDetailed getCurrentUserDetailed(@NotNull GithubConnection connection) throws IOException { try { - JsonElement result = getRequest(auth, "/user", ACCEPT_V3_JSON); + JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUserDetailed.class); } catch (GithubConfusingException e) { @@ -581,13 +226,13 @@ public class GithubApiUtil { } @NotNull - public static List<GithubRepo> getUserRepos(@NotNull GithubAuthData auth) throws IOException { + public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection) throws IOException { try { String path = "/user/repos?" + PER_PAGE; PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get user repositories"); @@ -596,13 +241,13 @@ public class GithubApiUtil { } @NotNull - public static List<GithubRepo> getUserRepos(@NotNull GithubAuthData auth, @NotNull String user) throws IOException { + public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection, @NotNull String user) throws IOException { try { String path = "/users/" + user + "/repos?" + PER_PAGE; PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get user repositories: " + user); @@ -611,21 +256,21 @@ public class GithubApiUtil { } @NotNull - public static List<GithubRepo> getAvailableRepos(@NotNull GithubAuthData auth) throws IOException { + public static List<GithubRepo> getAvailableRepos(@NotNull GithubConnection connection) throws IOException { try { List<GithubRepo> repos = new ArrayList<GithubRepo>(); - repos.addAll(getUserRepos(auth)); + repos.addAll(getUserRepos(connection)); // We already can return something useful from getUserRepos, so let's ignore errors. // One of this may not exist in GitHub enterprise try { - repos.addAll(getMembershipRepos(auth)); + repos.addAll(getMembershipRepos(connection)); } catch (GithubStatusCodeException ignore) { } try { - repos.addAll(getWatchedRepos(auth)); + repos.addAll(getWatchedRepos(connection)); } catch (GithubStatusCodeException ignore) { } @@ -639,36 +284,36 @@ public class GithubApiUtil { } @NotNull - public static List<GithubRepoOrg> getMembershipRepos(@NotNull GithubAuthData auth) throws IOException { + public static List<GithubRepoOrg> getMembershipRepos(@NotNull GithubConnection connection) throws IOException { String orgsPath = "/user/orgs?" + PER_PAGE; PagedRequest<GithubOrg> orgsRequest = new PagedRequest<GithubOrg>(orgsPath, GithubOrg.class, GithubOrgRaw[].class); List<GithubRepoOrg> repos = new ArrayList<GithubRepoOrg>(); - for (GithubOrg org : orgsRequest.getAll(auth)) { + for (GithubOrg org : orgsRequest.getAll(connection)) { String path = "/orgs/" + org.getLogin() + "/repos?type=member&" + PER_PAGE; PagedRequest<GithubRepoOrg> request = new PagedRequest<GithubRepoOrg>(path, GithubRepoOrg.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); - repos.addAll(request.getAll(auth)); + repos.addAll(request.getAll(connection)); } return repos; } @NotNull - public static List<GithubRepo> getWatchedRepos(@NotNull GithubAuthData auth) throws IOException { + public static List<GithubRepo> getWatchedRepos(@NotNull GithubConnection connection) throws IOException { String pathWatched = "/user/subscriptions?" + PER_PAGE; PagedRequest<GithubRepo> requestWatched = new PagedRequest<GithubRepo>(pathWatched, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); - return requestWatched.getAll(auth); + return requestWatched.getAll(connection); } @NotNull - public static GithubRepoDetailed getDetailedRepoInfo(@NotNull GithubAuthData auth, @NotNull String owner, @NotNull String name) + public static GithubRepoDetailed getDetailedRepoInfo(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name) throws IOException { try { final String request = "/repos/" + owner + "/" + name; - JsonElement jsonObject = getRequest(auth, request, ACCEPT_V3_JSON); + JsonElement jsonObject = connection.getRequest(request, ACCEPT_V3_JSON); return createDataFromRaw(fromJson(jsonObject, GithubRepoRaw.class), GithubRepoDetailed.class); } @@ -678,11 +323,11 @@ public class GithubApiUtil { } } - public static void deleteGithubRepository(@NotNull GithubAuthData auth, @NotNull String username, @NotNull String repo) + public static void deleteGithubRepository(@NotNull GithubConnection connection, @NotNull String username, @NotNull String repo) throws IOException { try { String path = "/repos/" + username + "/" + repo; - deleteRequest(auth, path); + connection.deleteRequest(path); } catch (GithubConfusingException e) { e.setDetails("Can't delete repository: " + username + "/" + repo); @@ -690,10 +335,10 @@ public class GithubApiUtil { } } - public static void deleteGist(@NotNull GithubAuthData auth, @NotNull String id) throws IOException { + public static void deleteGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException { try { String path = "/gists/" + id; - deleteRequest(auth, path); + connection.deleteRequest(path); } catch (GithubConfusingException e) { e.setDetails("Can't delete gist: id - " + id); @@ -702,10 +347,10 @@ public class GithubApiUtil { } @NotNull - public static GithubGist getGist(@NotNull GithubAuthData auth, @NotNull String id) throws IOException { + public static GithubGist getGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException { try { String path = "/gists/" + id; - JsonElement result = getRequest(auth, path, ACCEPT_V3_JSON); + JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubGistRaw.class), GithubGist.class); } @@ -716,13 +361,13 @@ public class GithubApiUtil { } @NotNull - public static GithubGist createGist(@NotNull GithubAuthData auth, + public static GithubGist createGist(@NotNull GithubConnection connection, @NotNull List<GithubGist.FileContent> contents, @NotNull String description, boolean isPrivate) throws IOException { try { String request = gson.toJson(new GithubGistRequest(contents, description, !isPrivate)); - return createDataFromRaw(fromJson(postRequest(auth, "/gists", request, ACCEPT_V3_JSON), GithubGistRaw.class), GithubGist.class); + return createDataFromRaw(fromJson(connection.postRequest("/gists", request, ACCEPT_V3_JSON), GithubGistRaw.class), GithubGist.class); } catch (GithubConfusingException e) { e.setDetails("Can't create gist"); @@ -731,7 +376,16 @@ public class GithubApiUtil { } @NotNull - public static GithubPullRequest createPullRequest(@NotNull GithubAuthData auth, + public static List<GithubRepo> getForks(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name) + throws IOException { + String path = "/repos/" + owner + "/" + name + "/forks?" + PER_PAGE; + PagedRequest<GithubRepo> requestWatched = + new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); + return requestWatched.getAll(connection); + } + + @NotNull + public static GithubPullRequest createPullRequest(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String title, @@ -741,7 +395,7 @@ public class GithubApiUtil { try { String request = gson.toJson(new GithubPullRequestRequest(title, description, head, base)); return createDataFromRaw( - fromJson(postRequest(auth, "/repos/" + user + "/" + repo + "/pulls", request, ACCEPT_V3_JSON), GithubPullRequestRaw.class), + fromJson(connection.postRequest("/repos/" + user + "/" + repo + "/pulls", request, ACCEPT_V3_JSON), GithubPullRequestRaw.class), GithubPullRequest.class); } catch (GithubConfusingException e) { @@ -751,14 +405,17 @@ public class GithubApiUtil { } @NotNull - public static GithubRepo createRepo(@NotNull GithubAuthData auth, @NotNull String name, @NotNull String description, boolean isPrivate) + public static GithubRepo createRepo(@NotNull GithubConnection connection, + @NotNull String name, + @NotNull String description, + boolean isPrivate) throws IOException { try { String path = "/user/repos"; GithubRepoRequest request = new GithubRepoRequest(name, description, isPrivate); - return createDataFromRaw(fromJson(postRequest(auth, path, gson.toJson(request), ACCEPT_V3_JSON), GithubRepoRaw.class), + return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubRepoRaw.class), GithubRepo.class); } catch (GithubConfusingException e) { @@ -771,7 +428,7 @@ public class GithubApiUtil { * Open issues only */ @NotNull - public static List<GithubIssue> getIssuesAssigned(@NotNull GithubAuthData auth, + public static List<GithubIssue> getIssuesAssigned(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @Nullable String assigned, @@ -791,7 +448,7 @@ public class GithubApiUtil { List<GithubIssue> result = new ArrayList<GithubIssue>(); while (request.hasNext() && max > result.size()) { - result.addAll(request.next(auth)); + result.addAll(request.next(connection)); } return result; } @@ -805,7 +462,7 @@ public class GithubApiUtil { /* * All issues - open and closed */ - public static List<GithubIssue> getIssuesQueried(@NotNull GithubAuthData auth, + public static List<GithubIssue> getIssuesQueried(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @Nullable String query, @@ -816,7 +473,7 @@ public class GithubApiUtil { String path = "/search/issues?q=" + query; //TODO: Use bodyHtml for issues - GitHub does not support this feature for SearchApi yet - JsonElement result = getRequest(auth, path, ACCEPT_V3_JSON); + JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubIssuesSearchResultRaw.class), GithubIssuesSearchResult.class).getIssues(); } @@ -827,12 +484,12 @@ public class GithubApiUtil { } @NotNull - public static GithubIssue getIssue(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, @NotNull String id) + public static GithubIssue getIssue(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String id) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/issues/" + id; - JsonElement result = getRequest(auth, path, ACCEPT_V3_JSON); + JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class); } @@ -843,7 +500,10 @@ public class GithubApiUtil { } @NotNull - public static List<GithubIssueComment> getIssueComments(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, long id) + public static List<GithubIssueComment> getIssueComments(@NotNull GithubConnection connection, + @NotNull String user, + @NotNull String repo, + long id) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/issues/" + id + "/comments?" + PER_PAGE; @@ -851,7 +511,7 @@ public class GithubApiUtil { PagedRequest<GithubIssueComment> request = new PagedRequest<GithubIssueComment>(path, GithubIssueComment.class, GithubIssueCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get issue comments: " + user + "/" + repo + " - " + id); @@ -859,15 +519,37 @@ public class GithubApiUtil { } } + public static void setIssueState(@NotNull GithubConnection connection, + @NotNull String user, + @NotNull String repo, + @NotNull String id, + boolean open) + throws IOException { + try { + String path = "/repos/" + user + "/" + repo + "/issues/" + id; + + GithubChangeIssueStateRequest request = new GithubChangeIssueStateRequest(open ? "open" : "closed"); + + JsonElement result = connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON); + + createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class); + } + catch (GithubConfusingException e) { + e.setDetails("Can't set issue state: " + user + "/" + repo + " - " + id + "@" + (open ? "open" : "closed")); + throw e; + } + } + + @NotNull - public static GithubCommitDetailed getCommit(@NotNull GithubAuthData auth, + public static GithubCommitDetailed getCommit(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String sha) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/commits/" + sha; - JsonElement result = getRequest(auth, path, ACCEPT_V3_JSON); + JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON); return createDataFromRaw(fromJson(result, GithubCommitRaw.class), GithubCommitDetailed.class); } catch (GithubConfusingException e) { @@ -877,7 +559,7 @@ public class GithubApiUtil { } @NotNull - public static List<GithubCommitComment> getCommitComments(@NotNull GithubAuthData auth, + public static List<GithubCommitComment> getCommitComments(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String sha) throws IOException { @@ -887,7 +569,7 @@ public class GithubApiUtil { PagedRequest<GithubCommitComment> request = new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get commit comments: " + user + "/" + repo + " - " + sha); @@ -896,7 +578,7 @@ public class GithubApiUtil { } @NotNull - public static List<GithubCommitComment> getPullRequestComments(@NotNull GithubAuthData auth, + public static List<GithubCommitComment> getPullRequestComments(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, long id) throws IOException { @@ -906,7 +588,7 @@ public class GithubApiUtil { PagedRequest<GithubCommitComment> request = new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get pull request comments: " + user + "/" + repo + " - " + id); @@ -915,11 +597,11 @@ public class GithubApiUtil { } @NotNull - public static GithubPullRequest getPullRequest(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, int id) + public static GithubPullRequest getPullRequest(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, int id) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/pulls/" + id; - return createDataFromRaw(fromJson(getRequest(auth, path, ACCEPT_V3_JSON_HTML_MARKUP), GithubPullRequestRaw.class), + return createDataFromRaw(fromJson(connection.getRequest(path, ACCEPT_V3_JSON_HTML_MARKUP), GithubPullRequestRaw.class), GithubPullRequest.class); } catch (GithubConfusingException e) { @@ -929,7 +611,7 @@ public class GithubApiUtil { } @NotNull - public static List<GithubPullRequest> getPullRequests(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo) + public static List<GithubPullRequest> getPullRequests(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE; @@ -937,7 +619,7 @@ public class GithubApiUtil { PagedRequest<GithubPullRequest> request = new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get pull requests" + user + "/" + repo); @@ -953,7 +635,10 @@ public class GithubApiUtil { } @NotNull - public static List<GithubCommit> getPullRequestCommits(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, long id) + public static List<GithubCommit> getPullRequestCommits(@NotNull GithubConnection connection, + @NotNull String user, + @NotNull String repo, + long id) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/commits?" + PER_PAGE; @@ -961,7 +646,7 @@ public class GithubApiUtil { PagedRequest<GithubCommit> request = new PagedRequest<GithubCommit>(path, GithubCommit.class, GithubCommitRaw[].class, ACCEPT_V3_JSON); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get pull request commits: " + user + "/" + repo + " - " + id); @@ -970,14 +655,17 @@ public class GithubApiUtil { } @NotNull - public static List<GithubFile> getPullRequestFiles(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo, long id) + public static List<GithubFile> getPullRequestFiles(@NotNull GithubConnection connection, + @NotNull String user, + @NotNull String repo, + long id) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/files?" + PER_PAGE; PagedRequest<GithubFile> request = new PagedRequest<GithubFile>(path, GithubFile.class, GithubFileRaw[].class, ACCEPT_V3_JSON); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get pull request files: " + user + "/" + repo + " - " + id); @@ -986,7 +674,7 @@ public class GithubApiUtil { } @NotNull - public static List<GithubBranch> getRepoBranches(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo) + public static List<GithubBranch> getRepoBranches(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo) throws IOException { try { String path = "/repos/" + user + "/" + repo + "/branches?" + PER_PAGE; @@ -994,7 +682,7 @@ public class GithubApiUtil { PagedRequest<GithubBranch> request = new PagedRequest<GithubBranch>(path, GithubBranch.class, GithubBranchRaw[].class, ACCEPT_V3_JSON); - return request.getAll(auth); + return request.getAll(connection); } catch (GithubConfusingException e) { e.setDetails("Can't get repository branches: " + user + "/" + repo); @@ -1003,7 +691,7 @@ public class GithubApiUtil { } @Nullable - public static GithubRepo findForkByUser(@NotNull GithubAuthData auth, + public static GithubRepo findForkByUser(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String forkUser) throws IOException { @@ -1013,7 +701,7 @@ public class GithubApiUtil { PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON); while (request.hasNext()) { - for (GithubRepo fork : request.next(auth)) { + for (GithubRepo fork : request.next(connection)) { if (StringUtil.equalsIgnoreCase(fork.getUserName(), forkUser)) { return fork; } @@ -1027,4 +715,4 @@ public class GithubApiUtil { throw e; } } -}
\ No newline at end of file +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubChangeIssueStateRequest.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubChangeIssueStateRequest.java new file mode 100644 index 000000000000..9e21f64046e5 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubChangeIssueStateRequest.java @@ -0,0 +1,12 @@ +package org.jetbrains.plugins.github.api; + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) +public class GithubChangeIssueStateRequest { + @NotNull private final String state; + + public GithubChangeIssueStateRequest(@NotNull String state) { + this.state = state; + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java new file mode 100644 index 000000000000..6358b79e38ee --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java @@ -0,0 +1,491 @@ +package org.jetbrains.plugins.github.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.net.HttpConfigurable; +import com.intellij.util.net.ssl.CertificateManager; +import org.apache.http.*; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.*; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.ConnectionConfig; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; +import org.jetbrains.plugins.github.exceptions.*; +import org.jetbrains.plugins.github.util.GithubAuthData; +import org.jetbrains.plugins.github.util.GithubSettings; +import org.jetbrains.plugins.github.util.GithubUrlUtil; +import org.jetbrains.plugins.github.util.GithubUtil; +import sun.security.validator.ValidatorException; + +import javax.net.ssl.SSLHandshakeException; +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.*; +import java.util.List; + +import static org.jetbrains.plugins.github.api.GithubApiUtil.createDataFromRaw; +import static org.jetbrains.plugins.github.api.GithubApiUtil.fromJson; + +public class GithubConnection { + private static final Logger LOG = GithubUtil.LOG; + + private static final HttpRequestInterceptor PREEMPTIVE_BASIC_AUTH = new PreemptiveBasicAuthInterceptor(); + + @NotNull private final String myHost; + @NotNull private final CloseableHttpClient myClient; + private final boolean myReusable; + + private volatile HttpUriRequest myRequest; + private volatile boolean myAborted; + + @TestOnly + public GithubConnection(@NotNull GithubAuthData auth) { + this(auth, false); + } + + public GithubConnection(@NotNull GithubAuthData auth, boolean reusable) { + myHost = auth.getHost(); + myClient = createClient(auth); + myReusable = reusable; + } + + private enum HttpVerb { + GET, POST, DELETE, HEAD, PATCH + } + + @Nullable + public JsonElement getRequest(@NotNull String path, + @NotNull Header... headers) throws IOException { + return request(path, null, Arrays.asList(headers), HttpVerb.GET).getJsonElement(); + } + + @Nullable + public JsonElement postRequest(@NotNull String path, + @Nullable String requestBody, + @NotNull Header... headers) throws IOException { + return request(path, requestBody, Arrays.asList(headers), HttpVerb.POST).getJsonElement(); + } + + @Nullable + public JsonElement patchRequest(@NotNull String path, + @Nullable String requestBody, + @NotNull Header... headers) throws IOException { + return request(path, requestBody, Arrays.asList(headers), HttpVerb.PATCH).getJsonElement(); + } + + @Nullable + public JsonElement deleteRequest(@NotNull String path, + @NotNull Header... headers) throws IOException { + return request(path, null, Arrays.asList(headers), HttpVerb.DELETE).getJsonElement(); + } + + @NotNull + public Header[] headRequest(@NotNull String path, + @NotNull Header... headers) throws IOException { + return request(path, null, Arrays.asList(headers), HttpVerb.HEAD).getHeaders(); + } + + public void abort() { + if (myAborted) return; + myAborted = true; + + HttpUriRequest request = myRequest; + if (request != null) request.abort(); + } + + public void close() throws IOException { + myClient.close(); + } + + @NotNull + private static CloseableHttpClient createClient(@NotNull GithubAuthData auth) { + HttpClientBuilder builder = HttpClients.custom(); + + return builder + .setDefaultRequestConfig(createRequestConfig(auth)) + .setDefaultConnectionConfig(createConnectionConfig(auth)) + .setDefaultCredentialsProvider(createCredentialsProvider(auth)) + .setDefaultHeaders(createHeaders(auth)) + .addInterceptorFirst(PREEMPTIVE_BASIC_AUTH) + .setSslcontext(CertificateManager.getInstance().getSslContext()) + .setHostnameVerifier((X509HostnameVerifier)CertificateManager.HOSTNAME_VERIFIER) + .build(); + } + + @NotNull + private static RequestConfig createRequestConfig(@NotNull GithubAuthData auth) { + RequestConfig.Builder builder = RequestConfig.custom(); + + int timeout = GithubSettings.getInstance().getConnectionTimeout(); + builder + .setConnectTimeout(timeout) + .setSocketTimeout(timeout); + + final HttpConfigurable proxySettings = HttpConfigurable.getInstance(); + if (auth.isUseProxy() && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) { + builder + .setProxy(new HttpHost(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT)); + } + + return builder.build(); + } + + @NotNull + private static ConnectionConfig createConnectionConfig(@NotNull GithubAuthData auth) { + return ConnectionConfig.custom() + .setCharset(Consts.UTF_8) + .build(); + } + + + @NotNull + private static CredentialsProvider createCredentialsProvider(@NotNull GithubAuthData auth) { + CredentialsProvider provider = new BasicCredentialsProvider(); + // Basic authentication + GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth(); + if (basicAuth != null) { + provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(basicAuth.getLogin(), basicAuth.getPassword())); + } + + final HttpConfigurable proxySettings = HttpConfigurable.getInstance(); + //proxySettings.USE_HTTP_PROXY + if (auth.isUseProxy() && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) { + if (proxySettings.PROXY_AUTHENTICATION) { + provider.setCredentials(new AuthScope(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT), + new UsernamePasswordCredentials(proxySettings.PROXY_LOGIN, proxySettings.getPlainProxyPassword())); + } + } + return provider; + } + + @NotNull + private static Collection<? extends Header> createHeaders(@NotNull GithubAuthData auth) { + List<Header> headers = new ArrayList<Header>(); + GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth(); + if (tokenAuth != null) { + headers.add(new BasicHeader("Authorization", "token " + tokenAuth.getToken())); + } + GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth(); + if (basicAuth != null && basicAuth.getCode() != null) { + headers.add(new BasicHeader("X-GitHub-OTP", basicAuth.getCode())); + } + return headers; + } + + @NotNull + private ResponsePage request(@NotNull String path, + @Nullable String requestBody, + @NotNull Collection<Header> headers, + @NotNull HttpVerb verb) throws IOException { + if (myAborted) throw new GithubOperationCanceledException(); + + if (EventQueue.isDispatchThread() && !ApplicationManager.getApplication().isUnitTestMode()) { + LOG.warn("Network operation in EDT"); // TODO: fix + } + + CloseableHttpResponse response = null; + try { + String uri = GithubUrlUtil.getApiUrl(myHost) + path; + response = doREST(uri, requestBody, headers, verb); + + if (myAborted) throw new GithubOperationCanceledException(); + + checkStatusCode(response, requestBody); + + HttpEntity entity = response.getEntity(); + if (entity == null) { + return createResponse(response); + } + + JsonElement ret = parseResponse(entity.getContent()); + if (ret.isJsonNull()) { + return createResponse(response); + } + + String newPath = null; + Header pageHeader = response.getFirstHeader("Link"); + if (pageHeader != null) { + for (HeaderElement element : pageHeader.getElements()) { + NameValuePair rel = element.getParameterByName("rel"); + if (rel != null && "next".equals(rel.getValue())) { + String urlString = element.toString(); + int begin = urlString.indexOf('<'); + int end = urlString.lastIndexOf('>'); + if (begin == -1 || end == -1) { + LOG.error("Invalid 'Link' header", "{" + pageHeader.toString() + "}"); + break; + } + + String url = urlString.substring(begin + 1, end); + String newUrl = GithubUrlUtil.removeProtocolPrefix(url); + int index = newUrl.indexOf('/'); + newPath = newUrl.substring(index); + break; + } + } + } + + return createResponse(ret, newPath, response); + } + catch (SSLHandshakeException e) { // User canceled operation from CertificateManager + if (e.getCause() instanceof ValidatorException) { + LOG.info("Host SSL certificate is not trusted", e); + throw new GithubOperationCanceledException("Host SSL certificate is not trusted", e); + } + throw e; + } + catch (IOException e) { + if (myAborted) throw new GithubOperationCanceledException("Operation canceled", e); + throw e; + } + finally { + myRequest = null; + if (response != null) { + response.close(); + } + if (!myReusable) { + myClient.close(); + } + } + } + + @NotNull + private CloseableHttpResponse doREST(@NotNull final String uri, + @Nullable final String requestBody, + @NotNull final Collection<Header> headers, + @NotNull final HttpVerb verb) throws IOException { + HttpRequestBase request; + switch (verb) { + case POST: + request = new HttpPost(uri); + if (requestBody != null) { + ((HttpPost)request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); + } + break; + case PATCH: + request = new HttpPatch(uri); + if (requestBody != null) { + ((HttpPatch)request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); + } + break; + case GET: + request = new HttpGet(uri); + break; + case DELETE: + request = new HttpDelete(uri); + break; + case HEAD: + request = new HttpHead(uri); + break; + default: + throw new IllegalStateException("Unknown HttpVerb: " + verb.toString()); + } + + for (Header header : headers) { + request.addHeader(header); + } + + myRequest = request; + return myClient.execute(request); + } + + private static void checkStatusCode(@NotNull CloseableHttpResponse response, @Nullable String body) throws IOException { + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case HttpStatus.SC_OK: + case HttpStatus.SC_CREATED: + case HttpStatus.SC_ACCEPTED: + case HttpStatus.SC_NO_CONTENT: + return; + case HttpStatus.SC_UNAUTHORIZED: + case HttpStatus.SC_PAYMENT_REQUIRED: + case HttpStatus.SC_FORBIDDEN: + String message = getErrorMessage(response); + + Header headerOTP = response.getFirstHeader("X-GitHub-OTP"); + if (headerOTP != null) { + for (HeaderElement element : headerOTP.getElements()) { + if ("required".equals(element.getName())) { + throw new GithubTwoFactorAuthenticationException(message); + } + } + } + + if (message.contains("API rate limit exceeded")) { + throw new GithubRateLimitExceededException(message); + } + + throw new GithubAuthenticationException("Request response: " + message); + case HttpStatus.SC_BAD_REQUEST: + case HttpStatus.SC_UNPROCESSABLE_ENTITY: + if (body != null) { + LOG.info(body); + } + throw new GithubStatusCodeException(code + ": " + getErrorMessage(response), code); + default: + throw new GithubStatusCodeException(code + ": " + getErrorMessage(response), code); + } + } + + @NotNull + private static String getErrorMessage(@NotNull CloseableHttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + if (entity != null) { + GithubErrorMessageRaw error = fromJson(parseResponse(entity.getContent()), GithubErrorMessageRaw.class); + return response.getStatusLine().getReasonPhrase() + " - " + error.getMessage(); + } + } + catch (IOException e) { + LOG.info(e); + } + return response.getStatusLine().getReasonPhrase(); + } + + @NotNull + private static JsonElement parseResponse(@NotNull InputStream githubResponse) throws IOException { + Reader reader = new InputStreamReader(githubResponse, "UTF-8"); + try { + return new JsonParser().parse(reader); + } + catch (JsonParseException jse) { + throw new GithubJsonException("Couldn't parse GitHub response", jse); + } + finally { + reader.close(); + } + } + + public static class PagedRequest<T> { + @Nullable private String myNextPage; + @NotNull private final Collection<Header> myHeaders; + @NotNull private final Class<T> myResult; + @NotNull private final Class<? extends DataConstructor[]> myRawArray; + + @SuppressWarnings("NullableProblems") + public PagedRequest(@NotNull String path, + @NotNull Class<T> result, + @NotNull Class<? extends DataConstructor[]> rawArray, + @NotNull Header... headers) { + myNextPage = path; + myResult = result; + myRawArray = rawArray; + myHeaders = Arrays.asList(headers); + } + + @NotNull + public List<T> next(@NotNull GithubConnection connection) throws IOException { + if (myNextPage == null) { + throw new NoSuchElementException(); + } + + String page = myNextPage; + myNextPage = null; + + ResponsePage response = connection.request(page, null, myHeaders, HttpVerb.GET); + + if (response.getJsonElement() == null) { + throw new GithubConfusingException("Empty response"); + } + + if (!response.getJsonElement().isJsonArray()) { + throw new GithubJsonException("Wrong json type: expected JsonArray", new Exception(response.getJsonElement().toString())); + } + + myNextPage = response.getNextPage(); + + List<T> result = new ArrayList<T>(); + for (DataConstructor raw : fromJson(response.getJsonElement().getAsJsonArray(), myRawArray)) { + result.add(createDataFromRaw(raw, myResult)); + } + return result; + } + + public boolean hasNext() { + return myNextPage != null; + } + + @NotNull + public List<T> getAll(@NotNull GithubConnection connection) throws IOException { + List<T> result = new ArrayList<T>(); + while (hasNext()) { + result.addAll(next(connection)); + } + return result; + } + } + + private ResponsePage createResponse(@NotNull CloseableHttpResponse response) throws GithubOperationCanceledException { + if (myAborted) throw new GithubOperationCanceledException(); + + return new ResponsePage(null, null, response.getAllHeaders()); + } + + private ResponsePage createResponse(@NotNull JsonElement ret, @Nullable String path, @NotNull CloseableHttpResponse response) + throws GithubOperationCanceledException { + if (myAborted) throw new GithubOperationCanceledException(); + + return new ResponsePage(ret, path, response.getAllHeaders()); + } + + private static class ResponsePage { + @Nullable private final JsonElement myResponse; + @Nullable private final String myNextPage; + @NotNull private final Header[] myHeaders; + + public ResponsePage(@Nullable JsonElement response, @Nullable String next, @NotNull Header[] headers) { + myResponse = response; + myNextPage = next; + myHeaders = headers; + } + + @Nullable + public JsonElement getJsonElement() { + return myResponse; + } + + @Nullable + public String getNextPage() { + return myNextPage; + } + + @NotNull + public Header[] getHeaders() { + return myHeaders; + } + } + + private static class PreemptiveBasicAuthInterceptor implements HttpRequestInterceptor { + @Override + public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { + CredentialsProvider provider = (CredentialsProvider)context.getAttribute(HttpClientContext.CREDS_PROVIDER); + Credentials credentials = provider.getCredentials(AuthScope.ANY); + if (credentials != null) { + request.addHeader(new BasicScheme(Consts.UTF_8).authenticate(credentials, request, context)); + } + } + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubFullPath.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubFullPath.java index ee0134e4fbc3..1575caee25db 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubFullPath.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubFullPath.java @@ -47,14 +47,19 @@ public class GithubFullPath { } @Override + public String toString() { + return "'" + getFullName() + "'"; + } + + @Override public boolean equals(@Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; GithubFullPath that = (GithubFullPath)o; - if (!StringUtil.endsWithIgnoreCase(myRepositoryName, that.myRepositoryName)) return false; - if (!StringUtil.endsWithIgnoreCase(myUserName, that.myUserName)) return false; + if (!StringUtil.equalsIgnoreCase(myRepositoryName, that.myRepositoryName)) return false; + if (!StringUtil.equalsIgnoreCase(myUserName, that.myUserName)) return false; return true; } diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUser.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUser.java index 29ef1acdfd78..bf81b8d62502 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUser.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUser.java @@ -26,12 +26,12 @@ public class GithubUser { @NotNull private final String myHtmlUrl; - @Nullable private final String myGravatarId; + @Nullable private final String myAvatarUrl; - public GithubUser(@NotNull String login, @NotNull String htmlUrl, @Nullable String gravatarId) { + public GithubUser(@NotNull String login, @NotNull String htmlUrl, @Nullable String avatarUrl) { myLogin = login; myHtmlUrl = htmlUrl; - myGravatarId = gravatarId; + myAvatarUrl = avatarUrl; } @NotNull @@ -45,7 +45,7 @@ public class GithubUser { } @Nullable - public String getGravatarId() { - return myGravatarId; + public String getAvatarUrl() { + return myAvatarUrl; } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserDetailed.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserDetailed.java index 2c82c078e339..3940b80f360a 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserDetailed.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserDetailed.java @@ -55,13 +55,13 @@ public class GithubUserDetailed extends GithubUser { public GithubUserDetailed(@NotNull String login, @NotNull String htmlUrl, - @Nullable String gravatarId, + @Nullable String avatarUrl, @Nullable String name, @Nullable String email, @Nullable Integer ownedPrivateRepos, @Nullable String type, @Nullable UserPlan plan) { - super(login, htmlUrl, gravatarId); + super(login, htmlUrl, avatarUrl); myName = name; myEmail = email; myOwnedPrivateRepos = ownedPrivateRepos; diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserRaw.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserRaw.java index 04d642f8eb2e..0bc673299847 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserRaw.java +++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubUserRaw.java @@ -74,14 +74,14 @@ class GithubUserRaw implements DataConstructor { @SuppressWarnings("ConstantConditions") @NotNull public GithubUser createUser() { - return new GithubUser(login, htmlUrl, gravatarId); + return new GithubUser(login, htmlUrl, avatarUrl); } @SuppressWarnings("ConstantConditions") @NotNull public GithubUserDetailed createUserDetailed() { GithubUserDetailed.UserPlan plan = this.plan == null ? null : this.plan.create(); - return new GithubUserDetailed(login, htmlUrl, gravatarId, name, email, ownedPrivateRepos, type, plan); + return new GithubUserDetailed(login, htmlUrl, avatarUrl, name, email, ownedPrivateRepos, type, plan); } @SuppressWarnings("unchecked") diff --git a/plugins/github/src/org/jetbrains/plugins/github/extensions/GithubCheckoutProvider.java b/plugins/github/src/org/jetbrains/plugins/github/extensions/GithubCheckoutProvider.java index 6b3f73f5f8be..911314c4f72d 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/extensions/GithubCheckoutProvider.java +++ b/plugins/github/src/org/jetbrains/plugins/github/extensions/GithubCheckoutProvider.java @@ -29,6 +29,7 @@ import git4idea.commands.Git; 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.GithubRepo; import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; import org.jetbrains.plugins.github.util.GithubAuthData; @@ -62,11 +63,11 @@ public class GithubCheckoutProvider implements CheckoutProvider { @Override public List<GithubRepo> convert(ProgressIndicator indicator) throws IOException { return GithubUtil.runTask(project, GithubAuthDataHolder.createFromSettings(), indicator, - new ThrowableConvertor<GithubAuthData, List<GithubRepo>, IOException>() { + new ThrowableConvertor<GithubConnection, List<GithubRepo>, IOException>() { @NotNull @Override - public List<GithubRepo> convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.getAvailableRepos(auth); + public List<GithubRepo> convert(@NotNull GithubConnection connection) throws IOException { + return GithubApiUtil.getAvailableRepos(connection); } } ); diff --git a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubComment.java b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubComment.java index d65a95044a9c..e3115f71975c 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubComment.java +++ b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubComment.java @@ -26,16 +26,16 @@ import java.util.Date; * @author Dennis.Ushakov */ public class GithubComment extends SimpleComment { - @Nullable private final String myGravatarId; + @Nullable private final String myAvatarUrl; @NotNull private final String myUserHtmlUrl; public GithubComment(@Nullable Date date, @Nullable String author, @NotNull String text, - @Nullable String gravatarId, + @Nullable String avatarUrl, @NotNull String userHtmlUrl) { super(date, author, text); - myGravatarId = gravatarId; + myAvatarUrl = avatarUrl; myUserHtmlUrl = userHtmlUrl; } @@ -43,9 +43,9 @@ public class GithubComment extends SimpleComment { builder.append("<hr>"); builder.append("<table>"); builder.append("<tr><td>"); - if (myGravatarId != null) { - builder.append("<img src=\"").append("http://www.gravatar.com/avatar/").append(myGravatarId).append("?s=40\"/><br>"); - } + if (myAvatarUrl != null) { + builder.append("<img src=\"").append(myAvatarUrl).append("\" height=\"40\" width=\"40\"/><br>"); + } builder.append("</td><td>"); if (getAuthor() != null) { builder.append("<b>Author:</b> <a href=\"").append(myUserHtmlUrl).append("\">").append(getAuthor()).append("</a><br>"); diff --git a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepository.java b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepository.java index 7ad3238f8a12..a8445b7f76ac 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepository.java +++ b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepository.java @@ -5,10 +5,7 @@ import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.PasswordUtil; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.tasks.Comment; -import com.intellij.tasks.Task; -import com.intellij.tasks.TaskRepository; -import com.intellij.tasks.TaskType; +import com.intellij.tasks.*; import com.intellij.tasks.impl.BaseRepository; import com.intellij.tasks.impl.BaseRepositoryImpl; import com.intellij.util.Function; @@ -19,12 +16,10 @@ import icons.TasksIcons; 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.GithubIssue; import org.jetbrains.plugins.github.api.GithubIssueComment; -import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException; -import org.jetbrains.plugins.github.exceptions.GithubJsonException; -import org.jetbrains.plugins.github.exceptions.GithubRateLimitExceededException; -import org.jetbrains.plugins.github.exceptions.GithubStatusCodeException; +import org.jetbrains.plugins.github.exceptions.*; import org.jetbrains.plugins.github.util.GithubAuthData; import org.jetbrains.plugins.github.util.GithubUtil; @@ -67,14 +62,20 @@ public class GithubRepository extends BaseRepositoryImpl { @Override public CancellableConnection createCancellableConnection() { return new CancellableConnection() { + private final GithubConnection myConnection = new GithubConnection(getAuthData(), false); + @Override protected void doTest() throws Exception { - getIssues("", 10, false); + try { + GithubApiUtil.getIssuesQueried(myConnection, getRepoAuthor(), getRepoName(), "", false); + } + catch (GithubOperationCanceledException ignore) { + } } @Override public void cancel() { - // TODO + myConnection.abort(); } }; } @@ -122,23 +123,30 @@ public class GithubRepository extends BaseRepositoryImpl { @NotNull private Task[] getIssues(@Nullable String query, int max, boolean withClosed) throws Exception { - List<GithubIssue> issues; - if (StringUtil.isEmptyOrSpaces(query)) { - if (StringUtil.isEmptyOrSpaces(myUser)) { - myUser = GithubApiUtil.getCurrentUser(getAuthData()).getLogin(); + GithubConnection connection = getConnection(); + + try { + List<GithubIssue> issues; + if (StringUtil.isEmptyOrSpaces(query)) { + if (StringUtil.isEmptyOrSpaces(myUser)) { + myUser = GithubApiUtil.getCurrentUser(connection).getLogin(); + } + issues = GithubApiUtil.getIssuesAssigned(connection, getRepoAuthor(), getRepoName(), myUser, max, withClosed); + } + else { + issues = GithubApiUtil.getIssuesQueried(connection, getRepoAuthor(), getRepoName(), query, withClosed); } - issues = GithubApiUtil.getIssuesAssigned(getAuthData(), getRepoAuthor(), getRepoName(), myUser, max, withClosed); + + return ContainerUtil.map2Array(issues, Task.class, new Function<GithubIssue, Task>() { + @Override + public Task fun(GithubIssue issue) { + return createTask(issue); + } + }); } - else { - issues = GithubApiUtil.getIssuesQueried(getAuthData(), getRepoAuthor(), getRepoName(), query, withClosed); + finally { + connection.close(); } - - return ContainerUtil.map2Array(issues, Task.class, new Function<GithubIssue, Task>() { - @Override - public Task fun(GithubIssue issue) { - return createTask(issue); - } - }); } @NotNull @@ -224,16 +232,22 @@ public class GithubRepository extends BaseRepositoryImpl { } private Comment[] fetchComments(final long id) throws Exception { - List<GithubIssueComment> result = GithubApiUtil.getIssueComments(getAuthData(), getRepoAuthor(), getRepoName(), id); - - return ContainerUtil.map2Array(result, Comment.class, new Function<GithubIssueComment, Comment>() { - @Override - public Comment fun(GithubIssueComment comment) { - return new GithubComment(comment.getCreatedAt(), comment.getUser().getLogin(), comment.getBodyHtml(), - comment.getUser().getGravatarId(), - comment.getUser().getHtmlUrl()); - } - }); + GithubConnection connection = getConnection(); + try { + List<GithubIssueComment> result = GithubApiUtil.getIssueComments(connection, getRepoAuthor(), getRepoName(), id); + + return ContainerUtil.map2Array(result, Comment.class, new Function<GithubIssueComment, Comment>() { + @Override + public Comment fun(GithubIssueComment comment) { + return new GithubComment(comment.getCreatedAt(), comment.getUser().getLogin(), comment.getBodyHtml(), + comment.getUser().getAvatarUrl(), + comment.getUser().getHtmlUrl()); + } + }); + } + finally { + connection.close(); + } } @Nullable @@ -245,7 +259,35 @@ public class GithubRepository extends BaseRepositoryImpl { @Nullable @Override public Task findTask(@NotNull String id) throws Exception { - return createTask(GithubApiUtil.getIssue(getAuthData(), getRepoAuthor(), getRepoName(), id)); + GithubConnection connection = getConnection(); + try { + return createTask(GithubApiUtil.getIssue(connection, getRepoAuthor(), getRepoName(), id)); + } + finally { + connection.close(); + } + } + + @Override + public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { + GithubConnection connection = getConnection(); + try { + boolean isOpen; + switch (state) { + case OPEN: + isOpen = true; + break; + case RESOLVED: + isOpen = false; + break; + default: + throw new IllegalStateException("Unknown state: " + state); + } + GithubApiUtil.setIssueState(connection, getRepoAuthor(), getRepoName(), task.getNumber(), isOpen); + } + finally { + connection.close(); + } } @NotNull @@ -311,6 +353,10 @@ public class GithubRepository extends BaseRepositoryImpl { return GithubAuthData.createTokenAuth(getUrl(), getToken(), isUseProxy()); } + private GithubConnection getConnection() { + return new GithubConnection(getAuthData(), true); + } + @Override public boolean equals(Object o) { if (!super.equals(o)) return false; @@ -326,6 +372,6 @@ public class GithubRepository extends BaseRepositoryImpl { @Override protected int getFeatures() { - return super.getFeatures() | BASIC_HTTP_AUTHORIZATION; + return super.getFeatures() | STATE_UPDATING; } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java index 5333d82e3baf..ba863621c969 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java +++ b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryEditor.java @@ -14,8 +14,7 @@ import com.intellij.util.ui.GridBag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.api.GithubApiUtil; -import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; -import org.jetbrains.plugins.github.util.GithubAuthData; +import org.jetbrains.plugins.github.api.GithubConnection; import org.jetbrains.plugins.github.util.GithubAuthDataHolder; import org.jetbrains.plugins.github.util.GithubNotifications; import org.jetbrains.plugins.github.util.GithubUtil; @@ -126,12 +125,12 @@ public class GithubRepositoryEditor extends BaseRepositoryEditor<GithubRepositor public String convert(ProgressIndicator indicator) throws IOException { return GithubUtil .runTaskWithBasicAuthForHost(myProject, GithubAuthDataHolder.createFromSettings(), indicator, getHost(), - new ThrowableConvertor<GithubAuthData, String, IOException>() { + new ThrowableConvertor<GithubConnection, String, IOException>() { @NotNull @Override - public String convert(@NotNull GithubAuthData auth) throws IOException { + public String convert(@NotNull GithubConnection connection) throws IOException { return GithubApiUtil - .getReadOnlyToken(auth, getRepoAuthor(), getRepoName(), "IntelliJ tasks plugin"); + .getReadOnlyToken(connection, getRepoAuthor(), getRepoName(), "IntelliJ tasks plugin"); } } ); diff --git a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryType.java b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryType.java index a677fad0d781..eefab9065add 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryType.java +++ b/plugins/github/src/org/jetbrains/plugins/github/tasks/GithubRepositoryType.java @@ -2,6 +2,7 @@ package org.jetbrains.plugins.github.tasks; import com.intellij.openapi.project.Project; import com.intellij.tasks.TaskRepository; +import com.intellij.tasks.TaskState; import com.intellij.tasks.config.TaskRepositoryEditor; import com.intellij.tasks.impl.BaseRepositoryType; import com.intellij.util.Consumer; @@ -9,6 +10,7 @@ import icons.TasksIcons; import org.jetbrains.annotations.NotNull; import javax.swing.*; +import java.util.EnumSet; /** * @author Dennis.Ushakov @@ -45,4 +47,9 @@ public class GithubRepositoryType extends BaseRepositoryType<GithubRepository> { Consumer<GithubRepository> changeListener) { return new GithubRepositoryEditor(project, repository, changeListener); } + + public EnumSet<TaskState> getPossibleTaskStates() { + return EnumSet.of(TaskState.OPEN, TaskState.RESOLVED); + } + } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestDialog.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestDialog.java index f062742d191c..482ef9492fdc 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestDialog.java +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestDialog.java @@ -15,142 +15,159 @@ */ package org.jetbrains.plugins.github.ui; +import com.intellij.CommonBundle; +import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.Consumer; -import com.intellij.util.ui.UIUtil; +import com.intellij.util.ThreeState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import org.jetbrains.plugins.github.GithubCreatePullRequestWorker; import org.jetbrains.plugins.github.api.GithubFullPath; +import org.jetbrains.plugins.github.util.GithubNotifications; import org.jetbrains.plugins.github.util.GithubProjectSettings; +import org.jetbrains.plugins.github.util.GithubSettings; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; -import java.util.Collection; import java.util.Collections; +import static org.jetbrains.plugins.github.GithubCreatePullRequestWorker.BranchInfo; +import static org.jetbrains.plugins.github.GithubCreatePullRequestWorker.ForkInfo; + /** * @author Aleksey Pivovarov */ public class GithubCreatePullRequestDialog extends DialogWrapper { - @NotNull private final GithubCreatePullRequestPanel myGithubCreatePullRequestPanel; + @NotNull private final GithubCreatePullRequestPanel myPanel; @NotNull private final GithubCreatePullRequestWorker myWorker; @NotNull private final GithubProjectSettings myProjectSettings; + @NotNull private static final CreateRemoteDoNotAskOption ourDoNotAskOption = new CreateRemoteDoNotAskOption(); - public GithubCreatePullRequestDialog(@NotNull GithubCreatePullRequestWorker worker) { - super(worker.getProject(), true); + public GithubCreatePullRequestDialog(@NotNull final Project project, @NotNull GithubCreatePullRequestWorker worker) { + super(project, true); myWorker = worker; - myProjectSettings = GithubProjectSettings.getInstance(myWorker.getProject()); - - myGithubCreatePullRequestPanel = new GithubCreatePullRequestPanel(); + myProjectSettings = GithubProjectSettings.getInstance(project); - myGithubCreatePullRequestPanel.getShowDiffButton().addActionListener(new ActionListener() { + myPanel = new GithubCreatePullRequestPanel(); + myPanel.getShowDiffButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - myWorker.showDiffDialog(myGithubCreatePullRequestPanel.getBranch()); + myWorker.showDiffDialog(myPanel.getSelectedBranch()); } }); - myGithubCreatePullRequestPanel.getSelectForkButton().addActionListener(new ActionListener() { + myPanel.getSelectForkButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - showTargetDialog(); + ForkInfo fork = myWorker.showTargetDialog(); + if (fork != null) { + myPanel.setForks(myWorker.getForks()); + myPanel.setSelectedFork(fork.getPath()); + } } }); - myGithubCreatePullRequestPanel.getBranchComboBox().addItemListener(new ItemListener() { + + myPanel.getForkComboBox().addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.DESELECTED) { + myPanel.setBranches(Collections.<BranchInfo>emptyList()); + } if (e.getStateChange() == ItemEvent.SELECTED) { - if (myWorker.canShowDiff()) { - myGithubCreatePullRequestPanel.setBusy(true); - myWorker.getDiffDescriptionInPooledThread(getTargetBranch(), new Consumer<GithubCreatePullRequestWorker.DiffDescription>() { - @Override - public void consume(final GithubCreatePullRequestWorker.DiffDescription info) { - UIUtil.invokeLaterIfNeeded(new Runnable() { - @Override - public void run() { - if (info == null) { - myGithubCreatePullRequestPanel.setBusy(false); - return; - } - if (getTargetBranch().equals(info.getBranch())) { - myGithubCreatePullRequestPanel.setBusy(false); - if (myGithubCreatePullRequestPanel.isTitleDescriptionEmptyOrNotModified()) { - myGithubCreatePullRequestPanel.setTitle(info.getTitle()); - myGithubCreatePullRequestPanel.setDescription(info.getDescription()); - } - } - } - }); - } - }); + final ForkInfo fork = (ForkInfo)e.getItem(); + if (fork == null) return; + + myPanel.setBranches(fork.getBranches()); + myPanel.setSelectedBranch(fork.getDefaultBranch()); + + if (fork.getRemoteName() == null && !fork.isProposedToCreateRemote()) { + fork.setProposedToCreateRemote(true); + boolean createRemote = false; + + switch (GithubSettings.getInstance().getCreatePullRequestCreateRemote()) { + case YES: + createRemote = true; + break; + case NO: + createRemote = false; + break; + case UNSURE: + createRemote = GithubNotifications.showYesNoDialog(project, + "Can't Find Remote", + "Configure remote for '" + fork.getPath().getUser() + "'?", + ourDoNotAskOption) == Messages.YES; + break; + } + + if (createRemote) { + myWorker.configureRemote(fork); + } + } + + if (fork.getRemoteName() == null) { + myPanel.setDiffEnabled(false); + } + else { + myPanel.setDiffEnabled(true); + myWorker.launchFetchRemote(fork); } } } }); - setTitle("Create Pull Request - " + myWorker.getCurrentBranch()); - init(); - } - - @Override - public void show() { - GithubFullPath defaultForkPath = myProjectSettings.getCreatePullRequestDefaultRepo(); - if (defaultForkPath != null) { - setTarget(defaultForkPath); - } - else { - if (!showTargetDialog(true)) { - close(CANCEL_EXIT_CODE); - return; - } - } - super.show(); - } - - private boolean showTargetDialog() { - return showTargetDialog(false); - } + myPanel.getBranchComboBox().addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + BranchInfo branch = (BranchInfo)e.getItem(); + if (branch == null) return; - private boolean showTargetDialog(boolean firstTime) { - GithubFullPath forkPath = myWorker.showTargetDialog(firstTime); - if (forkPath == null) { - return false; - } - return setTarget(forkPath); - } + if (branch.getForkInfo().getRemoteName() != null) { + if (branch.getDiffInfoTask() != null && branch.getDiffInfoTask().isDone() && branch.getDiffInfoTask().safeGet() == null) { + myPanel.setDiffEnabled(false); + } + else { + myPanel.setDiffEnabled(true); + } + } - private boolean setTarget(@NotNull GithubFullPath forkPath) { - GithubCreatePullRequestWorker.GithubTargetInfo forkInfo = myWorker.setTarget(forkPath); - if (forkInfo == null) { - return false; - } - myProjectSettings.setCreatePullRequestDefaultRepo(forkPath); - myGithubCreatePullRequestPanel.setDiffEnabled(myWorker.canShowDiff()); - updateBranches(forkInfo.getBranches(), forkPath); - return true; - } + if (myPanel.isTitleDescriptionEmptyOrNotModified()) { + Pair<String, String> description = myWorker.getDefaultDescriptionMessage(branch); + myPanel.setTitle(description.getFirst()); + myPanel.setDescription(description.getSecond()); + } - private void updateBranches(@NotNull Collection<String> branches, @NotNull GithubFullPath forkPath) { - myGithubCreatePullRequestPanel.setBranches(branches); + myWorker.launchLoadDiffInfo(branch); + } + } + }); - String configBranch = myProjectSettings.getCreatePullRequestDefaultBranch(); - if (configBranch != null) myGithubCreatePullRequestPanel.setSelectedBranch(configBranch); + myPanel.setForks(myWorker.getForks()); + myPanel.setSelectedFork(myProjectSettings.getCreatePullRequestDefaultRepo()); + myPanel.setSelectedBranch(myProjectSettings.getCreatePullRequestDefaultBranch()); - myGithubCreatePullRequestPanel.setForkName(forkPath.getFullName()); + setTitle("Create Pull Request - " + myWorker.getCurrentBranch()); + init(); } @Override protected void doOKAction() { - if (myWorker.checkAction(getTargetBranch())) { - myProjectSettings.setCreatePullRequestDefaultBranch(getTargetBranch()); - myWorker.performAction(getRequestTitle(), getDescription(), getTargetBranch()); + BranchInfo branch = myPanel.getSelectedBranch(); + if (myWorker.checkAction(branch)) { + assert branch != null; + myWorker.createPullRequest(branch, getRequestTitle(), getDescription()); + + myProjectSettings.setCreatePullRequestDefaultBranch(branch.getRemoteName()); + myProjectSettings.setCreatePullRequestDefaultRepo(branch.getForkInfo().getPath()); + super.doOKAction(); } } @@ -158,13 +175,13 @@ public class GithubCreatePullRequestDialog extends DialogWrapper { @Nullable @Override protected JComponent createCenterPanel() { - return myGithubCreatePullRequestPanel.getPanel(); + return myPanel.getPanel(); } @Nullable @Override public JComponent getPreferredFocusedComponent() { - return myGithubCreatePullRequestPanel.getPreferredComponent(); + return myPanel.getPreferredComponent(); } @Override @@ -179,50 +196,76 @@ public class GithubCreatePullRequestDialog extends DialogWrapper { @NotNull private String getRequestTitle() { - return myGithubCreatePullRequestPanel.getTitle(); + return myPanel.getTitle(); } @NotNull private String getDescription() { - return myGithubCreatePullRequestPanel.getDescription(); - } - - @NotNull - private String getTargetBranch() { - return myGithubCreatePullRequestPanel.getBranch(); + return myPanel.getDescription(); } @Nullable @Override protected ValidationInfo doValidate() { if (StringUtil.isEmptyOrSpaces(getRequestTitle())) { - return new ValidationInfo("Title can't be empty'", myGithubCreatePullRequestPanel.getTitleTextField()); + return new ValidationInfo("Title can't be empty'", myPanel.getTitleTextField()); } return null; } @TestOnly public void testSetRequestTitle(String title) { - myGithubCreatePullRequestPanel.setTitle(title); + myPanel.setTitle(title); } @TestOnly public void testSetBranch(String branch) { - myGithubCreatePullRequestPanel.setBranches(Collections.singleton(branch)); + myPanel.setSelectedBranch(branch); } @TestOnly public void testCreatePullRequest() { - myWorker.performAction(getRequestTitle(), getDescription(), getTargetBranch()); + myWorker.createPullRequest(myPanel.getSelectedBranch(), getRequestTitle(), getDescription()); } @TestOnly - public void testSetTarget(@NotNull GithubFullPath forkPath) { - GithubCreatePullRequestWorker.GithubTargetInfo forkInfo = myWorker.setTarget(forkPath); - if (forkInfo == null) { - doCancelAction(); - return; + public void testSetFork(@NotNull GithubFullPath forkPath) { + myPanel.setSelectedFork(forkPath); + } + + private static class CreateRemoteDoNotAskOption implements DoNotAskOption { + @Override + public boolean isToBeShown() { + return true; + } + + @Override + public void setToBeShown(boolean value, int exitCode) { + if (value) { + GithubSettings.getInstance().setCreatePullRequestCreateRemote(ThreeState.UNSURE); + } + else if (exitCode == DialogWrapper.OK_EXIT_CODE) { + GithubSettings.getInstance().setCreatePullRequestCreateRemote(ThreeState.YES); + } + else { + GithubSettings.getInstance().setCreatePullRequestCreateRemote(ThreeState.NO); + } + } + + @Override + public boolean canBeHidden() { + return true; + } + + @Override + public boolean shouldSaveOptionsOnCancel() { + return false; + } + + @NotNull + @Override + public String getDoNotShowMessage() { + return CommonBundle.message("dialog.options.do.not.ask"); } - myGithubCreatePullRequestPanel.setDiffEnabled(myWorker.canShowDiff()); } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.form b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.form index e19b8601fa01..342fcfaecde7 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.form +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.form @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.plugins.github.ui.GithubCreatePullRequestPanel"> - <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="5" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> + <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="5" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="0" left="0" bottom="0" right="0"/> <constraints> <xy x="20" y="20" width="500" height="400"/> @@ -34,7 +34,7 @@ </component> <component id="996e9" class="javax.swing.JTextField" binding="myTitleTextField"> <constraints> - <grid row="2" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> + <grid row="2" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> @@ -42,7 +42,7 @@ </component> <scrollpane id="61e54" class="com.intellij.ui.components.JBScrollPane"> <constraints> - <grid row="4" column="0" row-span="1" col-span="4" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"> + <grid row="4" column="0" row-span="1" col-span="3" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"> <minimum-size width="150" height="50"/> </grid> </constraints> @@ -57,7 +57,7 @@ </scrollpane> <component id="90c93" class="javax.swing.JButton" binding="myShowDiffButton"> <constraints> - <grid row="1" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/> + <grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Show Diff"/> @@ -73,26 +73,12 @@ </component> <component id="a5106" class="javax.swing.JButton" binding="mySelectForkButton"> <constraints> - <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/> + <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/> </constraints> <properties> - <text value="Select Base Fork"/> + <text value="Select Other Fork"/> </properties> </component> - <component id="e5548" class="javax.swing.JLabel" binding="myForkLabel"> - <constraints> - <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> - </constraints> - <properties> - <text value="repo"/> - </properties> - </component> - <component id="e5988" class="com.intellij.util.ui.AsyncProcessIcon" binding="myBusyIcon" custom-create="true"> - <constraints> - <grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/> - </constraints> - <properties/> - </component> <component id="78a64" class="com.intellij.openapi.ui.ComboBox" binding="myBranchComboBox"> <constraints> <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/> @@ -102,6 +88,12 @@ <toolTipText value=""/> </properties> </component> + <component id="952ec" class="com.intellij.openapi.ui.ComboBox" binding="myForkComboBox"> + <constraints> + <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="0" fill="1" indent="0" use-parent-layout="false"/> + </constraints> + <properties/> + </component> </children> </grid> </form> diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.java index 41512465eeea..f17562f12600 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.java +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubCreatePullRequestPanel.java @@ -19,9 +19,9 @@ import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.DocumentAdapter; import com.intellij.ui.SortedComboBoxModel; -import com.intellij.util.ui.AsyncProcessIcon; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.github.api.GithubFullPath; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -29,6 +29,9 @@ import javax.swing.event.DocumentListener; import java.util.Collection; import java.util.Comparator; +import static org.jetbrains.plugins.github.GithubCreatePullRequestWorker.BranchInfo; +import static org.jetbrains.plugins.github.GithubCreatePullRequestWorker.ForkInfo; + /** * @author Aleksey Pivovarov */ @@ -36,25 +39,35 @@ public class GithubCreatePullRequestPanel { private JTextField myTitleTextField; private JTextArea myDescriptionTextArea; private ComboBox myBranchComboBox; - private SortedComboBoxModel<String> myBranchModel; + private SortedComboBoxModel<ForkInfo> myForkModel; + private SortedComboBoxModel<BranchInfo> myBranchModel; private JPanel myPanel; private JButton myShowDiffButton; private JButton mySelectForkButton; private JLabel myForkLabel; - private AsyncProcessIcon myBusyIcon; + private ComboBox myForkComboBox; private boolean myTitleDescriptionUserModified = false; public GithubCreatePullRequestPanel() { myDescriptionTextArea.setBorder(BorderFactory.createEtchedBorder()); - myBranchModel = new SortedComboBoxModel<String>(new Comparator<String>() { + + myBranchModel = new SortedComboBoxModel<BranchInfo>(new Comparator<BranchInfo>() { @Override - public int compare(String o1, String o2) { - return StringUtil.naturalCompare(o1, o2); + public int compare(BranchInfo o1, BranchInfo o2) { + return StringUtil.naturalCompare(o1.getRemoteName(), o2.getRemoteName()); } }); myBranchComboBox.setModel(myBranchModel); + myForkModel = new SortedComboBoxModel<ForkInfo>(new Comparator<ForkInfo>() { + @Override + public int compare(ForkInfo o1, ForkInfo o2) { + return StringUtil.naturalCompare(o1.getPath().getUser(), o2.getPath().getUser()); + } + }); + myForkComboBox.setModel(myForkModel); + DocumentListener userModifiedDocumentListener = new DocumentAdapter() { @Override protected void textChanged(DocumentEvent e) { @@ -75,43 +88,69 @@ public class GithubCreatePullRequestPanel { return myDescriptionTextArea.getText(); } - @NotNull - public String getBranch() { - return myBranchComboBox.getSelectedItem().toString(); + @Nullable + public ForkInfo getSelectedFork() { + return myForkModel.getSelectedItem(); } - public void setDiffEnabled(boolean enabled) { - myShowDiffButton.setEnabled(enabled); + @Nullable + public BranchInfo getSelectedBranch() { + return myBranchModel.getSelectedItem(); } - public void setSelectedBranch(@Nullable String branch) { - if (StringUtil.isEmptyOrSpaces(branch)) { - return; + public void setSelectedFork(@Nullable GithubFullPath path) { + if (path != null) { + for (ForkInfo info : myForkModel.getItems()) { + if (path.equals(info.getPath())) { + myForkModel.setSelectedItem(info); + return; + } + } } - myBranchComboBox.setSelectedItem(branch); + if (myForkModel.getSize() > 0) myForkModel.setSelectedItem(myForkModel.get(0)); } - public void setBranches(@NotNull Collection<String> branches) { - myBranchModel.clear(); - myBranchModel.addAll(branches); - if (branches.size() > 0) { - myBranchComboBox.setSelectedIndex(0); + public void setSelectedBranch(@Nullable String branch) { + if (branch != null) { + for (BranchInfo info : myBranchModel.getItems()) { + if (branch.equals(info.getRemoteName())) { + myBranchModel.setSelectedItem(info); + return; + } + } } + + if (myBranchModel.getSize() > 0) myBranchModel.setSelectedItem(myBranchModel.get(0)); } - public JPanel getPanel() { - return myPanel; + public void setForks(@NotNull Collection<ForkInfo> forks) { + myForkModel.setSelectedItem(null); + myForkModel.setAll(forks); } - @NotNull - public JComponent getPreferredComponent() { - return myTitleTextField; + public void setBranches(@NotNull Collection<BranchInfo> branches) { + myBranchModel.setSelectedItem(null); + myBranchModel.setAll(branches); } - @NotNull - public JComponent getBranchEditor() { - return myBranchComboBox; + public void setTitle(@Nullable String title) { + myTitleTextField.setText(title); + myTitleDescriptionUserModified = false; + } + + public void setDescription(@Nullable String title) { + myDescriptionTextArea.setText(title); + myTitleDescriptionUserModified = false; + } + + public boolean isTitleDescriptionEmptyOrNotModified() { + return !myTitleDescriptionUserModified || + (StringUtil.isEmptyOrSpaces(myTitleTextField.getText()) && StringUtil.isEmptyOrSpaces(myDescriptionTextArea.getText())); + } + + public void setDiffEnabled(boolean enabled) { + myShowDiffButton.setEnabled(enabled); } @NotNull @@ -120,13 +159,18 @@ public class GithubCreatePullRequestPanel { } @NotNull + public JButton getSelectForkButton() { + return mySelectForkButton; + } + + @NotNull public JButton getShowDiffButton() { return myShowDiffButton; } @NotNull - public JButton getSelectForkButton() { - return mySelectForkButton; + public ComboBox getForkComboBox() { + return myForkComboBox; } @NotNull @@ -134,36 +178,12 @@ public class GithubCreatePullRequestPanel { return myBranchComboBox; } - public void setTitle(@Nullable String title) { - myTitleTextField.setText(title); - myTitleDescriptionUserModified = false; - } - - public void setDescription(@Nullable String title) { - myDescriptionTextArea.setText(title); - myTitleDescriptionUserModified = false; - } - - public boolean isTitleDescriptionEmptyOrNotModified() { - return !myTitleDescriptionUserModified || - (StringUtil.isEmptyOrSpaces(myTitleTextField.getText()) && StringUtil.isEmptyOrSpaces(myDescriptionTextArea.getText())); - } - - public void setForkName(@NotNull String forkName) { - myForkLabel.setText(forkName); - } - - public void setBusy(boolean enabled) { - if (enabled) { - myBusyIcon.resume(); - } - else { - myBusyIcon.suspend(); - } + public JPanel getPanel() { + return myPanel; } - private void createUIComponents() { - myBusyIcon = new AsyncProcessIcon("Loading diff..."); - myBusyIcon.suspend(); + @NotNull + public JComponent getPreferredComponent() { + return myTitleTextField; } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkDialog.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkDialog.java index d8ce684eeb38..cea8d206a845 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkDialog.java +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkDialog.java @@ -27,7 +27,9 @@ import org.jetbrains.plugins.github.api.GithubFullPath; import org.jetbrains.plugins.github.util.GithubNotifications; import javax.swing.*; -import java.util.Set; +import java.util.List; + +import static org.jetbrains.plugins.github.GithubCreatePullRequestWorker.ForkInfo; /** * @author Aleksey Pivovarov @@ -35,25 +37,27 @@ import java.util.Set; public class GithubSelectForkDialog extends DialogWrapper { @NotNull private final GithubSelectForkPanel myPanel; @NotNull private final Project myProject; - @NotNull private final Convertor<String, GithubFullPath> myCheckFork; - private GithubFullPath myFullPath; + @NotNull private final Convertor<String, ForkInfo> myCheckFork; + private ForkInfo mySelectedFork; public GithubSelectForkDialog(@NotNull Project project, - @NotNull Set<GithubFullPath> forks, - @NotNull Convertor<String, GithubFullPath> checkFork) { + @Nullable List<GithubFullPath> forks, + @NotNull Convertor<String, ForkInfo> checkFork) { super(project); myProject = project; myCheckFork = checkFork; myPanel = new GithubSelectForkPanel(); - myPanel.setUsers(ContainerUtil.map(forks, new Function<GithubFullPath, String>() { - @Override - public String fun(GithubFullPath path) { - return path.getUser(); - } - })); + if (forks != null) { + myPanel.setUsers(ContainerUtil.map(forks, new Function<GithubFullPath, String>() { + @Override + public String fun(GithubFullPath path) { + return path.getUser(); + } + })); + } setTitle("Select Base Fork Repository"); init(); @@ -61,12 +65,12 @@ public class GithubSelectForkDialog extends DialogWrapper { @Override protected void doOKAction() { - GithubFullPath path = myCheckFork.convert(myPanel.getUser()); - if (path == null) { + ForkInfo fork = myCheckFork.convert(myPanel.getUser()); + if (fork == null) { GithubNotifications.showErrorDialog(myProject, "Can't Find Repository", "Can't find fork for selected user"); } else { - myFullPath = path; + mySelectedFork = fork; super.doOKAction(); } } @@ -78,12 +82,7 @@ public class GithubSelectForkDialog extends DialogWrapper { } @NotNull - public GithubFullPath getPath() { - return myFullPath; - } - - @TestOnly - public void testSetUser(@NotNull String user) { - myPanel.setSelectedUser(user); + public ForkInfo getPath() { + return mySelectedFork; } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkPanel.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkPanel.java index 754afc617057..cb3e014ee7dc 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkPanel.java +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSelectForkPanel.java @@ -57,14 +57,6 @@ public class GithubSelectForkPanel { return myComboBox.getSelectedItem().toString(); } - public void setSelectedUser(@Nullable String user) { - if (StringUtil.isEmptyOrSpaces(user)) { - return; - } - - myComboBox.setSelectedItem(user); - } - public JPanel getPanel() { return myPanel; } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSettingsPanel.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSettingsPanel.java index 7de0dae30d69..9bcf2aa78fc8 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSettingsPanel.java +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubSettingsPanel.java @@ -30,6 +30,7 @@ import com.intellij.util.ThrowableConvertor; 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.GithubUser; import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException; import org.jetbrains.plugins.github.exceptions.GithubOperationCanceledException; @@ -112,11 +113,10 @@ public class GithubSettingsPanel { } } catch (GithubAuthenticationException ex) { - GithubNotifications.showErrorDialog(myPane, "Login Failure", "Can't login using given credentials: " + ex.getMessage()); + GithubNotifications.showErrorDialog(myPane, "Login Failure", "Can't login using given credentials: ", ex); } catch (IOException ex) { - LOG.info(ex); - GithubNotifications.showErrorDialog(myPane, "Login Failure", "Can't login: " + GithubUtil.getErrorTextFromException(ex)); + GithubNotifications.showErrorDialog(myPane, "Login Failure", "Can't login: ", ex); } } }); @@ -131,11 +131,12 @@ public class GithubSettingsPanel { @Override public String convert(ProgressIndicator indicator) throws IOException { return GithubUtil.runTaskWithBasicAuthForHost(project, GithubAuthDataHolder.createFromSettings(), indicator, getHost(), - new ThrowableConvertor<GithubAuthData, String, IOException>() { + new ThrowableConvertor<GithubConnection, String, IOException>() { @NotNull @Override - public String convert(@NotNull GithubAuthData auth) throws IOException { - return GithubApiUtil.getMasterToken(auth, "IntelliJ plugin"); + public String convert(@NotNull GithubConnection connection) + throws IOException { + return GithubApiUtil.getMasterToken(connection, "IntelliJ plugin"); } } ); diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubNotifications.java b/plugins/github/src/org/jetbrains/plugins/github/util/GithubNotifications.java index 767c620c93a4..d34441bf1ba3 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/util/GithubNotifications.java +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubNotifications.java @@ -18,6 +18,7 @@ package org.jetbrains.plugins.github.util; import com.intellij.notification.NotificationListener; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vcs.VcsNotifier; import org.jetbrains.annotations.NotNull; @@ -41,6 +42,12 @@ public class GithubNotifications { VcsNotifier.getInstance(project).notifyImportantWarning(title, message); } + public static void showWarning(@NotNull Project project, @NotNull String title, @NotNull Exception e) { + LOG.info(title + "; ", e); + if (e instanceof GithubOperationCanceledException) return; + VcsNotifier.getInstance(project).notifyImportantWarning(title, getErrorTextFromException(e)); + } + public static void showError(@NotNull Project project, @NotNull String title, @NotNull String message) { LOG.info(title + "; " + message); VcsNotifier.getInstance(project).notifyError(title, message); @@ -116,19 +123,28 @@ public class GithubNotifications { Messages.showErrorDialog(project, getErrorTextFromException(e), title); } - public static void showErrorDialog(@NotNull Component component, @NotNull String title, @NotNull String message) { - LOG.info(title + "; " + message); - Messages.showErrorDialog(component, message, title); - } - public static void showErrorDialog(@NotNull Component component, @NotNull String title, @NotNull Exception e) { LOG.info(title, e); if (e instanceof GithubOperationCanceledException) return; Messages.showErrorDialog(component, getErrorTextFromException(e), title); } + public static void showErrorDialog(@NotNull Component component, @NotNull String title, @NotNull String prefix, @NotNull Exception e) { + LOG.info(title, e); + if (e instanceof GithubOperationCanceledException) return; + Messages.showErrorDialog(component, prefix + getErrorTextFromException(e), title); + } + @Messages.YesNoResult public static int showYesNoDialog(@Nullable Project project, @NotNull String title, @NotNull String message) { return Messages.showYesNoDialog(project, message, title, Messages.getQuestionIcon()); } + + @Messages.YesNoResult + public static int showYesNoDialog(@Nullable Project project, + @NotNull String title, + @NotNull String message, + @NotNull DialogWrapper.DoNotAskOption doNotAskOption) { + return Messages.showYesNoDialog(project, message, title, Messages.getQuestionIcon(), doNotAskOption); + } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubSettings.java b/plugins/github/src/org/jetbrains/plugins/github/util/GithubSettings.java index 3523e412a95e..7e98d47d8220 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/util/GithubSettings.java +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubSettings.java @@ -22,6 +22,7 @@ import com.intellij.ide.passwordSafe.impl.PasswordSafeImpl; import com.intellij.openapi.components.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ThreeState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.api.GithubApiUtil; @@ -60,6 +61,7 @@ public class GithubSettings implements PersistentStateComponent<GithubSettings.S public boolean SAVE_PASSWORD = true; public int CONNECTION_TIMEOUT = 5000; public boolean VALID_GIT_AUTH = true; + public ThreeState CREATE_PULL_REQUEST_CREATE_REMOTE = ThreeState.UNSURE; } public static GithubSettings getInstance() { @@ -130,6 +132,15 @@ public class GithubSettings implements PersistentStateComponent<GithubSettings.S return passwordSafe.getSettings().getProviderType() == PasswordSafeSettings.ProviderType.MASTER_PASSWORD; } + @NotNull + public ThreeState getCreatePullRequestCreateRemote() { + return myState.CREATE_PULL_REQUEST_CREATE_REMOTE; + } + + public void setCreatePullRequestCreateRemote(@NotNull ThreeState value) { + myState.CREATE_PULL_REQUEST_CREATE_REMOTE = value; + } + public void setAnonymousGist(final boolean anonymousGist) { myState.ANONYMOUS_GIST = anonymousGist; } diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubUtil.java b/plugins/github/src/org/jetbrains/plugins/github/util/GithubUtil.java index edd12ab85ab6..34c53da7a97f 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/util/GithubUtil.java +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubUtil.java @@ -15,6 +15,7 @@ */ 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; @@ -23,12 +24,14 @@ 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; @@ -46,6 +49,7 @@ 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; @@ -57,6 +61,8 @@ 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. @@ -72,11 +78,21 @@ public class GithubUtil { // TODO: Consider sharing of GithubAuthData between actions (as member of GithubSettings) public static <T> T runTask(@NotNull Project project, @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator, - @NotNull ThrowableConvertor<GithubAuthData, T, IOException> task) throws IOException { + @NotNull final ProgressIndicator indicator, + @NotNull ThrowableConvertor<GithubConnection, T, IOException> task) throws IOException { GithubAuthData auth = authHolder.getAuthData(); try { - return task.convert(auth); + 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); @@ -90,11 +106,21 @@ public class GithubUtil { public static void runTask(@NotNull Project project, @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator, - @NotNull ThrowableConsumer<GithubAuthData, IOException> task) throws IOException { + @NotNull final ProgressIndicator indicator, + @NotNull ThrowableConsumer<GithubConnection, IOException> task) throws IOException { GithubAuthData auth = authHolder.getAuthData(); try { - task.consume(auth); + 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); @@ -108,15 +134,26 @@ public class GithubUtil { public static <T> T runTaskWithBasicAuthForHost(@NotNull Project project, @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator, + @NotNull final ProgressIndicator indicator, @NotNull String host, - @NotNull ThrowableConvertor<GithubAuthData, T, IOException> task) throws IOException { + @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"); } - return task.convert(auth); + + 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); @@ -131,10 +168,20 @@ public class GithubUtil { @NotNull private static GithubUserDetailed testConnection(@NotNull Project project, @NotNull GithubAuthDataHolder authHolder, - @NotNull ProgressIndicator indicator) throws IOException { + @NotNull final ProgressIndicator indicator) throws IOException { GithubAuthData auth = authHolder.getAuthData(); try { - return GithubApiUtil.getCurrentUserDetailed(auth); + 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); @@ -142,6 +189,33 @@ public class GithubUtil { } } + @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, @@ -220,7 +294,7 @@ public class GithubUtil { throw new GithubOperationCanceledException("Two factor authentication can be used only with Login/Password"); } - GithubApiUtil.askForTwoFactorCodeSMS(oldAuth); + GithubApiUtil.askForTwoFactorCodeSMS(new GithubConnection(oldAuth, false)); final Ref<String> codeRef = new Ref<String>(); ApplicationManager.getApplication().invokeAndWait(new Runnable() { @@ -301,13 +375,7 @@ public class GithubUtil { try { dataRef.set(task.convert(indicator)); } - catch (IOException e) { - exceptionRef.set(e); - } - catch (Error e) { - exceptionRef.set(e); - } - catch (RuntimeException e) { + catch (Throwable e) { exceptionRef.set(e); } } @@ -325,17 +393,21 @@ public class GithubUtil { 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, true) { + ProgressManager.getInstance().run(new Task.Modal(project, caption, canBeCancelled) { public void run(@NotNull ProgressIndicator indicator) { try { dataRef.set(task.convert(indicator)); } - catch (Error e) { - exceptionRef.set(e); - } - catch (RuntimeException e) { + catch (Throwable e) { exceptionRef.set(e); } } @@ -352,16 +424,20 @@ public class GithubUtil { 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, true) { + ProgressManager.getInstance().run(new Task.Modal(project, caption, canBeCancelled) { public void run(@NotNull ProgressIndicator indicator) { try { task.consume(indicator); } - catch (Error e) { - exceptionRef.set(e); - } - catch (RuntimeException e) { + catch (Throwable e) { exceptionRef.set(e); } } @@ -374,6 +450,49 @@ public class GithubUtil { } } + 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 */ |