com.microsoft.alm.plugin.idea.ui.pullrequest.CreatePullRequestModel.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.alm.plugin.idea.ui.pullrequest.CreatePullRequestModel.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.plugin.idea.ui.pullrequest;

import com.google.common.base.Predicate;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Collections2;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.ui.SortedComboBoxModel;
import com.microsoft.alm.plugin.authentication.AuthHelper;
import com.microsoft.alm.plugin.context.ServerContext;
import com.microsoft.alm.plugin.context.ServerContextManager;
import com.microsoft.alm.plugin.idea.resources.TfPluginBundle;
import com.microsoft.alm.plugin.idea.ui.common.AbstractModel;
import com.microsoft.alm.plugin.idea.ui.common.ModelValidationInfo;
import com.microsoft.alm.plugin.idea.utils.GeneralGitHelper;
import com.microsoft.alm.plugin.idea.utils.TfGitHelper;
import com.microsoft.alm.sourcecontrol.webapi.GitHttpClient;
import com.microsoft.alm.sourcecontrol.webapi.model.GitPullRequest;
import git4idea.GitBranch;
import git4idea.GitCommit;
import git4idea.GitExecutionException;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepoInfo;
import git4idea.repo.GitRepository;
import git4idea.util.GitCommitCompareInfo;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.ComboBoxModel;
import javax.swing.event.HyperlinkEvent;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

import static com.microsoft.alm.plugin.idea.ui.pullrequest.PullRequestHelper.PRCreateStatus;

public class CreatePullRequestModel extends AbstractModel {

    private static final Logger logger = LoggerFactory.getLogger(CreatePullRequestModel.class);

    /* size limit */
    public static final int MAX_SIZE_TITLE = 400;
    public static final int MAX_SIZE_DESCRIPTION = 4000;

    /**
     * Property names
     */
    public static final String PROP_TARGET_BRANCH = "targetBranch";
    public static final String PROP_TARGET_BRANCH_COMBO_MODEL = "targetBranchComboModel";
    public static final String PROP_SOURCE_BRANCH = "sourceBranch";
    public static final String PROP_TITLE = "title";
    public static final String PROP_DESCRIPTION = "description";
    public static final String PROP_LOADING = "loading";
    public static final String PROP_DIFF_MODEL = "diffModel";

    private Project project;
    private GitRepository gitRepository;
    private GitRemoteBranch targetBranch;
    private String title;
    private String description;
    private final Collection<GitRemote> tfGitRemotes;
    private final ComboBoxModel remoteBranchComboModel;
    private final PullRequestHelper pullRequestHelper;

    /* Branch diff provider, non-final for unit test */
    private DiffCompareInfoProvider diffCompareInfoProvider;

    /* UI properties */
    private boolean loading = false;
    private GitChangesContainer localBranchChanges;

    private final LoadingCache<Pair<String, String>, GitCommitCompareInfo> diffCache;

    /* Executor service for running diff calculating Futures */
    private final ListeningExecutorService executorService;

    private ApplicationProvider applicationProvider;

    public CreatePullRequestModel(@NotNull final Project project, @NotNull final GitRepository gitRepository) {
        this.project = project;
        this.gitRepository = gitRepository;

        this.tfGitRemotes = TfGitHelper.getTfGitRemotes(gitRepository);

        this.remoteBranchComboModel = createRemoteBranchDropdownModel();
        this.targetBranch = (GitRemoteBranch) this.remoteBranchComboModel.getSelectedItem();

        this.applicationProvider = new ApplicationProvider();
        this.pullRequestHelper = new PullRequestHelper();

        this.diffCompareInfoProvider = new DiffCompareInfoProvider();
        this.diffCache = CacheBuilder.newBuilder().maximumSize(20)
                .build(new CacheLoader<Pair<String, String>, GitCommitCompareInfo>() {
                    @Override
                    public GitCommitCompareInfo load(Pair<String, String> key) throws Exception {
                        // if we missed the cache, then show the loading spinner, otherwise
                        // just switch to the diff we have to avoid flickering the screen
                        applicationProvider.invokeAndWaitWithAnyModality(new Runnable() {
                            @Override
                            public void run() {
                                // set the view to show loading
                                setLoading(true);
                            }
                        });

                        return getDiffCompareInfoProvider().getBranchCompareInfo(project, gitRepository,
                                key.getFirst(), key.getSecond());
                    }
                });

        this.executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
    }

