[Mercurial] HgRemoveCheckinHandler: on commit provide a dialog to remove missing...
authorKirill Likhodedov <kirill.likhodedov@jetbrains.com>
Thu, 10 Jun 2010 07:32:58 +0000 (11:32 +0400)
committerKirill Likhodedov <kirill.likhodedov@jetbrains.com>
Thu, 10 Jun 2010 07:32:58 +0000 (11:32 +0400)
plugins/hg4idea/hg4idea.iml
plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties
plugins/hg4idea/src/META-INF/plugin.xml
plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java [new file with mode: 0644]
plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java [new file with mode: 0644]
plugins/hg4idea/src/org/zmlx/hg4idea/HgUtil.java
plugins/hg4idea/src/org/zmlx/hg4idea/HgVFSListener.java
plugins/hg4idea/src/org/zmlx/hg4idea/provider/commit/HgCheckinEnvironment.java

index b8677676d4935004731189470d101a66e64ae720..ccb452415959f410ee3ba777db216092c54f879d 100644 (file)
@@ -13,6 +13,7 @@
     <orderEntry type="library" name="commons-lang" level="project" />
     <orderEntry type="library" scope="TEST" name="TestNG" level="project" />
     <orderEntry type="module" module-name="testFramework" scope="TEST" />
+    <orderEntry type="module" module-name="vcs-impl" />
   </component>
 </module>
 
index acd0c6cce8f3b96f821e34f686742513c7cdd8f0..baa7501fcff7383f47288fe1e8a82f8c247f297e 100644 (file)
@@ -55,11 +55,21 @@ hg4idea.commit.error.unknown=Could not commit, no error message was provided
 hg4idea.commit.repository.title=Commit Repositories
 hg4idea.commit.repository.body=Commit all changes in all affected repositories
 
-hg4idea.add.confirmation.title=Add file(s)
-hg4idea.add.confirmation.body=Do you want to schedule the following file for addition to Mercurial?\n{0}
+hg4idea.add.title=Add files to Mercurial
+hg4idea.add.single.title=Add file to Mercurial
+hg4idea.add.body=Do you want to add the following file to Mercurial?\n{0}\n\nIf you say NO, you can still add it later manually.
+hg4idea.add.progress=Adding files to Mercurial
 
-hg4idea.delete.confirmation.title=Delete file(s)
-hg4idea.delete.confirmation.body=Do you want to schedule the following file for deletion from Mercurial?\n{0}
+hg4idea.remove.single.title=Remove file from Mercurial
+hg4idea.remove.single.body=Do you want to remove the following file from Mercurial?\n{0}\n\nIf you say NO, you can still remove it later manually.
+hg4idea.remove.commit.single.body=Do you want to remove the following file from Mercurial?\n{0}\n\nIf you say NO, the file will be excluded from the commit, but you can still remove it later manually.
+hg4idea.remove.multiple.title=Remove files from Mercurial
+hg4idea.remove.commit.multiple.title=Select files to remove from Mercurial
+hg4idea.remove.commit.multiple.description=<html>The files you select will be scheduled on deletion from Mercurial and then committed. <br/> Others won't be neither removed from the repository, nor committed.</html>
+hg4idea.remove.button.ok=Remove
+hg4idea.remove.progress=Removing files from the VCS...
+
+hg4idea.move.progress=Moving files in the VCS...
 
 hg4idea.update.error.uncommittedMerge=outstanding uncommitted merge in repository {0}, not updating or merging
 hg4idea.update.error.localchanges=outstanding uncommitted changes in repository {0}, not merging with pulled head
@@ -87,4 +97,4 @@ hg4idea.status.currentSituation.description=<html>Current mercurial branch and p
 hg4idea.warning.no-default-update-path=Skipped \"{0}\". No default update path.
 hg4idea.merge.please-commit=Merged heads, please commit repository \"{0}\"
 hg4idea.error.invalidExecutable=\"{0}\" is not a valid mercurial executable
-hg4idea.integrate.other.head=Other head: {0}
+hg4idea.integrate.other.head=Other head: {0}
\ No newline at end of file
index 0406e1850618eba8ef60eb9721049d2bba778786..98ab049147e285a66ca5abb8a686173761685533 100644 (file)
@@ -27,6 +27,7 @@
     <checkoutProvider implementation="org.zmlx.hg4idea.provider.HgCheckoutProvider"/>
     <errorHandler implementation="com.intellij.diagnostic.ITNReporter"/>
     <applicationConfigurable implementation="org.zmlx.hg4idea.HgIdeConfigurable"/>
