org.community.intellij.plugins.communitycase.checkin.CheckinEnvironment.java Source code

Java tutorial

Introduction

Here is the source code for org.community.intellij.plugins.communitycase.checkin.CheckinEnvironment.java

Source

/*
 * Copyright 2000-2009 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.community.intellij.plugins.communitycase.checkin;

import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.CheckinProjectPanel;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeList;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vcs.ui.RefreshableOnComponent;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.NullableFunction;
import com.intellij.util.PairConsumer;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.commands.Command;
import org.community.intellij.plugins.communitycase.commands.FileUtils;
import org.community.intellij.plugins.communitycase.commands.SimpleHandler;
import org.community.intellij.plugins.communitycase.config.VcsSettings;
import org.community.intellij.plugins.communitycase.history.HistoryUtils;
import org.community.intellij.plugins.communitycase.history.NewUsersComponent;
import org.community.intellij.plugins.communitycase.i18n.Bundle;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;

/**
 * Environment for commit operations.
 */
public class CheckinEnvironment implements com.intellij.openapi.vcs.checkin.CheckinEnvironment {
    private static final Logger log = Logger.getInstance("#" + CheckinEnvironment.class.getName());
    @NonNls
    private static final String GIT_COMMIT_MSG_FILE_PREFIX = "cc-commit-msg-"; // the file name prefix for commit message file
    @NonNls
    private static final String GIT_COMMIT_MSG_FILE_EXT = ".txt"; // the file extension for commit message file

    private final Project myProject;
    private final VcsDirtyScopeManager myDirtyScopeManager;
    private final VcsSettings mySettings;

    private boolean myNextCommitGenerate; //the status of the 'generate report' option added to the commit menu
    private Boolean myNextCommitIsPushed = null; // The push option of the next commit

    public CheckinEnvironment(@NotNull Project project, @NotNull final VcsDirtyScopeManager dirtyScopeManager,
            final VcsSettings settings) {
        myProject = project;
        myDirtyScopeManager = dirtyScopeManager;
        mySettings = settings;
    }

    /** {@inheritDoc} */
    @Override
    public boolean keepChangeListAfterCommit(ChangeList changeList) {
        return false;
    }

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

    /** {@inheritDoc} */
    @Nullable
    @Override
    public RefreshableOnComponent createAdditionalOptionsPanel(CheckinProjectPanel panel,
            PairConsumer<Object, Object> additionalDataConsumer) {
        return new CheckinOptions(myProject, panel.getRoots());
    }