    public Project getProject() {
        return project;
    }

    public void setProject(final Project project) {
        this.project = project;
    }

    public synchronized String getTitle() {
        return StringUtils.isNotBlank(title) ? title : StringUtils.EMPTY;
    }

    public void setTitle(final String title) {
        synchronized (this) {
            this.title = StringUtils.trim(title);
        }
        setChangedAndNotify(PROP_TITLE);
    }

    public synchronized String getDescription() {
        return StringUtils.isNotBlank(description) ? description : StringUtils.EMPTY;
    }

    public void setDescription(final String description) {
        synchronized (this) {
            this.description = StringUtils.trim(description);
        }
        setChangedAndNotify(PROP_DESCRIPTION);
    }

    public DiffCompareInfoProvider getDiffCompareInfoProvider() {
        return diffCompareInfoProvider;
    }

    public void setDiffCompareInfoProvider(final DiffCompareInfoProvider diffCompareInfoProvider) {
        this.diffCompareInfoProvider = diffCompareInfoProvider;
    }

    public boolean isLoading() {
        return loading;
    }

    public void setLoading(final boolean loading) {
        if (this.loading != loading) {
            this.loading = loading;
            setChangedAndNotify(PROP_LOADING);
        }
    }

    public GitChangesContainer getLocalBranchChanges() {
        return localBranchChanges;
    }

    public void setLocalBranchChanges(final GitChangesContainer localBranchChanges) {
        if (this.localBranchChanges != localBranchChanges) {
            this.localBranchChanges = localBranchChanges;
            setChangedAndNotify(PROP_DIFF_MODEL);
        }
    }

    /**
     * Get current branch
     *
     * @return current branch
     */
    @Nullable
    public GitLocalBranch getSourceBranch() {
        return getInfo() != null ? getInfo().getCurrentBranch() : null;
    }

    /**
     * Get target branch
     *
     * @return the target branch
     */
    public synchronized GitRemoteBranch getTargetBranch() {
        return this.targetBranch;
    }

    /**
     * Set the target branch
     *
     * @param targetBranch
     */
    public synchronized void setTargetBranch(final GitRemoteBranch targetBranch) {
        if (this.targetBranch != targetBranch) {
            this.targetBranch = targetBranch;
            setChangedAndNotify(PROP_TARGET_BRANCH);
        }
    }

    /**
     * Get all remote branches from all alm remotes
     *
     * @return ComboBoxModel with all alm remote branches
     */
    @NotNull
    public ComboBoxModel getRemoteBranchDropdownModel() {
        return this.remoteBranchComboModel;
    }

    private ComboBoxModel createRemoteBranchDropdownModel() {
        final SortedComboBoxModel<GitRemoteBranch> sortedRemoteBranches = new SortedComboBoxModel<GitRemoteBranch>(
                new TfGitHelper.BranchComparator());
        final GitRemoteBranch remoteTrackingBranch = this.getRemoteTrackingBranch();

        // only show valid remote branches
        sortedRemoteBranches
                .addAll(Collections2.filter(getInfo().getRemoteBranches(), new Predicate<GitRemoteBranch>() {
                    @Override
                    public boolean apply(final GitRemoteBranch remoteBranch) {
                        /* two conditions:
                         *   1. remote must be a vso/tfs remote
                         *   2. this isn't the remote tracking branch of current local branch
                         */
                        return tfGitRemotes.contains(remoteBranch.getRemote())
                                && !remoteBranch.equals(remoteTrackingBranch);
                    }
                }));
        sortedRemoteBranches
                .setSelectedItem(TfGitHelper.getDefaultBranch(sortedRemoteBranches.getItems(), tfGitRemotes));

        return sortedRemoteBranches;
    }

    @Nullable
    private GitRemoteBranch getRemoteTrackingBranch() {
        final GitLocalBranch localBranch = this.getSourceBranch();

        return localBranch != null && this.gitRepository != null ? localBranch.findTrackedBranch(this.gitRepository)
                : null;
    }

