com.microsoft.alm.plugin.idea.git.ui.branch.CreateBranchModel.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.alm.plugin.idea.git.ui.branch.CreateBranchModel.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.git.ui.branch;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.ui.SortedComboBoxModel;
import com.microsoft.alm.common.utils.UrlHelper;
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.common.resources.TfPluginBundle;
import com.microsoft.alm.plugin.idea.common.ui.common.AbstractModel;
import com.microsoft.alm.plugin.idea.common.ui.common.ModelValidationInfo;
import com.microsoft.alm.plugin.idea.git.utils.GeneralGitHelper;
import com.microsoft.alm.plugin.idea.common.utils.IdeaHelper;
import com.microsoft.alm.plugin.idea.git.utils.TfGitHelper;
import com.microsoft.alm.plugin.telemetry.TfsTelemetryHelper;
import com.microsoft.alm.sourcecontrol.webapi.model.GitRefUpdate;
import com.microsoft.alm.sourcecontrol.webapi.model.GitRefUpdateResult;
import git4idea.GitRemoteBranch;
import git4idea.branch.GitBrancher;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.update.GitFetchResult;
import git4idea.update.GitFetcher;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.ComboBoxModel;
import javax.swing.event.HyperlinkEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Model for creating a new branch from an existing remote branch
 */
public class CreateBranchModel extends AbstractModel {
    private static final Logger logger = LoggerFactory.getLogger(CreateBranchModel.class);

    private static final String REFS_PREFIX = "refs/heads/";
    private static final String ORIGIN_PREFIX = "origin/";
    private static final String BASE_HASH = "0000000000000000000000000000000000000000";

    public static final String PROP_BRANCH_NAME = "branchName";
    public static final String PROP_SELECTED_REMOTE_BRANCH = "selectedRemoteBranch";
    public static final String PROP_REMOTE_BRANCH_COMBO_MODEL = "remoteBranchComboModel";
    public static final String PROP_CHECKOUT_BRANCH = "checkoutBranch";
    public static final String CREATE_BRANCH_ACTION = "create-branch";

    private final Project project;
    private final GitRepository gitRepository;
    private final Collection<GitRemote> tfGitRemotes;
    private String branchName;
    private GitRemoteBranch selectedRemoteBranch;
    private SortedComboBoxModel<GitRemoteBranch> remoteBranchComboModel;
    private boolean checkoutBranch = true;
    private boolean branchWasCreated = false;

    protected CreateBranchModel(final Project project, final String defaultBranchName,
            final GitRepository gitRepository) {
        super();
        this.project = project;
        this.branchName = defaultBranchName;
        this.gitRepository = gitRepository;
        this.tfGitRemotes = TfGitHelper.getTfGitRemotes(gitRepository);
        this.remoteBranchComboModel = createRemoteBranchDropdownModel();
        this.selectedRemoteBranch = this.remoteBranchComboModel.getSelectedItem();
    }

    private SortedComboBoxModel<GitRemoteBranch> createRemoteBranchDropdownModel() {
        logger.info("CreateBranchModel.createRemoteBranchDropdownModel");
        final SortedComboBoxModel<GitRemoteBranch> sortedRemoteBranches = new SortedComboBoxModel<GitRemoteBranch>(
                new TfGitHelper.BranchComparator());

        // TODO: add option to retrieve more branches in case the branch they are looking for is missing local
        // only show valid remote branches
        sortedRemoteBranches.addAll(
                Collections2.filter(gitRepository.getInfo().getRemoteBranches(), new Predicate<GitRemoteBranch>() {
                    @Override
                    public boolean apply(final GitRemoteBranch remoteBranch) {
                        //  condition: remote must be a vso/tfs remote
                        return tfGitRemotes.contains(remoteBranch.getRemote());
                    }
                }));
        sortedRemoteBranches
                .setSelectedItem(TfGitHelper.getDefaultBranch(sortedRemoteBranches.getItems(), tfGitRemotes));
        return sortedRemoteBranches;
    }

    public ComboBoxModel getRemoteBranchDropdownModel() {
        return remoteBranchComboModel;
    }

    public GitRemoteBranch getSelectedRemoteBranch() {
        return selectedRemoteBranch;
    }

    public void setSelectedRemoteBranch(final GitRemoteBranch remoteBranch) {
        this.selectedRemoteBranch = remoteBranch;
        setChangedAndNotify(PROP_SELECTED_REMOTE_BRANCH);
    }

    public String getBranchName() {
        return branchName;
    }

    public void setBranchName(final String branchName) {
        if (!StringUtils.equals(this.branchName, branchName)) {
            this.branchName = branchName;
            setChangedAndNotify(PROP_BRANCH_NAME);
        }
    }

    public boolean getCheckoutBranch() {
        return checkoutBranch;
    }

    public void setCheckoutBranch(boolean checkoutBranch) {
        if (this.checkoutBranch != checkoutBranch) {
            this.checkoutBranch = checkoutBranch;
            setChangedAndNotify(PROP_CHECKOUT_BRANCH);
        }
    }

