org.zmlx.hg4idea.provider.commit.HgCheckinEnvironment.java Source code

Java tutorial

Introduction

Here is the source code for org.zmlx.hg4idea.provider.commit.HgCheckinEnvironment.java

Source

// Copyright 2008-2010 Victor Iacoban
//
// 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.zmlx.hg4idea.provider.commit;

import com.intellij.dvcs.AmendComponent;
import com.intellij.dvcs.push.ui.VcsPushDialog;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
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.vcs.CheckinProjectPanel;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
import com.intellij.openapi.vcs.ui.RefreshableOnComponent;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.GuiUtils;
import com.intellij.util.FunctionUtil;
import com.intellij.util.NullableFunction;
import com.intellij.util.PairConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.GridBag;
import com.intellij.util.ui.JBUI;
import com.intellij.vcsUtil.VcsUtil;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.*;
import org.zmlx.hg4idea.action.HgActionUtil;
import org.zmlx.hg4idea.command.*;
import org.zmlx.hg4idea.command.mq.HgQNewCommand;
import org.zmlx.hg4idea.execution.HgCommandException;
import org.zmlx.hg4idea.execution.HgCommandExecutor;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.provider.HgCurrentBinaryContentRevision;
import org.zmlx.hg4idea.repo.HgRepository;
import org.zmlx.hg4idea.repo.HgRepositoryManager;
import org.zmlx.hg4idea.util.HgUtil;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;

import static com.intellij.util.ObjectUtils.assertNotNull;
import static org.zmlx.hg4idea.util.HgUtil.getRepositoryManager;

public class HgCheckinEnvironment implements CheckinEnvironment {

    private final Project myProject;
    private boolean myNextCommitIsPushed;
    private boolean myNextCommitAmend; // If true, the next commit is amended
    private boolean myShouldCommitSubrepos;
    private boolean myMqNewPatch;
    private boolean myCloseBranch;
    @Nullable
    private Collection<HgRepository> myRepos;

    public HgCheckinEnvironment(Project project) {
        myProject = project;
    }

    public RefreshableOnComponent createAdditionalOptionsPanel(CheckinProjectPanel panel,
            PairConsumer<Object, Object> additionalDataConsumer) {
        reset();
        return new HgCommitAdditionalComponent(myProject, panel);
    }

    private void reset() {
        myNextCommitIsPushed = false;
        myShouldCommitSubrepos = false;
        myCloseBranch = false;
        myMqNewPatch = false;
        myRepos = null;
    }

    public String getDefaultMessageFor(FilePath[] filesToCheckin) {
        return null;
    }

    public String getHelpId() {
        return null;
    }

    public String getCheckinOperationName() {
        return HgVcsMessages.message("hg4idea.commit");
    }

    public List<VcsException> commit(List<Change> changes, String preparedComment,
            @NotNull NullableFunction<Object, Object> parametersHolder, Set<String> feedback) {
        List<VcsException> exceptions = new LinkedList<>();
        Map<HgRepository, Set<HgFile>> repositoriesMap = getFilesByRepository(changes);
        addRepositoriesWithoutChanges(repositoriesMap);
        for (Map.Entry<HgRepository, Set<HgFile>> entry : repositoriesMap.entrySet()) {

            HgRepository repo = entry.getKey();
            Set<HgFile> selectedFiles = entry.getValue();
            HgCommitTypeCommand command = myMqNewPatch
                    ? new HgQNewCommand(myProject, repo, preparedComment, myNextCommitAmend)
                    : new HgCommitCommand(myProject, repo, preparedComment, myNextCommitAmend, myCloseBranch,
                            myShouldCommitSubrepos && !selectedFiles.isEmpty());

            if (isMergeCommit(repo.getRoot())) {
                //partial commits are not allowed during merges
                //verifyResult that all changed files in the repo are selected
                //If so, commit the entire repository
                //If not, abort

                Set<HgFile> changedFilesNotInCommit = getChangedFilesNotInCommit(repo.getRoot(), selectedFiles);
                boolean partial = !changedFilesNotInCommit.isEmpty();

                if (partial) {
                    final StringBuilder filesNotIncludedString = new StringBuilder();
                    for (HgFile hgFile : changedFilesNotInCommit) {
                        filesNotIncludedString.append("<li>");
                        filesNotIncludedString.append(hgFile.getRelativePath());
                        filesNotIncludedString.append("</li>");
                    }
                    if (!mayCommitEverything(filesNotIncludedString.toString())) {
                        //abort
                        return exceptions;
                    }
                    //firstly selected changes marked dirty in CommitHelper -> postRefresh, so we need to mark others
                    VcsDirtyScopeManager dirtyManager = VcsDirtyScopeManager.getInstance(myProject);
                    for (HgFile hgFile : changedFilesNotInCommit) {
                        dirtyManager.fileDirty(hgFile.toFilePath());
                    }
                }
                // else : all was included, or it was OK to commit everything,
                // so no need to set the files on the command, because then mercurial will complain
            } else {
                command.setFiles(selectedFiles);
            }
            try {
                command.executeInCurrentThread();
            } catch (HgCommandException e) {
                exceptions.add(new VcsException(e));
            } catch (VcsException e) {
                exceptions.add(e);
            }
        }

        // push if needed
        if (myNextCommitIsPushed && exceptions.isEmpty()) {
            final List<HgRepository> preselectedRepositories = ContainerUtil.newArrayList(repositoriesMap.keySet());
            GuiUtils.invokeLaterIfNeeded(() -> new VcsPushDialog(myProject, preselectedRepositories,
                    HgUtil.getCurrentRepository(myProject)).show(), ModalityState.defaultModalityState());
        }

        return exceptions;
    }