    /**
     * This method calculates the commits and diff information against the tip of current branch and
     * the common ancestor of source branch (current branch) and target branch (selected remote branch).
     * <p/>
     * If there is no common parent (two branches are parallel), return an empty GitCommitCompareInfo
     * <p/>
     * This is potentially an expensive calculation, probably should do it on a background thread.
     * We will also attempt to cache the result
     * <p/>
     * default access for testing so we bypass UI code,
     * TODO: reevaluate the testing to properly shutoff the access level
     *
     * @return gitChangesContainer on what has changed on source branch
     */
    GitChangesContainer getMyChangesCompareInfo() throws VcsException {

        final GitBranch currBranch = this.getSourceBranch();

        final GitRemoteBranch selectedRemoteBranch = this.getTargetBranch();

        // if source branch or currentBranch isn't set, just return empty diff
        if (selectedRemoteBranch == null || currBranch == null) {
            return GitChangesContainer.createChangesContainer(null, null, null, null,
                    getDiffCompareInfoProvider().getEmptyDiff(this.gitRepository), this.gitRepository);
        }

        // get hash of last commit for each branch
        final String remoteBranchHash = GeneralGitHelper.getLastCommitHash(project, gitRepository,
                selectedRemoteBranch);
        final String currBranchHash = GeneralGitHelper.getLastCommitHash(project, gitRepository, currBranch);

        try {
            GitCommitCompareInfo changes = this.diffCache
                    .get(new Pair<String, String>(currBranchHash, remoteBranchHash));

            return GitChangesContainer.createChangesContainer(currBranch.getName(), selectedRemoteBranch.getName(),
                    currBranchHash, remoteBranchHash, changes, this.gitRepository);
        } catch (ExecutionException e) {
            throw new VcsException(e.getCause());
        }
    }

