Merge branch 'createPR'

Conflicts:
	plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestAction.java
	plugins/github/src/org/jetbrains/plugins/github/api/GithubApiUtil.java
This commit is contained in:
Aleksey Pivovarov
2013-10-28 18:00:41 +04:00
13 changed files with 1113 additions and 663 deletions
@@ -17,56 +17,16 @@ package org.jetbrains.plugins.github;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.ThrowableConsumer;
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.util.containers.HashSet;
import git4idea.DialogManager;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.branch.GitBranchUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitCompareBranchesDialog;
import git4idea.update.GitFetchResult;
import git4idea.update.GitFetcher;
import git4idea.util.GitCommitCompareInfo;
import icons.GithubIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.*;
import org.jetbrains.plugins.github.exceptions.GithubAuthenticationCanceledException;
import org.jetbrains.plugins.github.ui.GithubCreatePullRequestDialog;
import org.jetbrains.plugins.github.util.GithubAuthData;
import org.jetbrains.plugins.github.util.GithubNotifications;
import org.jetbrains.plugins.github.util.GithubUrlUtil;
import org.jetbrains.plugins.github.util.GithubUtil;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.plugins.github.util.*;
import static org.jetbrains.plugins.github.util.GithubUtil.setVisibleEnabled;
@@ -74,9 +34,6 @@ import static org.jetbrains.plugins.github.util.GithubUtil.setVisibleEnabled;
* @author Aleksey Pivovarov
*/
public class GithubCreatePullRequestAction extends DumbAwareAction {
private static final Logger LOG = GithubUtil.LOG;
private static final String CANNOT_CREATE_PULL_REQUEST = "Can't create pull request";
public GithubCreatePullRequestAction() {
super("Create Pull Request", "Create pull request from current branch", GithubIcons.Github_icon);
}
@@ -115,543 +72,13 @@ public class GithubCreatePullRequestAction extends DumbAwareAction {
createPullRequest(project, file);
}
static void createPullRequest(@NotNull final Project project, @Nullable final VirtualFile file) {
final Git git = ServiceManager.getService(Git.class);
final GitRepository repository = GithubUtil.getGitRepository(project, file);
if (repository == null) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find git repository");
return;
}
repository.update();
Pair<GitRemote, String> remote = GithubUtil.findGithubRemote(repository);
if (remote == null) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find GitHub remote");
return;
}
final String remoteUrl = remote.getSecond();
final String remoteName = remote.getFirst().getName();
GithubFullPath userAndRepo = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(remoteUrl);
if (userAndRepo == null) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't process remote: " + remoteUrl);
static void createPullRequest(@NotNull Project project, @Nullable VirtualFile file) {
GithubCreatePullRequestWorker worker = GithubCreatePullRequestWorker.createPullRequestWorker(project, file);
if (worker == null) {
return;
}
final GitLocalBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "No current branch");
return;
}
String upstreamUrl = GithubUtil.findUpstreamRemote(repository);
GithubFullPath upstreamUserAndRepo =
upstreamUrl == null || !GithubUrlUtil.isGithubUrl(upstreamUrl) ? null : GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(upstreamUrl);
final Map<String, String> forks = new HashMap<String, String>();
final Set<RemoteBranch> branches = new HashSet<RemoteBranch>();
addAvailableBranchesFromGit(repository, forks, branches);
GithubInfo info = loadGithubInfoAndBranchesWithModal(project, userAndRepo, upstreamUserAndRepo, forks, branches);
if (info == null) {
return;
}
final GithubRepoDetailed repo = info.getRepo();
final GithubAuthData auth = info.getAuthData();
GithubRepo parent = repo.getParent();
String defaultBranch =
parent == null || parent.getDefaultBranch() == null ? null : parent.getUserName() + ":" + parent.getDefaultBranch();
Collection<String> suggestions = ContainerUtil.map(branches, new Function<RemoteBranch, String>() {
@Override
public String fun(RemoteBranch remoteBranch) {
return remoteBranch.getReference();
}
});
Consumer<String> showDiff = new Consumer<String>() {
@Override
public void consume(String ref) {
showDiffByRef(project, ref, repository, currentBranch.getName(), auth, forks, branches, repo.getSource());
}
};
final GithubCreatePullRequestDialog dialog = new GithubCreatePullRequestDialog(project, suggestions, defaultBranch, showDiff);
GithubCreatePullRequestDialog dialog = new GithubCreatePullRequestDialog(worker);
DialogManager.show(dialog);
if (!dialog.isOK()) {
return;
}
new Task.Backgroundable(project, "Creating pull request...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
LOG.info("Pushing current branch");
indicator.setText("Pushing current branch...");
GitCommandResult result = git.push(repository, remoteName, remoteUrl, currentBranch.getName(), true);
if (!result.success()) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Push failed:<br/>" + result.getErrorOutputAsHtmlString());
return;
}
String from = repo.getUserName() + ":" + currentBranch.getName();
String onto = dialog.getTargetBranch();
String targetUser = onto.substring(0, onto.indexOf(':'));
GithubFullPath targetRepo = findRepositoryByUser(project, targetUser, forks, auth, repo.getSource());
if (targetRepo == null) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find repository for specified branch: " + onto);
return;
}
LOG.info("Creating pull request");
indicator.setText("Creating pull request...");
GithubPullRequest request =
createPullRequest(project, auth, targetRepo, dialog.getRequestTitle(), dialog.getDescription(), from, onto);
if (request == null) {
return;
}
GithubNotifications
.showInfoURL(project, "Successfully created pull request", "Pull Request #" + request.getNumber(), request.getHtmlUrl());
}
}.queue();
}
@Nullable
private static GithubInfo loadGithubInfoAndBranchesWithModal(@NotNull final Project project,
@NotNull final GithubFullPath userAndRepo,
@Nullable final GithubFullPath upstreamUserAndRepo,
@NotNull final Map<String, String> forks,
@NotNull final Set<RemoteBranch> branches) {
try {
return GithubUtil
.computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo, IOException>() {
@Override
public GithubInfo convert(ProgressIndicator indicator) throws IOException {
final AtomicReference<GithubRepoDetailed> reposRef = new AtomicReference<GithubRepoDetailed>();
final GithubAuthData auth =
GithubUtil.runAndGetValidAuth(project, indicator, new ThrowableConsumer<GithubAuthData, IOException>() {
@Override
public void consume(GithubAuthData authData) throws IOException {
reposRef.set(GithubApiUtil.getDetailedRepoInfo(authData, userAndRepo.getUser(), userAndRepo.getRepository()));
}
});
addAvailableBranchesFromGithub(project, auth, reposRef.get(), upstreamUserAndRepo, forks, branches);
return new GithubInfo(auth, reposRef.get());
}
});
}
catch (GithubAuthenticationCanceledException e) {
return null;
}
catch (IOException e) {
GithubNotifications.showErrorDialog(project, CANNOT_CREATE_PULL_REQUEST, e);
return null;
}
}
@Nullable
private static GithubFullPath findRepositoryByUser(@NotNull Project project,
@NotNull String user,
@NotNull Map<String, String> forks,
@NotNull GithubAuthData auth,
@Nullable GithubRepo source) {
for (Map.Entry<String, String> entry : forks.entrySet()) {
if (StringUtil.equalsIgnoreCase(user, entry.getKey())) {
return new GithubFullPath(entry.getKey(), entry.getValue());
}
}
if (source != null) {
try {
GithubRepoDetailed target = GithubApiUtil.getDetailedRepoInfo(auth, user, source.getName());
if (target.getSource() != null && StringUtil.equals(target.getSource().getUserName(), source.getUserName())) {
forks.put(target.getUserName(), target.getName());
return target.getFullPath();
}
}
catch (IOException ignore) {
// such repo may not exist
}
try {
GithubRepo fork = GithubApiUtil.findForkByUser(auth, source.getUserName(), source.getName(), user);
if (fork != null) {
forks.put(fork.getUserName(), fork.getName());
return fork.getFullPath();
}
}
catch (IOException e) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
}
}
return null;
}
@Nullable
private static GithubPullRequest createPullRequest(@NotNull Project project,
@NotNull GithubAuthData auth,
@NotNull GithubFullPath targetRepo,
@NotNull String title,
@NotNull String description,
@NotNull String from,
@NotNull String onto) {
try {
return GithubApiUtil.createPullRequest(auth, targetRepo.getUser(), targetRepo.getRepository(), title, description, from, onto);
}
catch (IOException e) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
return null;
}
}
private static void addAvailableBranchesFromGit(@NotNull GitRepository gitRepository,
@NotNull Map<String, String> forks,
@NotNull Set<RemoteBranch> branches) {
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.put(path.getUser(), path.getRepository());
branches.add(new RemoteBranch(path.getUser(), remoteBranch.getNameForRemoteOperations(), remoteBranch));
break;
}
}
}
}
}
private static void addAvailableBranchesFromGithub(@NotNull final Project project,
@NotNull final GithubAuthData auth,
@NotNull final GithubRepoDetailed repo,
@Nullable final GithubFullPath upstreamPath,
@NotNull Map<String, String> forks,
@NotNull Set<RemoteBranch> branches) {
try {
final GithubRepo parent = repo.getParent();
final GithubRepo source = repo.getSource();
forks.put(repo.getUserName(), repo.getName());
branches.addAll(getBranches(auth, repo.getUserName(), repo.getName()));
if (parent != null) {
forks.put(parent.getUserName(), parent.getName());
branches.addAll(getBranches(auth, parent.getUserName(), parent.getName()));
}
if (source != null && !equals(source, parent)) {
forks.put(source.getUserName(), source.getName());
branches.addAll(getBranches(auth, source.getUserName(), source.getName()));
}
if (upstreamPath != null && !equals(upstreamPath, repo) && !equals(upstreamPath, parent) && !equals(upstreamPath, source)) {
forks.put(upstreamPath.getUser(), upstreamPath.getRepository());
branches.addAll(getBranches(auth, upstreamPath.getUser(), upstreamPath.getRepository()));
}
}
catch (IOException e) {
GithubNotifications.showError(project, "Can't load available branches", e);
}
}
@NotNull
private static List<RemoteBranch> getBranches(@NotNull GithubAuthData auth, @NotNull final String user, @NotNull final String repo)
throws IOException {
List<GithubBranch> branches = GithubApiUtil.getRepoBranches(auth, user, repo);
return ContainerUtil.map(branches, new Function<GithubBranch, RemoteBranch>() {
@Override
public RemoteBranch fun(GithubBranch branch) {
return new RemoteBranch(user, branch.getName());
}
});
}
private static boolean equals(@NotNull GithubRepo repo1, @Nullable GithubRepo repo2) {
if (repo2 == null) {
return false;
}
return StringUtil.equals(repo1.getUserName(), repo2.getUserName());
}
private static boolean equals(@NotNull GithubFullPath repo1, @Nullable GithubRepo repo2) {
if (repo2 == null) {
return false;
}
return StringUtil.equals(repo1.getUser(), repo2.getUserName());
}
private static void showDiffByRef(@NotNull final Project project,
@Nullable final String ref,
@NotNull final GitRepository gitRepository,
@NotNull final String currentBranch,
@NotNull final GithubAuthData auth,
@NotNull final Map<String, String> forks,
@NotNull final Set<RemoteBranch> branches,
@Nullable final GithubRepo source) {
if (ref == null) {
return;
}
DiffInfo info = GithubUtil.computeValueInModal(project, "Collecting diff data...", new Convertor<ProgressIndicator, DiffInfo>() {
@Override
@Nullable
public DiffInfo convert(ProgressIndicator indicator) {
List<String> list = StringUtil.split(ref, ":");
assert list.size() == 2 : ref;
final String user = list.get(0);
final String branch = list.get(1);
TargetBranchInfo targetBranchInfo;
RemoteBranch remoteBranch = findRemoteBranch(branches, user, branch);
if (remoteBranch != null && remoteBranch.getRemoteBranch() != null) {
targetBranchInfo = getTargetBranchInfo(remoteBranch.getRemoteBranch());
}
else {
GithubFullPath forkPath = findRepositoryByUser(project, user, forks, auth, source);
if (forkPath == null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
GithubNotifications.showErrorDialog(project, "Can't show diff", "Can't find fork for user '" + user + "'");
}
}, indicator.getModalityState());
return null;
}
targetBranchInfo = findRemote(branch, gitRepository, forkPath);
if (targetBranchInfo == null) {
final AtomicReference<Integer> responseRef = new AtomicReference<Integer>();
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
responseRef.set(GithubNotifications.showYesNoDialog(project, "Can't find remote", "Configure remote for '" + user + "'?"));
}
}, indicator.getModalityState());
if (responseRef.get() != Messages.YES) {
return null;
}
targetBranchInfo = configureRemote(project, user, branch, gitRepository, forkPath);
}
}
if (targetBranchInfo == null) {
return null;
}
GitFetchResult result = new GitFetcher(project, indicator, false)
.fetch(gitRepository.getRoot(), targetBranchInfo.getRemote(), targetBranchInfo.getBranchNameForRemoteOperations());
if (!result.isSuccess()) {
GitFetcher.displayFetchResult(project, result, null, result.getErrors());
return null;
}
DiffInfo info = getDiffInfo(project, gitRepository, currentBranch, targetBranchInfo.getBranchNameForLocalOperations());
if (info == null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
GithubNotifications.showErrorDialog(project, "Can't show diff", "Can't get diff info");
}
}, indicator.getModalityState());
return null;
}
return info;
}
});
if (info == null) {
return;
}
GitCompareBranchesDialog dialog = new GitCompareBranchesDialog(project, info.getTo(), info.getFrom(), info.getInfo(), gitRepository);
dialog.show();
}
private static TargetBranchInfo getTargetBranchInfo(@NotNull GitRemoteBranch remoteBranch) {
return new TargetBranchInfo(remoteBranch.getRemote().getName(), remoteBranch.getNameForRemoteOperations());
}
@Nullable
private static TargetBranchInfo findRemote(@NotNull String branch,
@NotNull GitRepository gitRepository,
@NotNull GithubFullPath forkPath) {
GitRemote remote = GithubUtil.findGithubRemote(gitRepository, forkPath);
return remote == null ? null : new TargetBranchInfo(remote.getName(), branch);
}
@Nullable
private static TargetBranchInfo configureRemote(@NotNull Project project,
@NotNull String user,
@NotNull String branch,
@NotNull GitRepository gitRepository,
@NotNull GithubFullPath forkPath) {
String url = GithubUrlUtil.getCloneUrl(forkPath);
if (GithubUtil.addGithubRemote(project, gitRepository, user, url)) {
return new TargetBranchInfo(user, branch);
}
else {
return null;
}
}
@Nullable
private static RemoteBranch findRemoteBranch(@NotNull Set<RemoteBranch> branches, @NotNull String user, @NotNull String branch) {
for (RemoteBranch remoteBranch : branches) {
if (StringUtil.equalsIgnoreCase(user, remoteBranch.getUser()) && StringUtil.equals(branch, remoteBranch.getBranch())) {
return remoteBranch;
}
}
return null;
}
@Nullable
private static DiffInfo getDiffInfo(@NotNull final Project project,
@NotNull final GitRepository repository,
@NotNull final String currentBranch,
@NotNull final String targetBranch) {
try {
List<GitCommit> commits = 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, Pair.<List<GitCommit>, List<GitCommit>>create(new ArrayList<GitCommit>(), commits));
return new DiffInfo(info, currentBranch, targetBranch);
}
catch (VcsException e) {
LOG.info(e);
return null;
}
}
private static class RemoteBranch {
@NotNull final String myUser;
@NotNull final String myBranch;
@Nullable final GitRemoteBranch myRemoteBranch;
private RemoteBranch(@NotNull String user, @NotNull String branch) {
this(user, branch, null);
}
public RemoteBranch(@NotNull String user, @NotNull String branch, @Nullable GitRemoteBranch localBranch) {
myUser = user;
myBranch = branch;
myRemoteBranch = localBranch;
}
@NotNull
public String getReference() {
return myUser + ":" + myBranch;
}
@NotNull
public String getUser() {
return myUser;
}
@NotNull
public String getBranch() {
return myBranch;
}
@Nullable
public GitRemoteBranch getRemoteBranch() {
return myRemoteBranch;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RemoteBranch that = (RemoteBranch)o;
if (!StringUtil.equals(myUser, that.myUser)) return false;
if (!StringUtil.equals(myBranch, that.myBranch)) return false;
return true;
}
@Override
public int hashCode() {
int result = myUser.hashCode();
result = 31 * result + myBranch.hashCode();
return result;
}
}
private static class GithubInfo {
@NotNull private final GithubRepoDetailed myRepo;
@NotNull private final GithubAuthData myAuthData;
private GithubInfo(@NotNull GithubAuthData authData, @NotNull GithubRepoDetailed repo) {
myAuthData = authData;
myRepo = repo;
}
@NotNull
public GithubRepoDetailed getRepo() {
return myRepo;
}
@NotNull
public GithubAuthData getAuthData() {
return myAuthData;
}
}
private static class DiffInfo {
@NotNull private final GitCommitCompareInfo myInfo;
@NotNull private final String myFrom;
@NotNull private final String myTo;
private DiffInfo(@NotNull GitCommitCompareInfo info, @NotNull String from, @NotNull String to) {
myInfo = info;
myFrom = from;
myTo = to;
}
@NotNull
public GitCommitCompareInfo getInfo() {
return myInfo;
}
@NotNull
public String getFrom() {
return myFrom;
}
@NotNull
public String getTo() {
return myTo;
}
}
private static class TargetBranchInfo {
@NotNull private final String myRemote;
@NotNull private final String myName;
@NotNull private final String myNameAtRemote;
private TargetBranchInfo(@NotNull String remote, @NotNull String nameAtRemote) {
myRemote = remote;
myNameAtRemote = GitBranchUtil.stripRefsPrefix(nameAtRemote);
myName = myRemote + "/" + myNameAtRemote;
}
@NotNull
public String getRemote() {
return myRemote;
}
@NotNull
public String getBranchNameForLocalOperations() {
return myName;
}
@NotNull
public String getBranchNameForRemoteOperations() {
return myNameAtRemote;
}
}
}
}
@@ -0,0 +1,623 @@
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.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.containers.*;
import com.intellij.util.containers.HashMap;
import git4idea.DialogManager;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitCompareBranchesDialog;
import git4idea.update.GitFetchResult;
import git4idea.update.GitFetcher;
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.GithubAuthenticationCanceledException;
import org.jetbrains.plugins.github.ui.GithubSelectForkDialog;
import org.jetbrains.plugins.github.util.GithubAuthData;
import org.jetbrains.plugins.github.util.GithubNotifications;
import org.jetbrains.plugins.github.util.GithubUrlUtil;
import org.jetbrains.plugins.github.util.GithubUtil;
import java.io.IOException;
import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
/**
* @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";
@NotNull private final Project myProject;
@NotNull private final Git myGit;
@NotNull private final GitRepository myGitRepository;
@NotNull private final GithubFullPath myPath;
@NotNull private final String myRemoteName;
@NotNull private final String myRemoteUrl;
@NotNull private final String myCurrentBranch;
@NotNull private final GithubAuthData myAuth;
@NotNull private final Map<String, FutureTask<DiffInfo>> myDiffInfos;
private volatile GithubFullPath myForkPath;
private volatile String myTargetRemote;
private GithubCreatePullRequestWorker(@NotNull Project project,
@NotNull Git git,
@NotNull GitRepository gitRepository,
@NotNull GithubFullPath path,
@NotNull String remoteName,
@NotNull String remoteUrl,
@NotNull String currentBranch,
@NotNull GithubAuthData auth) {
myProject = project;
myGit = git;
myGitRepository = gitRepository;
myPath = path;
myRemoteName = remoteName;
myRemoteUrl = remoteUrl;
myCurrentBranch = currentBranch;
myAuth = auth;
myDiffInfos = new HashMap<String, FutureTask<DiffInfo>>();
}
@NotNull
public Project getProject() {
return myProject;
}
@NotNull
public String getCurrentBranch() {
return myCurrentBranch;
}
public boolean canShowDiff() {
return myTargetRemote != null;
}
@Nullable
public static GithubCreatePullRequestWorker createPullRequestWorker(@NotNull final Project project, @Nullable final VirtualFile file) {
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.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;
}
GithubAuthData auth;
try {
auth = GithubUtil
.computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubAuthData, IOException>() {
@Override
public GithubAuthData convert(ProgressIndicator indicator) throws IOException {
return GithubUtil.getValidAuthDataFromConfig(project, indicator);
}
});
}
catch (GithubAuthenticationCanceledException e) {
return null;
}
catch (IOException e) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
return null;
}
return new GithubCreatePullRequestWorker(project, git, gitRepository, path, remoteName, remoteUrl, currentBranch.getName(), auth);
}
@Nullable
public GithubTargetInfo setTarget(@NotNull final GithubFullPath forkPath) {
try {
GithubInfo info =
GithubUtil.computeValueInModal(myProject, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo, IOException>() {
@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 AtomicReference<Integer> responseRef = new AtomicReference<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);
}
}
// load available branches
List<String> branches = ContainerUtil.map(GithubApiUtil.getRepoBranches(myAuth, forkPath.getUser(), forkPath.getRepository()),
new Function<GithubBranch, String>() {
@Override
public String fun(GithubBranch githubBranch) {
return githubBranch.getName();
}
});
// 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;
}
}
return new GithubInfo(branches, targetRemoteName);
}
});
myForkPath = forkPath;
myTargetRemote = info.getTargetRemote();
myDiffInfos.clear();
if (canShowDiff()) {
for (final String branch : info.getBranches()) {
myDiffInfos.put(branch, new FutureTask<DiffInfo>(new Callable<DiffInfo>() {
@Override
public DiffInfo call() throws Exception {
return loadDiffInfo(myProject, myGitRepository, myCurrentBranch, myTargetRemote + "/" + branch);
}
}));
}
}
return new GithubTargetInfo(info.getBranches());
}
catch (GithubAuthenticationCanceledException e) {
return null;
}
catch (IOException e) {
GithubNotifications.showErrorDialog(myProject, CANNOT_CREATE_PULL_REQUEST, e);
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;
}
GitCompareBranchesDialog dialog =
new GitCompareBranchesDialog(myProject, info.getTo(), info.getFrom(), info.getInfo(), myGitRepository);
dialog.show();
}
}
@Nullable
public GithubFullPath showTargetDialog() {
final GithubInfo2 info = getAvailableForksInModal(myProject, myGitRepository, myAuth, myPath);
if (info == null) {
return null;
}
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
@Override
public GithubFullPath convert(ProgressIndicator o) {
return findRepositoryByUser(myProject, user, info.getForks(), myAuth, info.getSource());
}
});
}
};
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);
if (info == null) {
return true;
}
if (info.getInfo().getBranchToHeadCommits(myGitRepository).isEmpty()) {
GithubNotifications.showWarningDialog(myProject, CANNOT_CREATE_PULL_REQUEST,
"Can't create empty pull request: the branch" + getCurrentBranch() + " in fully merged to the branch " + targetBranch + ".");
return false;
}
if (info.getInfo().getHeadToBranchCommits(myGitRepository).isEmpty()) {
return GithubNotifications
.showYesNoDialog(myProject, "The branch" + targetBranch + " in not fully merged to the branch " + getCurrentBranch(),
"Do you want to proceed anyway?") == Messages.YES;
}
return true;
}
public void performAction(@NotNull final String title, @NotNull final String description, @NotNull final String targetBranch) {
@NotNull final Project project = myProject;
new Task.Backgroundable(myProject, "Creating pull request...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
LOG.info("Pushing current branch");
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());
return;
}
String headBranch = myPath.getUser() + ":" + myCurrentBranch;
LOG.info("Creating pull request");
indicator.setText("Creating pull request...");
GithubPullRequest request = createPullRequest(project, myAuth, myForkPath, title, description, headBranch, targetBranch);
if (request == null) {
return;
}
GithubNotifications
.showInfoURL(project, "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);
if (GithubUtil.addGithubRemote(project, gitRepository, forkPath.getUser(), url)) {
return forkPath.getUser();
}
else {
return null;
}
}
@Nullable
private static GithubPullRequest createPullRequest(@NotNull Project project,
@NotNull GithubAuthData auth,
@NotNull GithubFullPath targetRepo,
@NotNull String title,
@NotNull String description,
@NotNull String head,
@NotNull String base) {
try {
return GithubApiUtil.createPullRequest(auth, targetRepo.getUser(), targetRepo.getRepository(), title, description, head, base);
}
catch (IOException e) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
return null;
}
}
@Nullable
private DiffInfo getDiffInfo(@NotNull String branch) {
try {
FutureTask<DiffInfo> future = myDiffInfos.get(branch);
if (future == null) {
return null;
}
future.run();
return future.get();
}
catch (InterruptedException e) {
LOG.error(e);
return null;
}
catch (ExecutionException e) {
LOG.error(e);
return null;
}
}
@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 void getDiffDescriptionInPooledThread(@NotNull final String branch, @NotNull final Consumer<DiffDescription> after) {
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
after.consume(getDefaultDescriptionMessage(branch, getDiffInfo(branch), myGitRepository));
}
});
}
@Nullable
private static DiffInfo loadDiffInfo(@NotNull final Project project,
@NotNull final GitRepository repository,
@NotNull final String currentBranch,
@NotNull final String targetBranch) {
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, Pair.create(commits1, commits2));
return new DiffInfo(info, currentBranch, targetBranch);
}
catch (VcsException e) {
LOG.info(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);
}
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 GithubAuthData auth,
@NotNull final GithubFullPath path) {
try {
return GithubUtil
.computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo2, IOException>() {
@Override
public GithubInfo2 convert(ProgressIndicator indicator) throws IOException {
final Set<GithubFullPath> forks = new HashSet<GithubFullPath>();
// GitHub
GithubRepoDetailed repo = GithubApiUtil.getDetailedRepoInfo(auth, path.getUser(), path.getRepository());
forks.add(path);
if (repo.getParent() != null) {
forks.add(repo.getParent().getFullPath());
}
if (repo.getSource() != null) {
forks.add(repo.getSource().getFullPath());
}
// Git
forks.addAll(getAvailableForksFromGit(gitRepository));
GithubRepo forkTreeRoot = repo.getSource() == null ? repo : repo.getSource();
return new GithubInfo2(forks, forkTreeRoot);
}
});
}
catch (GithubAuthenticationCanceledException e) {
return null;
}
catch (IOException e) {
GithubNotifications.showErrorDialog(project, CANNOT_CREATE_PULL_REQUEST, 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;
}
}
}
}
return forks;
}
@Nullable
private static GithubFullPath findRepositoryByUser(@NotNull Project project,
@NotNull String user,
@NotNull Set<GithubFullPath> forks,
@NotNull GithubAuthData auth,
@NotNull GithubRepo source) {
for (GithubFullPath path : forks) {
if (StringUtil.equalsIgnoreCase(user, path.getUser())) {
return path;
}
}
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
}
try {
GithubRepo fork = GithubApiUtil.findForkByUser(auth, source.getUserName(), source.getName(), user);
if (fork != null) {
return fork.getFullPath();
}
}
catch (IOException e) {
GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
}
return null;
}
private static class GithubInfo {
@NotNull private final List<String> myBranches;
@Nullable private final String myTargetRemote;
private GithubInfo(@NotNull List<String> repo, @Nullable String targetRemote) {
myBranches = repo;
myTargetRemote = targetRemote;
}
@NotNull
public List<String> getBranches() {
return myBranches;
}
@Nullable
public String getTargetRemote() {
return myTargetRemote;
}
}
private static class GithubInfo2 {
@NotNull private final Set<GithubFullPath> myForks;
@NotNull private final GithubRepo mySource;
private GithubInfo2(@NotNull Set<GithubFullPath> forks, @NotNull GithubRepo source) {
myForks = forks;
mySource = source;
}
@NotNull
public Set<GithubFullPath> getForks() {
return myForks;
}
@NotNull
public GithubRepo getSource() {
return mySource;
}
}
public static class GithubTargetInfo {
@NotNull private final List<String> myBranches;
private GithubTargetInfo(@NotNull List<String> branches) {
myBranches = branches;
}
@NotNull
public List<String> getBranches() {
return myBranches;
}
}
private static class DiffInfo {
@NotNull private final GitCommitCompareInfo myInfo;
@NotNull private final String myFrom;
@NotNull private final String myTo;
private DiffInfo(@NotNull GitCommitCompareInfo info, @NotNull String from, @NotNull String to) {
myInfo = info;
myFrom = from; // HEAD
myTo = to; // BASE
}
@NotNull
public GitCommitCompareInfo getInfo() {
return myInfo;
}
@NotNull
public String getFrom() {
return myFrom;
}
@NotNull
public String getTo() {
return myTo;
}
}
public static class DiffDescription {
@NotNull private final String myBranch;
@Nullable private final String myTitle;
@Nullable private final String myDescription;
public DiffDescription(@NotNull String branch, @Nullable String title, @Nullable String description) {
myBranch = branch;
myTitle = title;
myDescription = description;
}
@NotNull
public String getBranch() {
return myBranch;
}
@Nullable
public String getTitle() {
return myTitle;
}
@Nullable
public String getDescription() {
return myDescription;
}
}
}
@@ -597,10 +597,10 @@ public class GithubApiUtil {
@NotNull String repo,
@NotNull String title,
@NotNull String description,
@NotNull String from,
@NotNull String onto) throws IOException {
@NotNull String head,
@NotNull String base) throws IOException {
try {
String request = gson.toJson(new GithubPullRequestRequest(title, description, from, onto));
String request = gson.toJson(new GithubPullRequestRequest(title, description, head, base));
return createDataFromRaw(fromJson(postRequest(auth, "/repos/" + user + "/" + repo + "/pulls", request), GithubPullRequestRaw.class),
GithubPullRequest.class);
}
@@ -41,7 +41,7 @@ public class GithubFullPath {
}
@NotNull
public String toString() {
public String getFullName() {
return myUserName + '/' + myRepositoryName;
}
@@ -15,43 +15,140 @@
*/
package org.jetbrains.plugins.github.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.ui.UIUtil;
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.GithubProjectSettings;
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.regex.Pattern;
import java.util.Collections;
/**
* @author Aleksey Pivovarov
*/
public class GithubCreatePullRequestDialog extends DialogWrapper {
@NotNull private final GithubCreatePullRequestPanel myGithubCreatePullRequestPanel;
@NotNull private static final Pattern GITHUB_REPO_PATTERN = Pattern.compile("[a-zA-Z0-9_.-]+:[a-zA-Z0-9_.-]+");
@NotNull private final Project myProject;
@NotNull private final GithubCreatePullRequestWorker myWorker;
@NotNull private final GithubProjectSettings myProjectSettings;
public GithubCreatePullRequestDialog(@NotNull final Project project,
@NotNull Collection<String> branches,
@Nullable String suggestedBranch,
@NotNull Consumer<String> showDiff) {
super(project, true);
myGithubCreatePullRequestPanel = new GithubCreatePullRequestPanel(showDiff);
public GithubCreatePullRequestDialog(@NotNull GithubCreatePullRequestWorker worker) {
super(worker.getProject(), true);
myWorker = worker;
myProjectSettings = GithubProjectSettings.getInstance(myWorker.getProject());
myGithubCreatePullRequestPanel = new GithubCreatePullRequestPanel();
myGithubCreatePullRequestPanel.getShowDiffButton().addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
myWorker.showDiffDialog(myGithubCreatePullRequestPanel.getBranch());
}
});
myGithubCreatePullRequestPanel.getSelectForkButton().addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showTargetDialog();
}
});
myGithubCreatePullRequestPanel.getBranchComboBox().addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
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());
}
}
}
});
}
});
}
}
}
});
setTitle("Create Pull Request - " + myWorker.getCurrentBranch());
init();
}
@Override
public void show() {
GithubFullPath defaultForkPath = myProjectSettings.getCreatePullRequestDefaultRepo();
if (defaultForkPath != null) {
setTarget(defaultForkPath);
}
else {
if (!showTargetDialog()) {
close(CANCEL_EXIT_CODE);
return;
}
}
super.show();
}
private boolean showTargetDialog() {
GithubFullPath forkPath = myWorker.showTargetDialog();
if (forkPath == null) {
return false;
}
return setTarget(forkPath);
}
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;
}
private void updateBranches(@NotNull Collection<String> branches, @NotNull GithubFullPath forkPath) {
myGithubCreatePullRequestPanel.setBranches(branches);
myProject = project;
String configBranch = GithubProjectSettings.getInstance(myProject).getCreatePullRequestDefaultBranch();
myGithubCreatePullRequestPanel.setSelectedBranch(configBranch != null ? configBranch : suggestedBranch);
String configBranch = myProjectSettings.getCreatePullRequestDefaultBranch();
if (configBranch != null) myGithubCreatePullRequestPanel.setSelectedBranch(configBranch);
setTitle("Create Pull Request");
init();
myGithubCreatePullRequestPanel.setForkName(forkPath.getFullName());
}
@Override
protected void doOKAction() {
if (myWorker.checkAction(getTargetBranch())) {
myProjectSettings.setCreatePullRequestDefaultBranch(getTargetBranch());
myWorker.performAction(getRequestTitle(), getDescription(), getTargetBranch());
super.doOKAction();
}
}
@Nullable
@@ -77,47 +174,51 @@ public class GithubCreatePullRequestDialog extends DialogWrapper {
}
@NotNull
public String getRequestTitle() {
private String getRequestTitle() {
return myGithubCreatePullRequestPanel.getTitle();
}
@NotNull
public String getDescription() {
private String getDescription() {
return myGithubCreatePullRequestPanel.getDescription();
}
@NotNull
public String getTargetBranch() {
private String getTargetBranch() {
return myGithubCreatePullRequestPanel.getBranch();
}
@Override
protected void doOKAction() {
super.doOKAction();
GithubProjectSettings.getInstance(myProject).setCreatePullRequestDefaultBranch(getTargetBranch());
}
@Nullable
@Override
protected ValidationInfo doValidate() {
if (StringUtil.isEmptyOrSpaces(getRequestTitle())) {
return new ValidationInfo("Title can't be empty'", myGithubCreatePullRequestPanel.getTitleTextField());
}
if (!GITHUB_REPO_PATTERN.matcher(getTargetBranch()).matches()) {
return new ValidationInfo("Branch must be specified like 'username:branch'", myGithubCreatePullRequestPanel.getBranchEditor());
}
return null;
}
@TestOnly
public void setRequestTitle(String title) {
public void testSetRequestTitle(String title) {
myGithubCreatePullRequestPanel.setTitle(title);
}
@TestOnly
public void setBranch(String branch) {
myGithubCreatePullRequestPanel.setSelectedBranch(branch);
public void testSetBranch(String branch) {
myGithubCreatePullRequestPanel.setBranches(Collections.singleton(branch));
}
@TestOnly
public void testCreatePullRequest() {
myWorker.performAction(getRequestTitle(), getDescription(), getTargetBranch());
}
@TestOnly
public void testSetTarget(@NotNull GithubFullPath forkPath) {
GithubCreatePullRequestWorker.GithubTargetInfo forkInfo = myWorker.setTarget(forkPath);
if (forkInfo == null) {
doCancelAction();
return;
}
myGithubCreatePullRequestPanel.setDiffEnabled(myWorker.canShowDiff());
}
}
@@ -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="4" column-count="3" 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="4" 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"/>
@@ -10,15 +10,15 @@
<children>
<component id="3f5fd" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="1" column="0" 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="Target branch:"/>
<text value="Base branch:"/>
</properties>
</component>
<component id="d2b10" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="2" column="0" 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="Title:"/>
@@ -26,7 +26,7 @@
</component>
<component id="f7387" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="3" column="0" 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="Description:"/>
@@ -34,29 +34,20 @@
</component>
<component id="996e9" class="javax.swing.JTextField" binding="myTitleTextField">
<constraints>
<grid row="1" column="1" row-span="1" col-span="2" 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="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="78a64" class="com.intellij.openapi.ui.ComboBox" binding="myBranchComboBox">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<editable value="true"/>
<toolTipText value="Target branch in format 'user:branch', where user - owner of the target repository"/>
</properties>
</component>
<scrollpane id="61e54" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="3" column="0" row-span="1" col-span="3" 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="4" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false">
<minimum-size width="150" height="50"/>
</grid>
</constraints>
<properties/>
<border type="none"/>
<border type="empty"/>
<children>
<component id="21aa4" class="javax.swing.JTextArea" binding="myDescriptionTextArea">
<constraints/>
@@ -66,12 +57,51 @@
</scrollpane>
<component id="90c93" class="javax.swing.JButton" binding="myShowDiffButton">
<constraints>
<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"/>
<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"/>
</constraints>
<properties>
<text value="Show Diff"/>
</properties>
</component>
<component id="fb73f" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" 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="Base fork:"/>
</properties>
</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"/>
</constraints>
<properties>
<text value="Select Base 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"/>
</constraints>
<properties>
<editable value="false"/>
<toolTipText value=""/>
</properties>
</component>
</children>
</grid>
</form>
@@ -17,14 +17,15 @@ package org.jetbrains.plugins.github.ui;
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.Consumer;
import com.intellij.util.ui.AsyncProcessIcon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.util.Collection;
import java.util.Comparator;
@@ -38,8 +39,13 @@ public class GithubCreatePullRequestPanel {
private SortedComboBoxModel<String> myBranchModel;
private JPanel myPanel;
private JButton myShowDiffButton;
private JButton mySelectForkButton;
private JLabel myForkLabel;
private AsyncProcessIcon myBusyIcon;
public GithubCreatePullRequestPanel(@NotNull final Consumer<String> showDiff) {
private boolean myTitleDescriptionUserModified = false;
public GithubCreatePullRequestPanel() {
myDescriptionTextArea.setBorder(BorderFactory.createEtchedBorder());
myBranchModel = new SortedComboBoxModel<String>(new Comparator<String>() {
@Override
@@ -48,12 +54,15 @@ public class GithubCreatePullRequestPanel {
}
});
myBranchComboBox.setModel(myBranchModel);
myShowDiffButton.addActionListener(new ActionListener() {
DocumentListener userModifiedDocumentListener = new DocumentAdapter() {
@Override
public void actionPerformed(ActionEvent e) {
showDiff.consume(getBranch());
protected void textChanged(DocumentEvent e) {
myTitleDescriptionUserModified = true;
}
});
};
myTitleTextField.getDocument().addDocumentListener(userModifiedDocumentListener);
myDescriptionTextArea.getDocument().addDocumentListener(userModifiedDocumentListener);
}
@NotNull
@@ -71,21 +80,24 @@ public class GithubCreatePullRequestPanel {
return myBranchComboBox.getSelectedItem().toString();
}
public void setDiffEnabled(boolean enabled) {
myShowDiffButton.setEnabled(enabled);
}
public void setSelectedBranch(@Nullable String branch) {
if (StringUtil.isEmptyOrSpaces(branch)) {
myBranchComboBox.setSelectedItem("");
return;
}
if (myBranchModel.indexOf(branch) == -1) {
myBranchModel.add(branch);
}
myBranchComboBox.setSelectedItem(branch);
}
public void setBranches(@NotNull Collection<String> branches) {
myBranchModel.clear();
myBranchModel.addAll(branches);
if (branches.size() > 0) {
myBranchComboBox.setSelectedIndex(0);
}
}
public JPanel getPanel() {
@@ -97,15 +109,61 @@ public class GithubCreatePullRequestPanel {
return myTitleTextField;
}
@NotNull
public JComponent getBranchEditor() {
return myBranchComboBox;
}
@NotNull
public JComponent getTitleTextField() {
return myTitleTextField;
}
public void setTitle(String title) {
@NotNull
public JButton getShowDiffButton() {
return myShowDiffButton;
}
@NotNull
public JButton getSelectForkButton() {
return mySelectForkButton;
}
@NotNull
public ComboBox getBranchComboBox() {
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();
}
}
private void createUIComponents() {
myBusyIcon = new AsyncProcessIcon("Loading diff...");
myBusyIcon.suspend();
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2000-2013 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.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.plugins.github.api.GithubFullPath;
import org.jetbrains.plugins.github.util.GithubNotifications;
import javax.swing.*;
import java.util.Set;
/**
* @author Aleksey Pivovarov
*/
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;
public GithubSelectForkDialog(@NotNull Project project,
@NotNull Set<GithubFullPath> forks,
@NotNull Convertor<String, GithubFullPath> 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();
}
}));
setTitle("Select Base Fork Repository");
init();
}
@Override
protected void doOKAction() {
GithubFullPath path = myCheckFork.convert(myPanel.getUser());
if (path == null) {
GithubNotifications.showErrorDialog(myProject, "Can't Find Repository", "Can't find fork for selected user");
}
else {
myFullPath = path;
super.doOKAction();
}
}
@Nullable
@Override
protected JComponent createCenterPanel() {
return myPanel.getPanel();
}
@NotNull
public GithubFullPath getPath() {
return myFullPath;
}
@TestOnly
public void testSetUser(@NotNull String user) {
myPanel.setSelectedUser(user);
}
}
@@ -0,0 +1,34 @@
<?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.GithubSelectForkPanel">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" 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"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="42560" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" 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="Fork owner:"/>
</properties>
</component>
<vspacer id="406">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="b3c77" class="com.intellij.openapi.ui.ComboBox" binding="myComboBox">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<editable value="true"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -0,0 +1,71 @@
/*
* Copyright 2000-2013 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.ui;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.SortedComboBoxModel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.Collection;
import java.util.Comparator;
/**
* @author Aleksey Pivovarov
*/
public class GithubSelectForkPanel {
private final SortedComboBoxModel<String> myModel;
private JPanel myPanel;
private ComboBox myComboBox;
public GithubSelectForkPanel() {
myModel = new SortedComboBoxModel<String>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return StringUtil.naturalCompare(o1, o2);
}
});
myComboBox.setModel(myModel);
}
public void setUsers(@NotNull Collection<String> users) {
myModel.clear();
myModel.addAll(users);
if (users.size() > 0) {
myComboBox.setSelectedIndex(0);
}
}
@NotNull
public String getUser() {
return myComboBox.getSelectedItem().toString();
}
public void setSelectedUser(@Nullable String user) {
if (StringUtil.isEmptyOrSpaces(user)) {
return;
}
myComboBox.setSelectedItem(user);
}
public JPanel getPanel() {
return myPanel;
}
}
@@ -19,6 +19,7 @@ import com.intellij.openapi.components.*;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.GithubFullPath;
/**
* @author Aleksey Pivovarov
@@ -38,8 +39,14 @@ public class GithubProjectSettings implements PersistentStateComponent<GithubPro
myState = state;
}
public static GithubProjectSettings getInstance(@NotNull Project project) {
return ServiceManager.getService(project, GithubProjectSettings.class);
}
public static class State {
@Nullable public String CREATE_PULL_REQUEST_DEFAULT_BRANCH = null;
@Nullable public String CREATE_PULL_REQUEST_DEFAULT_REPO_USER = null;
@Nullable public String CREATE_PULL_REQUEST_DEFAULT_REPO_NAME = null;
}
@Nullable
@@ -51,7 +58,16 @@ public class GithubProjectSettings implements PersistentStateComponent<GithubPro
myState.CREATE_PULL_REQUEST_DEFAULT_BRANCH = branch;
}
public static GithubProjectSettings getInstance(@NotNull Project project) {
return ServiceManager.getService(project, GithubProjectSettings.class);
@Nullable
public GithubFullPath getCreatePullRequestDefaultRepo() {
if (myState.CREATE_PULL_REQUEST_DEFAULT_REPO_USER == null || myState.CREATE_PULL_REQUEST_DEFAULT_REPO_NAME == null) {
return null;
}
return new GithubFullPath(myState.CREATE_PULL_REQUEST_DEFAULT_REPO_USER, myState.CREATE_PULL_REQUEST_DEFAULT_REPO_NAME);
}
public void setCreatePullRequestDefaultRepo(@NotNull GithubFullPath repo) {
myState.CREATE_PULL_REQUEST_DEFAULT_REPO_USER = repo.getUser();
myState.CREATE_PULL_REQUEST_DEFAULT_REPO_NAME = repo.getRepository();
}
}
@@ -22,7 +22,7 @@ import com.intellij.notification.NotificationType;
*/
public class GithubCreatePullRequestTest extends GithubCreatePullRequestTestBase {
public void testSimple() throws Exception {
registerDefaultCreatePullRequestDialogHandler(myLogin1 + ":master");
registerDefaultCreatePullRequestDialogHandler("master", myLogin1);
GithubCreatePullRequestAction.createPullRequest(myProject, myProjectRoot);
@@ -32,17 +32,8 @@ public class GithubCreatePullRequestTest extends GithubCreatePullRequestTestBase
}
public void testParent() throws Exception {
registerDefaultCreatePullRequestDialogHandler(myLogin2 + ":file2");
GithubCreatePullRequestAction.createPullRequest(myProject, myProjectRoot);
checkNotification(NotificationType.INFORMATION, "Successfully created pull request", null);
checkRemoteConfigured();
checkLastCommitPushed();
}
public void testCommitRef1() throws Exception {
registerDefaultCreatePullRequestDialogHandler(myLogin1 + ":refs/heads/master");
registerDefaultCreatePullRequestDialogHandler("file2", myLogin2);
addRemote(myLogin2);
GithubCreatePullRequestAction.createPullRequest(myProject, myProjectRoot);
@@ -24,8 +24,11 @@ import git4idea.actions.GitInit;
import git4idea.commands.Git;
import git4idea.repo.GitRepository;
import git4idea.test.TestDialogHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.github.api.GithubFullPath;
import org.jetbrains.plugins.github.test.GithubTest;
import org.jetbrains.plugins.github.ui.GithubCreatePullRequestDialog;
import org.jetbrains.plugins.github.util.GithubUrlUtil;
import org.jetbrains.plugins.github.util.GithubUtil;
import java.util.Random;
@@ -38,6 +41,7 @@ import static git4idea.test.GitExecutor.git;
*/
public abstract class GithubCreatePullRequestTestBase extends GithubTest {
protected static final String PROJECT_URL = "https://github.com/ideatest1/PullRequestTest";
protected static final String PROJECT_NAME = "PullRequestTest";
protected String BRANCH_NAME;
@Override
@@ -67,12 +71,14 @@ public abstract class GithubCreatePullRequestTestBase extends GithubTest {
}
}
protected void registerDefaultCreatePullRequestDialogHandler(final String branch) {
protected void registerDefaultCreatePullRequestDialogHandler(@NotNull final String branch, @NotNull final String user) {
myDialogManager.registerDialogHandler(GithubCreatePullRequestDialog.class, new TestDialogHandler<GithubCreatePullRequestDialog>() {
@Override
public int handleDialog(GithubCreatePullRequestDialog dialog) {
dialog.setRequestTitle(BRANCH_NAME);
dialog.setBranch(branch);
dialog.testSetRequestTitle(BRANCH_NAME);
dialog.testSetBranch(branch);
dialog.testSetTarget(new GithubFullPath(user, PROJECT_NAME));
dialog.testCreatePullRequest();
return DialogWrapper.OK_EXIT_CODE;
}
});
@@ -84,6 +90,10 @@ public abstract class GithubCreatePullRequestTestBase extends GithubTest {
GitInit.refreshAndConfigureVcsMappings(myProject, myProjectRoot, myProjectRoot.getPath());
}
protected void addRemote(@NotNull String user) {
git("remote add somename " + GithubUrlUtil.getCloneUrl(new GithubFullPath(user, PROJECT_NAME)));
}
protected void createBranch() {
git("branch " + BRANCH_NAME);
git("checkout " + BRANCH_NAME);