    @Nullable
    @Override
    public String getDefaultMessageFor(FilePath[] filesToCheckin) {
        StringBuilder rc = new StringBuilder();

        //todo wc get use the checkout message from the first file that has one

        if (rc.length() != 0) {
            return rc.toString();
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public String getHelpId() {
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public String getCheckinOperationName() {
        return Bundle.getString("commit.action.name");
    }

    /** {@inheritDoc} */
    @Override
    public List<VcsException> commit(@NotNull List<Change> changes, @NotNull String message,
            @NotNull NullableFunction<Object, Object> parametersHolder, @Nullable Set<String> feedback) {
        List<VcsException> exceptions = new ArrayList<VcsException>();
        if (message.length() == 0) {
            //noinspection ThrowableInstanceNeverThrown
            exceptions.add(new VcsException("Empty commit message is not supported for the Git"));
            return exceptions;
        }
        Map<VirtualFile, List<Change>> sortedChanges = sortChangesByGitRoot(changes, exceptions);
        for (Map.Entry<VirtualFile, List<Change>> entry : sortedChanges.entrySet()) {
            Set<FilePath> files = new HashSet<FilePath>();
            final VirtualFile root = entry.getKey();
            try {
                File messageFile = createMessageFile(root, message);
                try {
                    final Set<FilePath> added = new HashSet<FilePath>();
                    final Set<FilePath> modified = new HashSet<FilePath>();
                    final Set<FilePath> removed = new HashSet<FilePath>();
                    for (Change change : entry.getValue()) {
                        switch (change.getType()) {
                        case NEW:
                            added.add(change.getAfterRevision().getFile());
                            break;
                        case MODIFICATION:
                            modified.add(change.getAfterRevision().getFile());
                            break;
                        case DELETED:
                            removed.add(change.getBeforeRevision().getFile());
                            break;
                        case MOVED:
                            added.add(change.getAfterRevision().getFile());
                            removed.add(change.getBeforeRevision().getFile());
                            break;
                        default:
                            throw new IllegalStateException("Unknown change type: " + change.getType());
                        }
                    }
                    try {
                        if (updateIndex(myProject, root, added, removed, exceptions)) {
                            try {
                                files.addAll(added);
                                files.addAll(modified);
                                files.addAll(removed);
                                commit(myProject, root, files, messageFile, myNextCommitGenerate);
                            } catch (VcsException ex) {
                                if (!isMergeCommit(ex)) {
                                    throw ex;
                                }
                                if (!mergeCommit(myProject, root, added, removed, modified, messageFile,
                                        exceptions)) {
                                    throw ex;
                                }
                            }
                        }
                    } finally {
                        if (!messageFile.delete()) {
                            log.warn("Failed to remove temporary file: " + messageFile);
                        }
                    }
                } catch (VcsException e) {
                    exceptions.add(e);
                }
            } catch (IOException ex) {
                //noinspection ThrowableInstanceNeverThrown
                exceptions.add(new VcsException("Creation of commit message file failed", ex));
            } catch (Exception e) {
                //noinspection ThrowableInstanceNeverThrown
                exceptions.add(new VcsException(e));
            }
        }
        if (myNextCommitIsPushed != null && myNextCommitIsPushed.booleanValue() && exceptions.isEmpty()) {
            // push
            UIUtil.invokeLaterIfNeeded(new Runnable() {
                public void run() {
                    PushActiveBranchesDialog.showDialogForProject(myProject);
                }
            });
        }
        return exceptions;
    }

    /** {@inheritDoc} */
    @Override
    public List<VcsException> commit(List<Change> changes, String preparedComment) {
        //noinspection unchecked
        return commit(changes, preparedComment, NullableFunction.NULL, null);
    }

    /**
     * Preform a merge commit
     *
     *
     * @param project     a project
     * @param root        a vcs root
     * @param added       added files
     * @param removed     removed files
     * @param modified    modified files
     * @param messageFile a message file for commit
     * @param exceptions  the list of exceptions to report    @return true if merge commit was successful
     * */
    private static boolean mergeCommit(final Project project, final VirtualFile root, final Set<FilePath> added,
            final Set<FilePath> removed, final Set<FilePath> modified, final File messageFile,
            List<VcsException> exceptions) {
        /*    HashSet<FilePath> realAdded = new HashSet<FilePath>();
            HashSet<FilePath> realRemoved = new HashSet<FilePath>();
            // perform diff
            SimpleHandler diff = new SimpleHandler(project, root, Command.DIFF);
            diff.setRemote(true);
            diff.setSilent(true);
            diff.setStdoutSuppressed(true);
            diff.addParameters("--diff-filter=ADMRUX", "--name-status", "HEAD");
            diff.endOptions();
            String output;
            try {
              output = diff.run();
            }
            catch (VcsException ex) {
              exceptions.add(ex);
              return false;
            }
            String rootPath = root.getPath();
            for (StringTokenizer lines = new StringTokenizer(output, "\n", false); lines.hasMoreTokens();) {
              String line = lines.nextToken().trim();
              if (line.length() == 0) {
                continue;
              }
              String[] tk = line.split("[ \t]+");
              switch (tk[0].charAt(0)) {
                case 'M':
                case 'A':
                  realAdded.add(VcsUtil.getFilePath(rootPath + "/" + tk[tk.length - 1]));
                  break;
                case 'D':
                  realRemoved.add(VcsUtil.getFilePathForDeletedFile(rootPath + "/" + tk[tk.length - 1], false));
                  break;
                default:
                  throw new IllegalStateException("Unexpected status: " + line);
              }
            }
            realAdded.removeAll(added);
            realRemoved.removeAll(removed);
        */
        //if (realAdded.size() != 0 || realRemoved.size() != 0) {
        TreeSet<String> files = new TreeSet<String>();
        /*
          for (FilePath f : realAdded) {
            files.add(f.getPresentableUrl());
          }
          for (FilePath f : realRemoved) {
            files.add(f.getPresentableUrl());
          }
          */
        for (FilePath f : added) {
            files.add(f.getPresentableUrl());
        }
        for (FilePath f : removed) {
            files.add(f.getPresentableUrl());
        }
        for (FilePath f : modified) {
            files.add(f.getPresentableUrl());
        }
        final StringBuilder fileList = new StringBuilder();
        for (String f : files) {
            //noinspection HardCodedStringLiteral
            fileList.append("<li>");
            fileList.append(StringUtil.escapeXml(f));
            fileList.append("</li>");
        }
        final int[] rc = new int[1];
        try {
            EventQueue.invokeAndWait(new Runnable() {
                public void run() {
                    rc[0] = Messages.showOkCancelDialog(project,
                            Bundle.message("commit.partial.merge.message", fileList.toString()),
                            Bundle.getString("commit.partial.merge.title"), null);

                }
            });
        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new RuntimeException("Unable to invoke a message box on AWT thread", ex);
        }
        if (rc[0] != 0) {
            return false;
        }
        // update non-indexed files
        /*if (!updateIndex(project, root, realAdded, realRemoved, exceptions)) {
          return false;
        }
        for (FilePath f : realAdded) {
          VcsDirtyScopeManager.getInstance(project).fileDirty(f);
        }
        for (FilePath f : realRemoved) {
          VcsDirtyScopeManager.getInstance(project).fileDirty(f);
        }*/
        //}
        // perform merge commit
        try {
            SimpleHandler handler = new SimpleHandler(project, root, Command.CHECKIN);
            handler.addParameters("-cfi", messageFile.getAbsolutePath());
            handler.endOptions();
            for (FilePath path : added)
                handler.addParameters(path.getName());
            for (FilePath path : modified)
                handler.addParameters(path.getName());
            for (FilePath path : removed)
                handler.addParameters(path.getName());
            handler.run();
        } catch (VcsException ex) {
            exceptions.add(ex);
            return false;
        }
        return true;
    }

    /**
     * Check if commit has failed due to unfinished merge
     *
     * @param ex an exception to examine
     * @return true if exception means that there is a partial commit during merge
     */
    private static boolean isMergeCommit(final VcsException ex) {
        //noinspection HardCodedStringLiteral
        return -1 != ex.getMessage().indexOf("fatal: cannot do a partial commit during a merge.");
    }

    /**
     * Update index (delete and remove files)
     *
     * @param project    the project
     * @param root       a vcs root
     * @param added      added/modified files to commit
     * @param removed    removed files to commit
     * @param exceptions a list of exceptions to update
     * @return true if index was updated successfully
     */
    private static boolean updateIndex(final Project project, final VirtualFile root,
            final Collection<FilePath> added, final Collection<FilePath> removed,
            final List<VcsException> exceptions) {
        boolean rc = true;
        if (!added.isEmpty()) {
            try {
                FileUtils.addPaths(project, root, added);
            } catch (VcsException ex) {
                exceptions.add(ex);
                rc = false;
            }
        }
        if (!removed.isEmpty()) {
            try {
                FileUtils.delete(project, root, removed, "--ignore-unmatch");
            } catch (VcsException ex) {
                exceptions.add(ex);
                rc = false;
            }
        }
        return rc;
    }

    /**
     * Create a file that contains the specified message
     *
     * @param root    a git repository root
     * @param message a message to write
     * @return a file reference
     * @throws IOException if file cannot be created
     */
    private File createMessageFile(VirtualFile root, final String message) throws IOException {
        // filter comment lines
        File file = FileUtil.createTempFile(GIT_COMMIT_MSG_FILE_PREFIX, GIT_COMMIT_MSG_FILE_EXT);
        file.deleteOnExit();
        //@NonNls String encoding = ConfigUtil.getCommitEncoding(myProject, root);
        Writer out = new OutputStreamWriter(new FileOutputStream(file)); //new OutputStreamWriter(new FileOutputStream(file), encoding);
        try {
            out.write(message);
        } finally {
            out.close();
        }
        return file;
    }

    /**
     * {@inheritDoc}
     */
    public List<VcsException> scheduleMissingFileForDeletion(List<FilePath> files) {
        ArrayList<VcsException> rc = new ArrayList<VcsException>();
        Map<VirtualFile, List<FilePath>> sortedFiles;
        try {
            sortedFiles = Util.sortFilePathsByRoot(files);
        } catch (VcsException e) {
            rc.add(e);
            return rc;
        }
        for (Map.Entry<VirtualFile, List<FilePath>> e : sortedFiles.entrySet()) {
            try {
                final VirtualFile root = e.getKey();
                FileUtils.delete(myProject, root, e.getValue());
                markRootDirty(root);
            } catch (VcsException ex) {
                rc.add(ex);
            }
        }
        return rc;
    }

    /**
     * Prepare delete files handler.
     *
     *
     * @param project          the project
     * @param root             a vcs root
     * @param files            a files to commit
     * @param message          a message file to use
     * @param nextCommitGenerate  true, if the commit should be amended
     * @throws VcsException in case of git problem
     */
    private static void commit(Project project, VirtualFile root, Collection<FilePath> files, File message,
            boolean nextCommitGenerate) throws VcsException {
        //todo wc checkin directories last??
        //todo wc fix directory checkin comments
        for (List<String> paths : FileUtils.chunkPaths(root, files)) {
            SimpleHandler handler = new SimpleHandler(project, root, Command.CHECKIN);
            handler.setRemote(true);
            handler.addParameters("-cfi", message.getAbsolutePath());
            handler.endOptions();
            handler.addParameters(paths);
            handler.run();
        }
        if (nextCommitGenerate) {
            //Map<FilePath,VcsRevisionNumber> pathAndVersions=new HashMap<FilePath,VcsRevisionNumber>();
            StringBuilder changes = new StringBuilder();
            for (FilePath fp : files)
                changes.append(Util.relativePath(VcsUtil.getVcsRootFor(project, fp), fp)).append("@@")
                        .append(HistoryUtils.getCurrentRevision(project, fp)).append("\n");
            //pathAndVersions.put(fp,HistoryUtils.getCurrentRevision(project,fp,null));

            /*
                  JTextArea report=new JTextArea(changes.toString());
                  JBPopupFactory.getInstance().createComponentPopupBuilder(report,report).createPopup();
            */
            String title = "Checkin Report";
            //Messages.showDialog(project,msg,title,);
            //Messages.showInfoMessage(project,msg,title);

            //TODO create a message report window. Messages.showMessageDialog used to work, but does not anymore.
            //Messages.showMessageDialog(project,changes.toString(),title,null);

            //Messages.showMultilineInputDialog(project,msg,title,"bla",null,null);

            /*
            DialogBuilder db=new DialogBuilder(project);
            db.setCenterPanel(new JTextArea("bla\nfile2 .java\nfile3\n"));
            db.addCloseButton();
            db.show();
            */
            /*
            JBPopupFactory.getInstance().createComponentPopupBuilder(new JTextArea("bla\nfile2 .java\nfile3\n"),null)
                    .setResizable(true)
                    .setMovable(true)
                    .setRequestFocus(true)
                    .createPopup()
                    .show(new RelativePoint(new Point(0,0)));
            */
            Notifications.Bus
                    .notify(new Notification(Vcs.NOTIFICATION_GROUP_ID, Bundle.message("checkin.success.title"),
                            Bundle.getString("checkin.success.message"), NotificationType.INFORMATION), project);
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<VcsException> scheduleUnversionedFilesForAddition(List<VirtualFile> files) {
        ArrayList<VcsException> rc = new ArrayList<VcsException>();
        Map<VirtualFile, List<VirtualFile>> sortedFiles;
        try {
            sortedFiles = Util.sortFilesByRoot(files);
        } catch (VcsException e) {
            rc.add(e);
            return rc;
        }
        for (Map.Entry<VirtualFile, List<VirtualFile>> e : sortedFiles.entrySet()) {
            try {
                final VirtualFile root = e.getKey();
                FileUtils.addFiles(myProject, root, e.getValue());
                markRootDirty(root);
            } catch (VcsException ex) {
                rc.add(ex);
            }
        }
        return rc;
    }

    /**
     * Sort changes by roots
     *
     * @param changes    a change list
     * @param exceptions exceptions to collect
     * @return sorted changes
     */
    private static Map<VirtualFile, List<Change>> sortChangesByGitRoot(@NotNull List<Change> changes,
            List<VcsException> exceptions) {
        Map<VirtualFile, List<Change>> result = new HashMap<VirtualFile, List<Change>>();
        for (Change change : changes) {
            final ContentRevision afterRevision = change.getAfterRevision();
            final ContentRevision beforeRevision = change.getBeforeRevision();
            // nothing-to-nothing change cannot happen.
            assert beforeRevision != null || afterRevision != null;
            // note that any path will work, because changes could happen within single vcs root
            final FilePath filePath = afterRevision != null ? afterRevision.getFile() : beforeRevision.getFile();
            final VirtualFile vcsRoot;
            try {
                // the parent paths for calculating roots in order to account for submodules that contribute
                // to the parent change. The path "." is never is valid change, so there should be no problem
                // with it.
                vcsRoot = Util.getRoot(filePath.getParentPath());
            } catch (VcsException e) {
                exceptions.add(e);
                continue;
            }
            List<Change> changeList = result.get(vcsRoot);
            if (changeList == null) {
                changeList = new ArrayList<Change>();
                result.put(vcsRoot, changeList);
            }
            changeList.add(change);
        }
        return result;
    }

    /**
     * Mark root as dirty
     *
     * @param root a vcs root to rescan
     */
    private void markRootDirty(final VirtualFile root) {
        // Note that the root is invalidated because changes are detected per-root anyway.
        // Otherwise it is not possible to detect moves.
        myDirtyScopeManager.dirDirtyRecursively(root);
    }

    /**
     * Checkin options for git
     */
    private class CheckinOptions implements RefreshableOnComponent {
        /**
         * A container panel
         */
        private final JPanel myPanel;
        /**
         * The 'generate report' checkbox
         */
        private final JCheckBox myGenerate;

        /**
         * A constructor
         *
         * @param project
         * @param roots
         */
        CheckinOptions(Project project, Collection<VirtualFile> roots) {
            myPanel = new JPanel(new GridBagLayout());
            final Insets insets = new Insets(2, 2, 2, 2);
            GridBagConstraints c = new GridBagConstraints();
            c.gridx = 0;
            c.gridy = 0;
            c.anchor = GridBagConstraints.WEST;
            c.insets = insets;
            c.weightx = 1.0;
            myGenerate = new JCheckBox(Bundle.getString("commit.generate"));
            //myGenerate.setMnemonic('m');
            myGenerate.setSelected(true); //todo wc make this configurable from CC VCS options
            myGenerate.setToolTipText(Bundle.getString("commit.generate.tooltip"));
            myPanel.add(myGenerate, c);
        }

        private List<String> getUsersList(final Project project, final Collection<VirtualFile> roots) {
            return NewUsersComponent.getInstance(project).get();
        }

        /**
         * {@inheritDoc}
         */
        public JComponent getComponent() {
            return myPanel;
        }

        /**
         * {@inheritDoc}
         */
        public void refresh() {
            myGenerate.setSelected(true); //todo wc fix this...
            myNextCommitIsPushed = null;
        }

        /**
         * {@inheritDoc}
         */
        public void saveState() {
            myNextCommitGenerate = myGenerate.isSelected();
        }

        /**
         * {@inheritDoc}
         */
        public void restoreState() {
            refresh();
        }
    }

    public void setNextCommitIsPushed(Boolean nextCommitIsPushed) {
        myNextCommitIsPushed = nextCommitIsPushed;
    }
}