    private void setBranchWasCreated(final boolean branchWasCreated) {
        this.branchWasCreated = branchWasCreated;
    }

    public boolean getBranchWasCreated() {
        return branchWasCreated;
    }

    public ModelValidationInfo validate() {
        final String branchName = getBranchName();
        if (branchName == null || branchName.isEmpty()) {
            return ModelValidationInfo.createWithResource(PROP_BRANCH_NAME,
                    TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_ERRORS_BRANCH_NAME_EMPTY);
        }

        for (GitRemoteBranch ref : remoteBranchComboModel.getItems()) {
            if (StringUtils.equals(ref.getName().replace(ORIGIN_PREFIX, StringUtils.EMPTY), getBranchName())) {
                return ModelValidationInfo.createWithResource(PROP_BRANCH_NAME,
                        TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_ERRORS_BRANCH_NAME_EXISTS);
            }
        }

        return ModelValidationInfo.NO_ERRORS;
    }

    /**
     * Creates a new branch on the server from an existing branch on the server
     * TODO: remove method if not needed for other create branch flows
     */
    public void createBranch() {
        logger.info("CreateBranchModel.createBranch");
        final ModelValidationInfo validationInfo = validate();
        if (validationInfo == null) {
            final String gitRemoteUrl = TfGitHelper.getTfGitRemote(gitRepository).getFirstUrl();
            final Task.Backgroundable createBranchTask = new Task.Backgroundable(project,
                    TfPluginBundle.message(TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_TITLE), true,
                    PerformInBackgroundOption.DEAF) {

                @Override
                public void run(@NotNull final ProgressIndicator progressIndicator) {
                    progressIndicator
                            .setText(TfPluginBundle.message(TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_TITLE));
                    // get context from manager, and store in active context
                    final ServerContext context = ServerContextManager.getInstance().getUpdatedContext(gitRemoteUrl,
                            true);

                    if (context == null) {
                        VcsNotifier.getInstance(project)
                                .notifyError(TfPluginBundle.message(
                                        TfPluginBundle.KEY_CREATE_BRANCH_ERRORS_AUTHENTICATION_FAILED_TITLE),
                                        TfPluginBundle.message(TfPluginBundle.KEY_ERRORS_AUTH_NOT_SUCCESSFUL,
                                                gitRemoteUrl));
                        return;
                    }
                    doBranchCreate(context, progressIndicator);
                }
            };
            createBranchTask.queue();
        }
    }