+    <checkinHandlerFactory implementation="org.zmlx.hg4idea.HgRemoveCheckinHandlerFactory" />
   </extensions>
 
   <actions>
diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java
new file mode 100644 (file)
index 0000000..ac0f6e8
--- /dev/null
@@ -0,0 +1,130 @@
+package org.zmlx.hg4idea;
+
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.ui.TitlePanel;
+import com.intellij.openapi.vcs.*;
+import com.intellij.openapi.vcs.changes.Change;
+import com.intellij.openapi.vcs.changes.CommitExecutor;
+import com.intellij.openapi.vcs.changes.ui.*;
+import com.intellij.openapi.vcs.checkin.CheckinHandler;
+import com.intellij.util.PairConsumer;
+import com.intellij.util.ui.ConfirmationDialog;
+import gnu.trove.THashSet;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultTreeModel;
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+
+/**
+ * HgRemoveCheckinHandler scans the changes which are ready for commit
+ * for files, which were deleted on the file system, but not from the VCS,
+ * and proposes a dialog to select files which are to be removed from the VCS.
+ */
+public class HgRemoveCheckinHandler extends CheckinHandler {
+
+  private final CheckinProjectPanel myCheckinPanel;
+  private final Project myProject;
+
+  public HgRemoveCheckinHandler(CheckinProjectPanel checkinPanel) {
+    myCheckinPanel = checkinPanel;
+    myProject = checkinPanel.getProject();
+  }
+
+  @Override
+  public ReturnResult beforeCheckin(CommitExecutor executor, final PairConsumer<Object, Object> additionalDataConsumer) {
+    // find missing changes
+    final List<Change> missingChanges = new LinkedList<Change>();
+    for (Change c : myCheckinPanel.getSelectedChanges()) {
+      if (c.getFileStatus() == FileStatus.DELETED_FROM_FS) {
+        missingChanges.add(c);
+      }
+    }
+
+    if (missingChanges.isEmpty()) {
+      return ReturnResult.COMMIT;
+    }
+
+    // show a simple confirmation for 1 missing change, or more complex dialog for 2 or more changes
+    final Collection<Change> changesToRemove = new THashSet<Change>();
+    VcsShowConfirmationOption confirmation = ProjectLevelVcsManager.getInstance(myProject).getStandardConfirmation(VcsConfiguration.StandardConfirmation.REMOVE, HgVcs.getInstance(myProject));
+    if (missingChanges.size() == 1) {
+      if (ConfirmationDialog
+        .requestForConfirmation(confirmation, myProject,
+                                HgVcsMessages.message("hg4idea.remove.commit.single.body", missingChanges.get(0).getBeforeRevision().getFile().getPresentableUrl()),
+                                HgVcsMessages.message("hg4idea.remove.single.title"), Messages.getQuestionIcon())) {
+        changesToRemove.add(missingChanges.get(0));
+      }
+    } else {
+      final SelectMissingChangesDialog dialog = new SelectMissingChangesDialog(myProject, missingChanges, confirmation);
+      dialog.show();
+      if (dialog.isOK()) {
+        changesToRemove.addAll(dialog.getSelectedChanges());
+      } else {
+        return ReturnResult.CANCEL;
+      }
+    }
+
+    if (!changesToRemove.isEmpty()) {
+      removeChangesAndUpdateCommitted(changesToRemove, additionalDataConsumer);
+    }
+    return ReturnResult.COMMIT;
+  }
+
+  private void removeChangesAndUpdateCommitted(final Collection<Change> changesToRemove,
+                                               final PairConsumer<Object, Object> additionalDataConsumer) {
+    ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
+      public void run() {
+        final List<FilePath> filepathsToRemove = new ArrayList<FilePath>(changesToRemove.size());
+        for (Change c : changesToRemove) {
+          filepathsToRemove.add(c.getBeforeRevision().getFile());
+        }
+        HgUtil.removeFilesFromVcs(myProject, filepathsToRemove);
+        additionalDataConsumer.consume(HgVcs.getInstance(myProject), changesToRemove);
+      }
+    }, HgVcsMessages.message("hg4idea.remove.progress"), true, myProject);
+  }
+
+  private class SelectMissingChangesDialog extends AbstractSelectFilesDialog<Change> {
+
+    public SelectMissingChangesDialog(final Project project, List<Change> originalFiles, VcsShowConfirmationOption confirmation) {
+      super(project, false, confirmation, null);
+      myFileList = new ChangesTreeList<Change>(project, originalFiles, true, true, null, null) {
+        protected DefaultTreeModel buildTreeModel(final List<Change> changes, ChangeNodeDecorator changeNodeDecorator) {
+          return new TreeModelBuilder(project, false).buildModel(changes, changeNodeDecorator);
+        }
+
+        protected List<Change> getSelectedObjects(final ChangesBrowserNode node) {
+          return node.getAllChangesUnder();
+        }
+
+        protected Change getLeadSelectedObject(final ChangesBrowserNode node) {
+          final Object o = node.getUserObject();
+          if (o instanceof Change) {
+            return (Change) o;
+          }
+          return null;
+        }
+      };
+      myFileList.setChangesToDisplay(originalFiles);
+      myPanel.add(myFileList, BorderLayout.CENTER);
+      setOKButtonText(HgVcsMessages.message("hg4idea.remove.button.ok"));
+      setTitle(HgVcsMessages.message("hg4idea.remove.multiple.title"));
+      init();
+    }
+
+    public Collection<Change> getSelectedChanges() {
+      return myFileList.getIncludedChanges();
+    }
+
+    protected JComponent createTitlePane() {
+      return new TitlePanel(HgVcsMessages.message("hg4idea.remove.commit.multiple.title"), HgVcsMessages.message(
+        "hg4idea.remove.commit.multiple.description"));
+    }
+
+  }
+
+}
\ No newline at end of file
diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java
new file mode 100644 (file)
index 0000000..78f0ec9
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2010 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.zmlx.hg4idea;
+
+import com.intellij.openapi.vcs.CheckinProjectPanel;
+import com.intellij.openapi.vcs.checkin.CheckinHandler;
+import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * HgRemoveCheckinHandlerFactory provides the {@link CheckinHandler} which scans
+ * the changes list for files, which were deleted on the file system, but not from
+ * the VCS. 
+ *
+ * @author Kirill Likhodedov
+ */
+public class HgRemoveCheckinHandlerFactory extends CheckinHandlerFactory {
+
+  @NotNull
+  @Override
+  public CheckinHandler createHandler(final CheckinProjectPanel checkinPanel) {
+    return new HgRemoveCheckinHandler(checkinPanel);
+  }
+
+}
index 0b4d97770d2cd2886e28e085414127a0cbd0f834..52b77a2a0d7e86206b0f722c402caa7d7d44e634 100644 (file)
@@ -16,12 +16,15 @@ import com.intellij.openapi.application.*;
 import com.intellij.openapi.project.*;
 import com.intellij.openapi.util.ShutDownTracker;
 import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.VcsException;
 import com.intellij.openapi.vcs.changes.*;
 import com.intellij.openapi.vfs.*;
 import com.intellij.vcsUtil.*;
 import org.jetbrains.annotations.Nullable;
