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();
                }
              });
    }
  }
  public boolean checkAction(@Nullable final BranchInfo branch) {
    if (branch == null) {
      GithubNotifications.showWarningDialog(
          myProject, CANNOT_CREATE_PULL_REQUEST, "Target branch is not selected");
      return false;
    }

    DiffInfo info;
    try {
      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 (IOException e) {
      GithubNotifications.showError(myProject, "Can't collect diff data", e);
      return true;
    }
    if (info == null) {
      return true;
    }

    ForkInfo fork = branch.getForkInfo();

    String localBranchName = "'" + myCurrentBranch + "'";
    String targetBranchName = "'" + fork.getRemoteName() + "/" + branch.getRemoteName() + "'";
    if (info.getInfo().getBranchToHeadCommits(myGitRepository).isEmpty()) {
      return 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);
    }

    return true;
  }
  @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;
    }
  }
  @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);
  }
  @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);
            }
          }
        });
  }
  @Nullable
  private GithubPullRequest doCreatePullRequest(
      @NotNull ProgressIndicator indicator,
      @NotNull final BranchInfo branch,
      @NotNull final String title,
      @NotNull final String description) {
    final ForkInfo fork = branch.getForkInfo();

    final String head = myPath.getUser() + ":" + myCurrentBranch;
    final String base = branch.getRemoteName();

    try {
      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(myProject, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }
  }