    public boolean doBranchCreate(@NotNull final ServerContext context, final ProgressIndicator progressIndicator) {
        logger.info("CreateBranchModel.doBranchCreate");
        // call server to create branch
        boolean hasNotifiedUser = false; //keep track of notifications because of recursive call
        String errorMessage = StringUtils.EMPTY;
        try {
            // ref update will create a new ref when no existing ref is found (we check for existing)
            final GitRefUpdate gitRefUpdate = new GitRefUpdate();
            gitRefUpdate.setName(REFS_PREFIX + getBranchName().replaceFirst(ORIGIN_PREFIX, StringUtils.EMPTY));
            gitRefUpdate.setOldObjectId(BASE_HASH); // since branch is new the last commit hash is all 0's
            gitRefUpdate.setNewObjectId(
                    GeneralGitHelper.getLastCommitHash(project, gitRepository, selectedRemoteBranch)); // TODO: get the latest commit from server b/c the latest local commit could be incorrect
            gitRefUpdate.setRepositoryId(context.getGitRepository().getId());

            logger.info("CreateBranchModel.createBranch sending create ref call to server");
            final List<GitRefUpdateResult> results = context.getGitHttpClient().updateRefs(
                    Arrays.asList(gitRefUpdate), context.getGitRepository().getId(),
                    context.getTeamProjectReference().getId().toString());

            // check returned results
            if (results.size() < 1 || !results.get(0).getSuccess()) {
                errorMessage = results.size() > 0 ? results.get(0).getCustomMessage()
                        : TfPluginBundle.KEY_CREATE_BRANCH_ERRORS_UNEXPECTED_SERVER_ERROR;
            } else {
                // Get the repository object for this project
                final GitRepository gitRepository = TfGitHelper.getTfGitRepository(project);
                if (gitRepository != null) {
                    // Create a progressIndicator if one was not passed in
                    // Fetch server changes so we can checkout here if we want to
                    logger.info("Fetching latest from server so that the new branch is available to checkout");
                    final GitFetcher fetcher = new GitFetcher(project, getProgressIndicator(progressIndicator),
                            true);
                    final GitFetchResult fetchResult = fetcher.fetch(gitRepository);
                    if (fetchResult.isSuccess() && this.checkoutBranch) {
                        logger.info("Checking out new branch: " + branchName);
                        // Creating a branch using the brancher has to start on the UI thread (it will background the work itself)
                        IdeaHelper.runOnUIThread(new Runnable() {
                            @Override
                            public void run() {
                                logger.info("Finishing: Checking out new branch: " + branchName);
                                final String remoteBranchName = TfGitHelper
                                        .getRemoteBranchName(selectedRemoteBranch.getRemote(), branchName);
                                final GitBrancher brancher = ServiceManager.getService(project, GitBrancher.class);
                                // Checkout a new branch from the remote branch that was created on the server and fetched above
                                brancher.checkoutNewBranchStartingFrom(branchName, remoteBranchName,
                                        Collections.singletonList(gitRepository), null);
                            }
                        });
                    }
                } else {
                    logger.warn(
                            "Could not fetch branch from server. Unable to retrieve the git repo object from the project.");
                }

            }
        } catch (Throwable t) {
            if (AuthHelper.isNotAuthorizedError(t)) {
                final ServerContext newContext = ServerContextManager.getInstance()
                        .updateAuthenticationInfo(context.getGitRepository().getRemoteUrl());
                if (newContext != null) {
                    //retry creating the branch with new context and authentication info
                    hasNotifiedUser = doBranchCreate(newContext, progressIndicator);
                } else {
                    //user cancelled login, don't retry
                    errorMessage = t.getMessage();
                }
            } else {
                errorMessage = t.getMessage();
            }
        }

        if (!hasNotifiedUser) {
            // alert user to success or error in creating the branch
            if (StringUtils.isEmpty(errorMessage)) {
                logger.info("Create branch succeeded");
                setBranchWasCreated(true);

                // create user notification
                final String branchLink = String.format(UrlHelper.SHORT_HTTP_LINK_FORMATTER,
                        UrlHelper.getBranchURI(context.getUri(), getBranchName()), getBranchName());
                VcsNotifier.getInstance(project)
                        .notifyImportantInfo(
                                TfPluginBundle.message(TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_SUCCESSFUL_TITLE),
                                TfPluginBundle.message(
                                        TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_SUCCESSFUL_DESCRIPTION, branchLink),
                                new NotificationListener() {
                                    @Override
                                    public void hyperlinkUpdate(@NotNull Notification notification,
                                            @NotNull HyperlinkEvent hyperlinkEvent) {
                                        BrowserUtil.browse(hyperlinkEvent.getURL());
                                    }
                                });
            } else {
                logger.warn("Create branch failed", errorMessage);
                VcsNotifier.getInstance(project).notifyError(
                        TfPluginBundle.message(TfPluginBundle.KEY_CREATE_BRANCH_DIALOG_FAILED_TITLE),
                        TfPluginBundle.message(TfPluginBundle.KEY_CREATE_BRANCH_ERRORS_BRANCH_CREATE_FAILED,
                                errorMessage));
            }

            // Add Telemetry for the create call along with it's success/failure
            TfsTelemetryHelper.getInstance().sendEvent(CREATE_BRANCH_ACTION,
                    new TfsTelemetryHelper.PropertyMapBuilder().currentOrActiveContext(context)
                            .actionName(CREATE_BRANCH_ACTION).success(StringUtils.isEmpty(errorMessage))
                            .message(errorMessage).build());

            hasNotifiedUser = true;
        }
        return hasNotifiedUser;
    }

    // This method will ensure that we have a progress indicator to pass on to other methods
    private ProgressIndicator getProgressIndicator(final ProgressIndicator indicator) {
        if (indicator != null) {
            return indicator;
        }

        // return a default implementation that doesn't do anything
        return new ProgressIndicator() {
            @Override
            public void start() {
            }

            @Override
            public void stop() {
            }

            @Override
            public boolean isRunning() {
                return false;
            }

            @Override
            public void cancel() {
            }

            @Override
            public boolean isCanceled() {
                return false;
            }

            @Override
            public void setText(String text) {
            }

            @Override
            public String getText() {
                return null;
            }

            @Override
            public void setText2(String text) {
            }

            @Override
            public String getText2() {
                return null;
            }

            @Override
            public double getFraction() {
                return 0;
            }

            @Override
            public void setFraction(double fraction) {
            }

            @Override
            public void pushState() {
            }

            @Override
            public void popState() {
            }

            @Override
            public void startNonCancelableSection() {
            }

            @Override
            public void finishNonCancelableSection() {
            }

            @Override
            public boolean isModal() {
                return false;
            }

            @NotNull
            @Override
            public ModalityState getModalityState() {
                return null;
            }

            @Override
            public void setModalityProgress(ProgressIndicator modalityProgress) {
            }

            @Override
            public boolean isIndeterminate() {
                return false;
            }

            @Override
            public void setIndeterminate(boolean indeterminate) {
            }

            @Override
            public void checkCanceled() throws ProcessCanceledException {
            }

            @Override
            public boolean isPopupWasShown() {
                return false;
            }

            @Override
            public boolean isShowing() {
                return false;
            }
        };
    }
}