+import org.zmlx.hg4idea.command.HgRemoveCommand;
 
 import java.io.*;
+import java.util.List;
 
 /**
  * <strong><font color="#FF0000">TODO JavaDoc.</font></strong>
@@ -153,4 +156,22 @@ public abstract class HgUtil {
       return null;
     }
   }
+
+  /**
+   * Calls 'hg remove' to remove given files from the VCS.
+   * @param project
+   * @param files files to be removed from the VCS.
+   */
+  public static void removeFilesFromVcs(Project project, List<FilePath> files) {
+    final HgRemoveCommand command = new HgRemoveCommand(project);
+    for (FilePath filePath : files) {
+      final VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath);
+      if (vcsRoot == null) {
+        continue;
+      }
+      command.execute(new HgFile(vcsRoot, filePath));
+    }
+  }
+
+
 }
index 4568dbce2dc91f78e7968d7ea1c160f3b248eea3..cd54d7e5884f0906b3defbdb3afc9216ed4a2019 100644 (file)
@@ -85,7 +85,7 @@ public class HgVFSListener extends VcsVFSListener {
 
   @Override
   protected String getDeleteTitle() {
-    return HgVcsMessages.message("hg4idea.remove.title");
+    return HgVcsMessages.message("hg4idea.remove.multiple.title");
   }
 
   @Override
@@ -95,7 +95,7 @@ public class HgVFSListener extends VcsVFSListener {
 
   @Override
   protected String getSingleFileDeletePromptTemplate() {
-    return HgVcsMessages.message("hg4idea.remove.body");
+    return HgVcsMessages.message("hg4idea.remove.single.body");
   }
 
   @Override
index f1e0c624394df82e59ddab7dd2e26f91c8245307..698868ba2cb782f9c67ab65a6d379a7406a5a81c 100644 (file)
@@ -18,6 +18,7 @@ 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.FileStatus;
 import com.intellij.openapi.vcs.VcsException;
 import com.intellij.openapi.vcs.changes.Change;
 import com.intellij.openapi.vcs.changes.ChangeList;
@@ -33,16 +34,20 @@ import org.jetbrains.annotations.NotNull;
 import org.zmlx.hg4idea.HgFile;
 import org.zmlx.hg4idea.HgRevisionNumber;
 import org.zmlx.hg4idea.HgVcsMessages;
+import gnu.trove.THashSet;
+import org.zmlx.hg4idea.*;
+
 import org.zmlx.hg4idea.command.*;
 
 import java.util.*;
+import java.util.List;
 
 public class HgCheckinEnvironment implements CheckinEnvironment {
 
-  private final Project project;
+  private final Project myProject;
 
   public HgCheckinEnvironment(Project project) {
-    this.project = project;
+    this.myProject = project;
   }
 
   public RefreshableOnComponent createAdditionalOptionsPanel(CheckinProjectPanel panel,
@@ -64,25 +69,36 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
 
   @SuppressWarnings({"ThrowableInstanceNeverThrown"})
   public List<VcsException> commit(List<Change> changes, String preparedComment, @NotNull NullableFunction<Object, Object> parametersHolder) {
-    List<VcsException> exceptions = new LinkedList<VcsException>();
-    for (Map.Entry<VirtualFile, Set<HgFile>> entry : getFilesByRepository(changes).entrySet()) {
-
-      VirtualFile repo = entry.getKey();
-      Set<HgFile> selectedFiles = entry.getValue();
+    final List<VcsException> exceptions = new LinkedList<VcsException>();
+    final Collection<Change> removedChanges = (Collection<Change>) parametersHolder.fun(HgVcs.getInstance(myProject));
+
+    for (final Map.Entry<VirtualFile, List<Change>> entry : groupChangesByRepository(changes).entrySet()) {
+      // separate commit for each repository
+      final VirtualFile repo = entry.getKey();
+      final HgCommitCommand command = new HgCommitCommand(myProject, repo, preparedComment);
+
+      // commit files, except those which were deleted from filesystem, but not from the VCS.
+      // HgRemoveCheckinHandler proposes to remove such files from the VCS before commit.
+      // If some of those weren't removed, it was done intentionally, so just silently ignore them.
+      final Set<HgFile> selectedFiles = new THashSet<HgFile>();
+      for (Change c : entry.getValue()) {
+        if (c.getFileStatus() == FileStatus.DELETED_FROM_FS) {
+          if (removedChanges == null || !removedChanges.contains(c)) { // missing and not removed from vcs via the HgRemoveCheckinHandler
+            continue;
+          }
+        }
+        final FilePath filepath = (c.getAfterRevision() == null ? c.getBeforeRevision().getFile() : c.getAfterRevision().getFile());
+        selectedFiles.add(new HgFile(repo, filepath));
+      }
 
-      HgCommitCommand command = new HgCommitCommand(project, repo, preparedComment);
-      
       if (isMergeCommit(repo)) {
         //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, selectedFiles);
-        boolean partial = !changedFilesNotInCommit.isEmpty();
-
-
-        if (partial) {
+        final Set<HgFile> changedFilesNotInCommit = getChangedFilesNotInCommit(repo, selectedFiles);
+        if (!changedFilesNotInCommit.isEmpty()) {
           final StringBuilder filesNotIncludedString = new StringBuilder();
           for (HgFile hgFile : changedFilesNotInCommit) {
             filesNotIncludedString.append("<li>");
@@ -93,12 +109,17 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
             //abort
             return exceptions;
           }
+          // 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 {
+        if (selectedFiles.isEmpty()) {  // nothing to commit. Aborting here, because otherwise 'hg commit' without specifying files will commit all files.
+          return exceptions;
         }
-        // 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.execute();
       } catch (HgCommandException e) {
@@ -111,13 +132,13 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
   }
 
   private boolean isMergeCommit(VirtualFile repo) {
-    return new HgWorkingCopyRevisionsCommand(project).parents(repo).size() > 1;
+    return new HgWorkingCopyRevisionsCommand(myProject).parents(repo).size() > 1;
   }
 
   private Set<HgFile> getChangedFilesNotInCommit(VirtualFile repo, Set<HgFile> selectedFiles) {
-    List<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(project).parents(repo);
+    List<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo);
 
-    HgStatusCommand statusCommand = new HgStatusCommand(project);
+    HgStatusCommand statusCommand = new HgStatusCommand(myProject);
     statusCommand.setBaseRevision(parents.get(0));
     statusCommand.setIncludeUnknown(false);
     statusCommand.setIncludeIgnored(false);
@@ -142,7 +163,7 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
     Runnable runnable = new Runnable() {
       public void run() {
         choice[0] = Messages.showOkCancelDialog(
-          project, 
+          myProject,
           HgVcsMessages.message("hg4idea.commit.partial.merge.message", filesNotIncludedString),
           HgVcsMessages.message("hg4idea.commit.partial.merge.title"),
           null
@@ -163,21 +184,14 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
   }
 
   public List<VcsException> scheduleMissingFileForDeletion(List<FilePath> files) {
-    HgRemoveCommand command = new HgRemoveCommand(project);
-    for (FilePath filePath : files) {
-      VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath);
-      if (vcsRoot == null) {
-        continue;
-      }
-      command.execute(new HgFile(vcsRoot, filePath));
-    }
+    HgUtil.removeFilesFromVcs(myProject, files);
     return null;
   }
 
   public List<VcsException> scheduleUnversionedFilesForAddition(List<VirtualFile> files) {
-    HgAddCommand command = new HgAddCommand(project);
+    HgAddCommand command = new HgAddCommand(myProject);
     for (VirtualFile file : files) {
-      VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, file);
+      VirtualFile vcsRoot = VcsUtil.getVcsRootFor(myProject, file);
       if (vcsRoot == null) {
         continue;
       }
@@ -190,39 +204,32 @@ public class HgCheckinEnvironment implements CheckinEnvironment {
     return false;
   }
 
-  private Map<VirtualFile, Set<HgFile>> getFilesByRepository(List<Change> changes) {
-    Map<VirtualFile, Set<HgFile>> result = new HashMap<VirtualFile, Set<HgFile>>();
+  /**
+   * Groups the changes by repository roots.
+   * @param changes the list of all changes.
+   * @return Changes grouped by repository roots.
+   */
+  private Map<VirtualFile, List<Change>> groupChangesByRepository(List<Change> changes) {
+    final Map<VirtualFile, List<Change>> result = new HashMap<VirtualFile, List<Change>>();
     for (Change change : changes) {
-      ContentRevision afterRevision = change.getAfterRevision();
-      ContentRevision beforeRevision = change.getBeforeRevision();
+      final ContentRevision afterRevision = change.getAfterRevision();
+      final ContentRevision beforeRevision = change.getBeforeRevision();
+      assert beforeRevision != null || afterRevision != null; // nothing-to-nothing change cannot happen.
+      final FilePath filePath = (afterRevision != null) ? afterRevision.getFile() : beforeRevision.getFile();
 
-      if (afterRevision != null) {
-        addFile(result, afterRevision.getFile());
+      final VirtualFile repo = VcsUtil.getVcsRootFor(myProject, filePath);
+      if (repo == null || filePath.isDirectory()) {
+        continue;
       }
-      if (beforeRevision != null) {
-        addFile(result, beforeRevision.getFile());
+
+      List<Change> repoChanges = result.get(repo);
+      if (repoChanges == null) {
+        repoChanges = new ArrayList<Change>();
+        result.put(repo, repoChanges);
       }
+      repoChanges.add(change);
     }
     return result;
   }
 
-  private void addFile(Map<VirtualFile, Set<HgFile>> result, FilePath filePath) {
-    if (filePath == null) {
-      return;
-    }
-
-    VirtualFile repo = VcsUtil.getVcsRootFor(project, filePath);
-    if (repo == null || filePath.isDirectory()) {
-      return;
-    }
-
-    Set<HgFile> hgFiles = result.get(repo);
-    if (hgFiles == null) {
-      hgFiles = new HashSet<HgFile>();
-      result.put(repo, hgFiles);
-    }
-
-    hgFiles.add(new HgFile(repo, filePath));
-  }
-
 }