    /**
     * This method spawns a background thread to calculate the diff
     * <p/>
     * TODO: refactor the onSuccess/onFailure callback so we can test this method
     */
    public void loadDiff() {
        if (this.getSourceBranch() != null && this.getTargetBranch() != null) {
            ListenableFuture<GitChangesContainer> diffFuture = this.executorService
                    .submit(new Callable<GitChangesContainer>() {
                        @Override
                        public GitChangesContainer call() throws Exception {
                            // calculate the diffs
                            return getMyChangesCompareInfo();
                        }
                    });

            Futures.addCallback(diffFuture, new FutureCallback<GitChangesContainer>() {
                public void onSuccess(final GitChangesContainer changesContainer) {
                    applicationProvider.invokeAndWaitWithAnyModality(new Runnable() {
                        @Override
                        public void run() {
                            // try to update the view to show diff, but only if the calculated diff
                            // is still upto date -- make sure user didn't select another branch
                            // while we were busy calculating the diffs
                            if (changesContainer != null && isChangesUpToDate(changesContainer)) {
                                setLoading(false);

                                final GitCommitCompareInfo compareInfo = changesContainer.getGitCommitCompareInfo();
                                if (compareInfo != null) {
                                    List<GitCommit> commits = compareInfo
                                            .getBranchToHeadCommits(changesContainer.getGitRepository());

                                    final GitLocalBranch sourceBranch = getSourceBranch();
                                    final GitRemoteBranch targetBranch = getTargetBranch();

                                    if (commits != null && sourceBranch.getName() != null
                                            && targetBranch.getNameForRemoteOperations() != null) {
                                        final String defaultTitle = pullRequestHelper.createDefaultTitle(commits,
                                                sourceBranch.getName(), targetBranch.getNameForRemoteOperations());
                                        setTitle(defaultTitle);

                                        final String defaultDescription = pullRequestHelper
                                                .createDefaultDescription(commits);
                                        setDescription(defaultDescription);
                                    }
                                }

                                setLocalBranchChanges(changesContainer);
                            }
                        }
                    });
                }

                public void onFailure(final Throwable thrown) {
                    logger.warn("onFailure in loadDiff", thrown);

                    applicationProvider.invokeAndWaitWithAnyModality(new Runnable() {
                        public void run() {
                            final GitLocalBranch sourceBranch = getSourceBranch();
                            final GitRemoteBranch targetBranch = getTargetBranch();
                            final String sourceBranchName = sourceBranch != null ? sourceBranch.getName() : "";
                            final String targetBranchName = targetBranch != null ? targetBranch.getName() : "";
                            notifyDiffFailedError(getProject(),
                                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_DIFF_FAILED_MSG,
                                            sourceBranchName, targetBranchName));

                            final GitChangesContainer changesContainer = GitChangesContainer.createChangesContainer(
                                    sourceBranchName, targetBranchName, null, null,
                                    getDiffCompareInfoProvider().getEmptyDiff(gitRepository), gitRepository);
                            if (isChangesUpToDate(changesContainer)) {
                                setLoading(false);
                                setLocalBranchChanges(changesContainer);
                            }

                        }
                    });
                }
            });
        }
    }

    /**
     * Create pull request on a background thread
     * <p/>
     * This method will first check to see if the local branch has a tracking branch:
     * yes:
     * push the commits to the remote tracking branch
     * no:
     * try create a remote branch matching the local branch name exactly, with the remote set to the GitRemote of
     * the target branch
     * <p/>
     * If push fails for whatever reason, stop and show an error message
     * <p/>
     * After we push the local branch, then create the pull request.  Pull request link should be returned
     * in a notification bubble
     */
    public void createPullRequest() {
        /* verifying branch selections */
        final GitLocalBranch sourceBranch = this.getSourceBranch();
        final GitRemoteBranch targetBranch = this.getTargetBranch();

        if (sourceBranch == null) {
            // how did we get here? validation failed?
            notifyCreateFailedError(project,
                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_SOURCE_EMPTY));
            return;
        }

        if (targetBranch == null) {
            // how did we get here? validation failed?
            notifyCreateFailedError(project,
                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_TARGET_NOT_SELECTED));
            return;
        }

        if (targetBranch.equals(this.getRemoteTrackingBranch())) {
            // how did we get here? Didn't we filter you out?
            notifyCreateFailedError(project,
                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_TARGET_IS_LOCAL_TRACKING));
            return;
        }

        //TODO Determine the correct/best way to get the remote url
        final String gitRemoteUrl = TfGitHelper.getTfGitRemote(gitRepository).getFirstUrl();
        final CreatePullRequestModel createModel = this;
        /* Let's keep all server interactions to a background thread */
        final Task.Backgroundable createPullRequestTask = new Task.Backgroundable(project,
                TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_DIALOG_TITLE), true,
                PerformInBackgroundOption.DEAF) {
            @Override
            public void run(@NotNull ProgressIndicator progressIndicator) {
                ListenableFuture<Pair<String, GitCommandResult>> pushResult = doPushCommits(gitRepository,
                        sourceBranch, targetBranch.getRemote(), progressIndicator);

                Futures.addCallback(pushResult, new FutureCallback<Pair<String, GitCommandResult>>() {
                    @Override
                    public void onSuccess(@Nullable Pair<String, GitCommandResult> result) {
                        if (result != null && StringUtils.isNotEmpty(result.getFirst())) {
                            final String title = createModel.getTitle();
                            final String description = createModel.getDescription();
                            final String branchNameOnRemoteServer = result.getFirst();

                            // get context from manager, we want to do this after push completes since credentials could have changed during the Git push
                            final ServerContext context = ServerContextManager.getInstance()
                                    .getAuthenticatedContext(gitRemoteUrl, true);

                            if (context == null) {
                                notifyCreateFailedError(project, TfPluginBundle
                                        .message(TfPluginBundle.KEY_ERRORS_AUTH_NOT_SUCCESSFUL, gitRemoteUrl));
                                return;
                            }

                            doCreatePullRequest(project, context, title, description, branchNameOnRemoteServer,
                                    targetBranch);
                        } else {
                            // I really don't have anything else to say, push failed, the title says it all
                            // I have no error message to be more specific
                            notifyPushFailedError(createModel.getProject(), StringUtils.EMPTY);
                        }
                    }

                    @Override
                    public void onFailure(Throwable t) {
                        notifyPushFailedError(createModel.getProject(), t.getLocalizedMessage());
                    }
                });
            }
        };

        createPullRequestTask.queue();
    }

    private ListenableFuture<Pair<String, GitCommandResult>> doPushCommits(
            @NotNull final GitRepository gitRepository, @NotNull final GitLocalBranch localBranch,
            @NotNull final GitRemote gitRemote, @NotNull final ProgressIndicator indicator) {
        // just set the result without going off to another thread, we should already be in a background task
        SettableFuture<Pair<String, GitCommandResult>> pushResult = SettableFuture
                .<Pair<String, GitCommandResult>>create();

        indicator.setText(TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_PUSH_TITLE));
        final Git git = ServiceManager.getService(Git.class);

        final GitRemoteBranch trackingBranch = localBranch.findTrackedBranch(gitRepository);

        final String createdBranchNameOnServer;
        final StringBuilder pushSpec = new StringBuilder(localBranch.getName());
        if (trackingBranch != null && trackingBranch.getRemote().equals(gitRemote)) {
            // if the tracking branch is on the same remote, we should update that
            pushSpec.append(":").append(trackingBranch.getNameForRemoteOperations());
            createdBranchNameOnServer = trackingBranch.getNameForRemoteOperations();
        } else {
            createdBranchNameOnServer = localBranch.getName();
        }

        final String fetchUrl = getFetchUrl(gitRemote);
        final String pushSpecStr = pushSpec.toString();
        final String gitRemoteName = gitRemote.getName();
        logger.debug("Pushing {} to {}: {}", pushSpecStr, gitRemoteName, fetchUrl);
        final GitCommandResult result = git.push(gitRepository, gitRemoteName, fetchUrl, pushSpecStr, true);

        if (result.success()) {
            pushResult.set(Pair.create(createdBranchNameOnServer, result));
        } else {
            final String errMsg = result.getErrorOutputAsJoinedString();
            pushResult.setException(new GitExecutionException(errMsg, null));
        }

        return pushResult;
    }

    private void doCreatePullRequest(@NotNull final Project project, @NotNull final ServerContext context,
            @NotNull final String title, @NotNull final String description,
            @NotNull final String branchNameOnRemoteServer, @NotNull final GitRemoteBranch targetBranch) {
        final GitHttpClient gitClient = context.getGitHttpClient();

        try {
            final UUID repositoryId = context.getGitRepository().getId();
            final UUID projectId = context.getTeamProjectReference().getId();

            final GitPullRequest pullRequestToBeCreated = pullRequestHelper.generateGitPullRequest(title,
                    description, branchNameOnRemoteServer, targetBranch);

            final GitPullRequest gitPullRequest = gitClient.createPullRequest(pullRequestToBeCreated, projectId,
                    repositoryId);

            final String repositoryRemoteUrl = context.getGitRepository().getRemoteUrl();
            notifySuccess(project, TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_CREATED_TITLE),
                    pullRequestHelper.getHtmlMsg(repositoryRemoteUrl, gitPullRequest.getPullRequestId()));

        } catch (Throwable t) {
            if (AuthHelper.isNotAuthorizedError(t)) {
                final ServerContext newContext = ServerContextManager.getInstance()
                        .updateAuthenticationInfo(context.getGitRepository().getRemoteUrl());
                if (newContext != null) {
                    //retry creating the pull request with new context and authentication info
                    doCreatePullRequest(project, newContext, title, description, branchNameOnRemoteServer,
                            targetBranch);
                } else {
                    //user cancelled login, don't retry
                }
            } else {
                // catch everything so we don't bubble up to Intellij
                final Pair<PRCreateStatus, String> parsed = pullRequestHelper.parseException(t,
                        branchNameOnRemoteServer, targetBranch, context, gitClient);

                if (parsed.getFirst() == PRCreateStatus.DUPLICATE) {
                    notifySuccess(project,
                            TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ALREADY_EXISTS_TITLE),
                            parsed.getSecond());
                } else {
                    notifyCreateFailedError(project, parsed.getSecond());
                    logger.warn("Create pull request failed", t);
                }
            }
        }
    }

    private String getFetchUrl(@NotNull final GitRemote gitRemote) {
        return gitRemote.getFirstUrl();
    }

    /* if user has changed the dropdown while we calculate the diff, this diff is out of date */
    private boolean isChangesUpToDate(final GitChangesContainer changesContainer) {

        // target branches must match
        final GitRemoteBranch targetBranch = this.getTargetBranch();
        if (changesContainer.getTargetBranchName() != null && targetBranch != null) {
            if (!changesContainer.getTargetBranchName().equals(targetBranch.getName())) {
                return false;
            }
        }

        // source branches must match
        final GitLocalBranch sourceBranch = this.getSourceBranch();
        if (changesContainer.getSourceBranchName() != null && sourceBranch != null) {
            if (!changesContainer.getSourceBranchName().equals(sourceBranch.getName())) {
                return false;
            }
        }

        return true;
    }

    public ModelValidationInfo validate() {
        if (StringUtils.isEmpty(this.getTitle())) {
            return ModelValidationInfo.createWithResource(PROP_TITLE,
                    TfPluginBundle.KEY_CREATE_PR_ERRORS_TITLE_EMPTY);
        }

        if (this.getTitle().length() > MAX_SIZE_TITLE) {
            return ModelValidationInfo.createWithResource(PROP_TITLE,
                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_TITLE_TOO_LONG, MAX_SIZE_TITLE));
        }

        if (StringUtils.isEmpty(this.getDescription())) {
            return ModelValidationInfo.createWithResource(PROP_DESCRIPTION,
                    TfPluginBundle.KEY_CREATE_PR_ERRORS_DESCRIPTION_EMPTY);
        }

        if (this.getDescription().length() > MAX_SIZE_DESCRIPTION) {
            return ModelValidationInfo.createWithResource(PROP_DESCRIPTION, TfPluginBundle
                    .message(TfPluginBundle.KEY_CREATE_PR_ERRORS_DESCRIPTION_TOO_LONG, MAX_SIZE_DESCRIPTION));
        }

        if (this.getSourceBranch() == null) {
            return ModelValidationInfo.createWithResource(PROP_SOURCE_BRANCH,
                    TfPluginBundle.KEY_CREATE_PR_ERRORS_SOURCE_EMPTY);
        }

        if (this.getTargetBranch() == null) {
            return ModelValidationInfo.createWithResource(PROP_TARGET_BRANCH,
                    TfPluginBundle.KEY_CREATE_PR_ERRORS_TARGET_NOT_SELECTED);
        }

        return ModelValidationInfo.NO_ERRORS;
    }

    private GitRepoInfo getInfo() {
        return this.gitRepository.getInfo();
    }

    private void notifyDiffFailedError(final Project project, final String message) {
        notifyError(project, TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_DIFF_FAILED_TITLE),
                message);
    }

    private void notifyPushFailedError(final Project project, final String message) {
        notifyError(project, TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_PUSH_FAILED_TITLE),
                message);
    }

    private void notifyCreateFailedError(final Project project, final String message) {
        notifyError(project, TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_ERRORS_CREATE_FAILED_TITLE),
                message);
    }

    private void notifyError(final Project project, final String title, final String message) {
        if (message != null) {
            VcsNotifier.getInstance(project).notifyError(title, message);
        } else {
            VcsNotifier.getInstance(project).notifyError(title, "");
        }
    }

    private void notifySuccess(final Project project, final String title, final String message) {
        VcsNotifier.getInstance(project).notifyImportantInfo(title, message, new NotificationListener() {
            @Override
            public void hyperlinkUpdate(@NotNull final Notification n, @NotNull final HyperlinkEvent e) {
                BrowserUtil.browse(e.getURL());
            }
        });
    }

    static class ApplicationProvider {
        public Application getApplication() {
            return ApplicationManager.getApplication();
        }

        /* must mock the ModalityState.any() call as it also goes out to ApplicationManager */
        public void invokeAndWaitWithAnyModality(final Runnable r) {
            getApplication().invokeAndWait(r, ModalityState.any());
        }
    }

    /* default */
    void setApplicationProvider(final ApplicationProvider applicationProvider) {
        this.applicationProvider = applicationProvider;
    }
}