    private boolean isMergeCommit(VirtualFile repo) {
        return new HgWorkingCopyRevisionsCommand(myProject).parents(repo).size() > 1;
    }

    private Set<HgFile> getChangedFilesNotInCommit(VirtualFile repo, Set<HgFile> selectedFiles) {
        List<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo);

        HgStatusCommand statusCommand = new HgStatusCommand.Builder(true).unknown(false).ignored(false)
                .baseRevision(parents.get(0)).build(myProject);
        Set<HgChange> allChangedFilesInRepo = statusCommand.executeInCurrentThread(repo);

        Set<HgFile> filesNotIncluded = new HashSet<>();

        for (HgChange change : allChangedFilesInRepo) {
            HgFile beforeFile = change.beforeFile();
            HgFile afterFile = change.afterFile();
            if (!selectedFiles.contains(beforeFile)) {
                filesNotIncluded.add(beforeFile);
            } else if (!selectedFiles.contains(afterFile)) {
                filesNotIncluded.add(afterFile);
            }
        }
        return filesNotIncluded;
    }

    private boolean mayCommitEverything(final String filesNotIncludedString) {
        final int[] choice = new int[1];
        Runnable runnable = new Runnable() {
            public void run() {
                choice[0] = Messages.showOkCancelDialog(myProject,
                        HgVcsMessages.message("hg4idea.commit.partial.merge.message", filesNotIncludedString),
                        HgVcsMessages.message("hg4idea.commit.partial.merge.title"), null);
            }
        };
        ApplicationManager.getApplication().invokeAndWait(runnable);
        return choice[0] == Messages.OK;
    }

    public List<VcsException> commit(List<Change> changes, String preparedComment) {
        return commit(changes, preparedComment, FunctionUtil.nullConstant(), null);
    }

    public List<VcsException> scheduleMissingFileForDeletion(List<FilePath> files) {
        final List<HgFile> filesWithRoots = new ArrayList<>();
        for (FilePath filePath : files) {
            VirtualFile vcsRoot = VcsUtil.getVcsRootFor(myProject, filePath);
            if (vcsRoot == null) {
                continue;
            }
            filesWithRoots.add(new HgFile(vcsRoot, filePath));
        }
        new Task.Backgroundable(myProject, "Removing Files...") {
            @Override
            public void run(@NotNull ProgressIndicator indicator) {
                new HgRemoveCommand(myProject).executeInCurrentThread(filesWithRoots);
            }
        }.queue();
        return null;
    }

    public List<VcsException> scheduleUnversionedFilesForAddition(final List<VirtualFile> files) {
        new HgAddCommand(myProject).addWithProgress(files);
        return null;
    }

    public boolean keepChangeListAfterCommit(ChangeList changeList) {
        return false;
    }

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

    @NotNull
    private Map<HgRepository, Set<HgFile>> getFilesByRepository(List<Change> changes) {
        Map<HgRepository, Set<HgFile>> result = new HashMap<>();
        for (Change change : changes) {
            ContentRevision afterRevision = change.getAfterRevision();
            ContentRevision beforeRevision = change.getBeforeRevision();

            if (afterRevision != null) {
                addFile(result, afterRevision);
            }
            if (beforeRevision != null) {
                addFile(result, beforeRevision);
            }
        }
        return result;
    }

    private void addFile(Map<HgRepository, Set<HgFile>> result, ContentRevision contentRevision) {
        FilePath filePath = contentRevision.getFile();
        // try to find repository from hgFile from change: to be able commit sub repositories as expected
        HgRepository repo = HgUtil.getRepositoryForFile(myProject,
                contentRevision instanceof HgCurrentBinaryContentRevision
                        ? ((HgCurrentBinaryContentRevision) contentRevision).getRepositoryRoot()
                        : ChangesUtil.findValidParentAccurately(filePath));
        if (repo == null) {
            return;
        }

        Set<HgFile> hgFiles = result.get(repo);
        if (hgFiles == null) {
            hgFiles = new HashSet<>();
            result.put(repo, hgFiles);
        }

        hgFiles.add(new HgFile(repo.getRoot(), filePath));
    }

    public void setNextCommitIsPushed() {
        myNextCommitIsPushed = true;
    }

    public void setMqNew() {
        myMqNewPatch = true;
    }

    public void setCloseBranch(boolean closeBranch) {
        myCloseBranch = closeBranch;
    }

    public void setRepos(@NotNull Collection<HgRepository> repos) {
        myRepos = repos;
    }

    private void addRepositoriesWithoutChanges(@NotNull Map<HgRepository, Set<HgFile>> repositoryMap) {
        if (myRepos == null)
            return;
        for (HgRepository repository : myRepos) {
            if (!repositoryMap.keySet().contains(repository)) {
                repositoryMap.put(repository, Collections.<HgFile>emptySet());
            }
        }
    }

    /**
     * Commit options for hg
     */
    public class HgCommitAdditionalComponent implements RefreshableOnComponent {
        @NotNull
        private final JPanel myPanel;
        @NotNull
        private final AmendComponent myAmend;
        @NotNull
        private final JCheckBox myCommitSubrepos;

        HgCommitAdditionalComponent(@NotNull Project project, @NotNull CheckinProjectPanel panel) {
            HgVcs vcs = assertNotNull(HgVcs.getInstance(myProject));

            myAmend = new MyAmendComponent(project, getRepositoryManager(project), panel,
                    "Amend Commit (QRefresh)");
            myAmend.getComponent().setEnabled(vcs.getVersion().isAmendSupported());

            myCommitSubrepos = new JCheckBox("Commit subrepositories", false);
            myCommitSubrepos
                    .setToolTipText(XmlStringUtil.wrapInHtml("Commit all subrepos for selected repositories.<br>"
                            + " <code>hg ci <i><b>files</b></i> -S <i><b>subrepos</b></i></code>"));
            myCommitSubrepos.setMnemonic('s');
            Collection<HgRepository> repos = HgActionUtil
                    .collectRepositoriesFromFiles(getRepositoryManager(myProject), panel.getRoots());
            myCommitSubrepos.setVisible(ContainerUtil.exists(repos, HgRepository::hasSubrepos));

            myCommitSubrepos.addActionListener(new MySelectionListener(myAmend.getCheckBox()));
            myAmend.getCheckBox().addActionListener(new MySelectionListener(myCommitSubrepos));

            GridBag gb = new GridBag().setDefaultInsets(JBUI.insets(2)).setDefaultAnchor(GridBagConstraints.WEST)
                    .setDefaultWeightX(1).setDefaultFill(GridBagConstraints.HORIZONTAL);
            myPanel = new JPanel(new GridBagLayout());
            myPanel.add(myAmend.getComponent(), gb.nextLine().next());
            myPanel.add(myCommitSubrepos, gb.nextLine().next());
        }

        @Override
        public void refresh() {
            myAmend.refresh();
            restoreState();
        }

        @Override
        public void saveState() {
            myNextCommitAmend = isAmend();
            myShouldCommitSubrepos = myCommitSubrepos.isSelected();
        }

        @Override
        public void restoreState() {
            myNextCommitAmend = false;
            myShouldCommitSubrepos = false;
        }

        @Override
        public JComponent getComponent() {
            return myPanel;
        }

        public boolean isAmend() {
            return myAmend.isAmend();
        }

        private class MyAmendComponent extends AmendComponent {
            public MyAmendComponent(@NotNull Project project, @NotNull HgRepositoryManager repoManager,
                    @NotNull CheckinProjectPanel panel, @NotNull String title) {
                super(project, repoManager, panel, title);
            }

            @NotNull
            @Override
            protected Set<VirtualFile> getVcsRoots(@NotNull Collection<FilePath> filePaths) {
                return HgUtil.hgRoots(myProject, filePaths);
            }

            @Nullable
            @Override
            protected String getLastCommitMessage(@NotNull VirtualFile repo) throws VcsException {
                HgCommandExecutor commandExecutor = new HgCommandExecutor(myProject);
                List<String> args = new ArrayList<>();
                args.add("-r");
                args.add(".");
                args.add("--template");
                args.add("{desc}");
                HgCommandResult result = commandExecutor.executeInCurrentThread(repo, "log", args);
                return result == null ? "" : result.getRawOutput();
            }
        }

        private class MySelectionListener implements ActionListener {
            private final JCheckBox myUnselectedComponent;

            public MySelectionListener(JCheckBox unselectedComponent) {
                myUnselectedComponent = unselectedComponent;
            }

            @Override
            public void actionPerformed(ActionEvent e) {
                JCheckBox source = (JCheckBox) e.getSource();
                if (source.isSelected()) {
                    myUnselectedComponent.setSelected(false);
                    myUnselectedComponent.setEnabled(false);
                } else {
                    myUnselectedComponent.setEnabled(true);
                }
            }
        }
    }
}