[push]: add pre-push handlers that can be run on changes before the push
authorSergey Patrikeev <sergey.patrikeev@jetbrains.com>
Fri, 20 Jan 2017 10:57:58 +0000 (13:57 +0300)
committerNadya Zabrodina <Nadya.Zabrodina@jetbrains.com>
Wed, 1 Feb 2017 14:16:11 +0000 (17:16 +0300)
* add an extension point for pre-push handlers;
* catch handler exceptions and show a message if something went wrong
 during pre-push checks;
* update progress indicator fraction according to each handler progress
 state;
* handle user cancel action by suggesting to push anyway or cancel;

platform/dvcs-impl/src/META-INF/dvcs.xml
platform/dvcs-impl/src/com/intellij/dvcs/push/PrePushHandler.java [new file with mode: 0644]
platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
platform/dvcs-impl/src/com/intellij/dvcs/push/PushDetail.java [new file with mode: 0644]
platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java

index 6a6de37de05ab00bdf88a6163ea0df5dc9bdd2f3..0cd11c6abb9f66731123dc8e12bde8b930f2abf0 100644 (file)
@@ -5,6 +5,9 @@
     <extensionPoint name="cherryPicker"
                     interface="com.intellij.dvcs.cherrypick.VcsCherryPicker" area="IDEA_PROJECT"/>
     <extensionPoint name="vcsRepositoryCreator" interface="com.intellij.dvcs.repo.VcsRepositoryCreator" area="IDEA_PROJECT"/>
+
+    <extensionPoint name="prePushHandler" interface="com.intellij.dvcs.push.PrePushHandler" area="IDEA_PROJECT"/>
+
   </extensionPoints>
   <actions>
     <action id="Vcs.CherryPick" class="com.intellij.dvcs.cherrypick.VcsCherryPickAction" icon="DvcsImplIcons.CherryPick"/>
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/PrePushHandler.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/PrePushHandler.java
new file mode 100644 (file)
index 0000000..b0096db
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2000-2017 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 com.intellij.dvcs.push;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.progress.ProgressIndicator;
+import org.jetbrains.annotations.CalledInAny;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Interface for any checkers that should be called right before push operation started.
+ * All implemented handlers will be called on a background thread one by one (in unspecified order)
+ * with cancelable progress indicator.
+ */
+public interface PrePushHandler {
+  ExtensionPointName<PrePushHandler> EP_NAME = ExtensionPointName.create("com.intellij.prePushHandler");
+
+  /**
+   * Handler's decision of whether a push must be performed or canceled
+   */
+  enum Result {
+    /**
+     * Push is allowed.
+     */
+    OK,
+    /**
+     * Push is not allowed. The Push Dialog won't be closed.
+     */
+    ABORT,
+    /**
+     * Push is not allowed. The Push Dialog will be closed immediately.
+     */
+    ABORT_AND_CLOSE
+  }
+
+  /**
+   * Presentable name used in dialogs, UI, etc
+   *
+   * @return presentable name of this handler
+   */
+  @NotNull
+  @Nls(capitalization = Nls.Capitalization.Title)
+  String getPresentableName();
+
+  /**
+   * Check synchronously if the push operation should be performed or canceled for specified {@link PushDetail}s
+   * <p>
+   * Note: it is permissible for a handler to show it's own modal dialogs with specifying
+   * the supplied {@code indicator}'s {@link ProgressIndicator#getModalityState() modality} state.
+   *
+   * @param pushDetails information about repository, source and target branches, and commits to be pushed
+   * @param indicator progress indicator to cancel this handler if necessary
+   * @return handler's decision on whether the push must be performed or canceled
+   */
+  @CalledInAny
+  @NotNull
+  Result handle(@NotNull List<PushDetail> pushDetails, @NotNull ProgressIndicator indicator);
+
+}
index 27ea806bbe4452d85964ecc2b39a04a0b29a6513..c651e6cee30d2bd3d93182d2fbaa08cf6f9765ff 100644 (file)
@@ -21,8 +21,10 @@ import com.intellij.dvcs.push.ui.*;
 import com.intellij.dvcs.repo.Repository;
 import com.intellij.dvcs.repo.VcsRepositoryManager;
 import com.intellij.dvcs.ui.DvcsBundle;
+import com.intellij.ide.util.DelegatingProgressIndicator;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.progress.ProcessCanceledException;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.Task;
 import com.intellij.openapi.project.Project;
@@ -39,6 +41,7 @@ import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.ui.UIUtil;
 import com.intellij.vcs.log.VcsFullCommitDetails;
 import com.intellij.xml.util.XmlStringUtil;
+import org.jetbrains.annotations.CalledInAny;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -52,6 +55,7 @@ import java.beans.PropertyChangeListener;
 import java.io.File;
 import java.util.*;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static com.intellij.openapi.ui.Messages.OK;
@@ -67,6 +71,7 @@ public class PushController implements Disposable {
   @NotNull private final PushSettings myPushSettings;
   @NotNull private final Set<String> myExcludedRepositoryRoots;
   @Nullable private final Repository myCurrentlyOpenedRepository;
+  private final List<PrePushHandler> myHandlers = ContainerUtil.newArrayList();
   private final boolean mySingleRepoProject;
   private static final int DEFAULT_CHILDREN_PRESENTATION_NUMBER = 20;
   private final ExecutorService myExecutorService = ConcurrencyUtil.newSingleThreadExecutor("DVCS Push");
@@ -78,6 +83,7 @@ public class PushController implements Disposable {
                         @NotNull List<? extends Repository> preselectedRepositories, @Nullable Repository currentRepo) {
     myProject = project;
     myPushSettings = ServiceManager.getService(project, PushSettings.class);
+    ContainerUtil.addAll(myHandlers, PrePushHandler.EP_NAME.getExtensions(project));
     myGlobalRepositoryManager = VcsRepositoryManager.getInstance(project);
     myExcludedRepositoryRoots = ContainerUtil.newHashSet(myPushSettings.getExcludedRepoRoots());
     myPreselectedRepositories = preselectedRepositories;
@@ -479,6 +485,73 @@ public class PushController implements Disposable {
     return myPushLog;
   }
 
+  public static class HandlerException extends RuntimeException {
+
+    private final String myHandlerName;
+
+    public HandlerException(@NotNull String name, @NotNull Throwable cause) {
+      super(cause);
+      myHandlerName = name;
+    }
+
+    @NotNull
+    public String getHandlerName() {
+      return myHandlerName;
+    }
+  }
+
+  private static class StepsProgressIndicator extends DelegatingProgressIndicator {
+    private final int myTotalSteps;
+    private final AtomicInteger myFinishedTasks = new AtomicInteger();
+
+    public StepsProgressIndicator(@NotNull ProgressIndicator indicator, int totalSteps) {
+      super(indicator);
+      myTotalSteps = totalSteps;
+    }
+
+    public void nextStep() {
+      myFinishedTasks.incrementAndGet();
+      setFraction(0);
+    }
+
+    @Override
+    public void setFraction(double fraction) {
+      super.setFraction((myFinishedTasks.get() + fraction) / (double) myTotalSteps);
+    }
+  }
+
+  @NotNull
+  @CalledInAny
+  public PrePushHandler.Result executeHandlers(@NotNull ProgressIndicator indicator) throws ProcessCanceledException, HandlerException {
+    if (myHandlers.isEmpty()) return PrePushHandler.Result.OK;
+    List<PushDetail> pushDetails = preparePushDetails();
+    StepsProgressIndicator stepsIndicator = new StepsProgressIndicator(indicator, myHandlers.size());
+    stepsIndicator.setIndeterminate(false);
+    stepsIndicator.setFraction(0);
+    for (PrePushHandler handler : myHandlers) {
+      stepsIndicator.checkCanceled();
+      stepsIndicator.setText(handler.getPresentableName());
+      PrePushHandler.Result prePushHandlerResult;
+      try {
+        prePushHandlerResult = handler.handle(pushDetails, stepsIndicator);
+      }
+      catch (ProcessCanceledException pce) {
+        throw pce;
+      }
+      catch (Throwable e) {
+        throw new HandlerException(handler.getPresentableName(), e);
+      }
+
+      if (prePushHandlerResult != PrePushHandler.Result.OK) {
+        return prePushHandlerResult;
+      }
+      //the handler could change an indeterminate flag
+      stepsIndicator.setIndeterminate(false);
+      stepsIndicator.nextStep();
+    }
+    return PrePushHandler.Result.OK;
+  }
+
   public void push(final boolean force) {
     Task.Backgroundable task = new Task.Backgroundable(myProject, "Pushing...", true) {
       @Override
@@ -502,6 +575,44 @@ public class PushController implements Disposable {
     }
   }
 
+  private static <R extends Repository, S extends PushSource, T extends PushTarget> List<? extends VcsFullCommitDetails> loadCommits(@NotNull MyRepoModel<R, S, T> model) {
+    PushSupport<R, S, T> support = model.getSupport();
+    R repository = model.getRepository();
+    S source = model.getSource();
+    T target = model.getTarget();
+    if (target == null) {
+      return ContainerUtil.emptyList();
+    }
+    OutgoingCommitsProvider<R, S, T> outgoingCommitsProvider = support.getOutgoingCommitsProvider();
+    return outgoingCommitsProvider.getOutgoingCommits(repository, new PushSpec<>(source, target), true).getCommits();
+  }
+
+  @NotNull
+  private List<PushDetail> preparePushDetails() {
+    List<PushDetail> allDetails = ContainerUtil.newArrayList();
+    Collection<MyRepoModel<?, ?, ?>> repoModels = getSelectedRepoNode();
+
+    for (MyRepoModel<?, ?, ?> model : repoModels) {
+      PushTarget target = model.getTarget();
+      if (target == null) {
+        continue;
+      }
+      PushSpec<PushSource, PushTarget> pushSpec = new PushSpec<>(model.getSource(), target);
+
+      List<VcsFullCommitDetails> loadedCommits = ContainerUtil.newArrayList();
+      loadedCommits.addAll(model.getLoadedCommits());
+      if (loadedCommits.isEmpty()) {
+        //Note: loadCommits is cancellable - it tracks current thread's progress indicator under the hood!
+        loadedCommits.addAll(loadCommits(model));
+      }
+
+      //sort commits in the time-ascending order
+      Collections.reverse(loadedCommits);
+      allDetails.add(new PushDetailImpl(model.getRepository(), pushSpec, loadedCommits));
+    }
+    return Collections.unmodifiableList(allDetails);
+  }
+
   @NotNull
   private <R extends Repository, S extends PushSource, T extends PushTarget> Map<R, PushSpec<S, T>> collectPushSpecsForVcs(@NotNull PushSupport<R, S, T> pushSupport) {
     Map<R, PushSpec<S, T>> pushSpecs = ContainerUtil.newHashMap();
@@ -625,6 +736,39 @@ public class PushController implements Disposable {
     }) ? commonTarget : null;
   }
 
+  private static class PushDetailImpl implements PushDetail {
+
+    private final Repository myRepository;
+    private final PushSpec<PushSource, PushTarget> myPushSpec;
+    private final List<VcsFullCommitDetails> myCommits;
+
+    private PushDetailImpl(@NotNull Repository repository,
+                           @NotNull PushSpec<PushSource, PushTarget> spec,
+                           @NotNull List<VcsFullCommitDetails> commits) {
+      myRepository = repository;
+      myPushSpec = spec;
+      myCommits = commits;
+    }
+
+    @NotNull
+    @Override
+    public Repository repository() {
+      return myRepository;
+    }
+
+    @NotNull
+    @Override
+    public PushSpec<PushSource, PushTarget> pushSpec() {
+      return myPushSpec;
+    }
+
+    @NotNull
+    @Override
+    public List<VcsFullCommitDetails> commits() {
+      return myCommits;
+    }
+  }
+
   private static class MyRepoModel<Repo extends Repository, S extends PushSource, T extends PushTarget> {
     @NotNull private final Repo myRepository;
     @NotNull private final PushSupport<Repo, S, T> mySupport;
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/PushDetail.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushDetail.java
new file mode 100644 (file)
index 0000000..471c08b
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2017 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 com.intellij.dvcs.push;
+
+import com.intellij.dvcs.repo.Repository;
+import com.intellij.vcs.log.VcsFullCommitDetails;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Upcoming push information holder
+ */
+public interface PushDetail {
+  /**
+   * Repository of the push source
+   */
+  @NotNull
+  Repository repository();
+
+  /**
+   * For a {@link #repository()} specifies what would be pushed and where
+   *
+   * @return push specification
+   */
+  @NotNull
+  PushSpec<PushSource, PushTarget> pushSpec();
+
+  /**
+   * Returns list of commits to be pushed.
+   * a.e. result of `git log source..target` for updated git branches; empty list for newly created branches.
+   * Commits should be ordered by commit time (ex: committer time for git);
+   */
+  @NotNull
+  List<VcsFullCommitDetails> commits();
+}
index 2249f0fe23d1262ed4f83c50d1525cf3c86b7115..2b9fa61e80bc4d06ff9b8ad2f358ce240019730c 100644 (file)
 package com.intellij.dvcs.push.ui;
 
 import com.intellij.dvcs.push.*;
+import com.intellij.dvcs.push.PrePushHandler;
 import com.intellij.dvcs.repo.Repository;
 import com.intellij.ide.actions.ShowSettingsUtilImpl;
 import com.intellij.openapi.actionSystem.AnAction;
 import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
 import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
 import com.intellij.openapi.ui.OptionAction;
 import com.intellij.openapi.ui.ValidationInfo;
 import com.intellij.openapi.util.registry.Registry;
 import com.intellij.ui.components.labels.ActionLink;
 import com.intellij.util.ui.JBUI;
+import com.intellij.util.ui.UIUtil;
 import com.intellij.util.ui.components.BorderLayoutPanel;
 import net.miginfocom.swing.MigLayout;
+import org.jetbrains.annotations.CalledInAwt;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -38,6 +45,7 @@ import java.awt.event.ActionEvent;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 public class VcsPushDialog extends DialogWrapper {
 
@@ -54,7 +62,6 @@ public class VcsPushDialog extends DialogWrapper {
                        @NotNull List<? extends Repository> selectedRepositories,
                        @Nullable Repository currentRepo) {
     super(project, true, (Registry.is("ide.perProjectModality")) ? IdeModalityType.PROJECT : IdeModalityType.IDE);
-
     myController = new PushController(project, this, selectedRepositories, currentRepo);
     myAdditionalPanels = myController.createAdditionalPanels();
     myListPanel = myController.getPushPanelLog();
@@ -122,8 +129,7 @@ public class VcsPushDialog extends DialogWrapper {
 
   @Override
   protected void doOKAction() {
-    myController.push(false);
-    close(OK_EXIT_CODE);
+    push(false);
   }
 
   @Override
@@ -171,6 +177,67 @@ public class VcsPushDialog extends DialogWrapper {
     return ID;
   }
 
+  @CalledInAwt
+  private void push(boolean forcePush) {
+    FileDocumentManager.getInstance().saveAllDocuments();
+    AtomicReference<PrePushHandler.Result> result = new AtomicReference<>(PrePushHandler.Result.OK);
+    new Task.Modal(myController.getProject(), "Checking Commits...", true) {
+      @Override
+      public void run(@NotNull ProgressIndicator indicator) {
+        result.set(myController.executeHandlers(indicator));
+      }
+
+      @Override
+      public void onSuccess() {
+        super.onSuccess();
+        if (result.get() == PrePushHandler.Result.OK) {
+          doPush();
+        }
+        else if (result.get() == PrePushHandler.Result.ABORT_AND_CLOSE) {
+          doCancelAction();
+        }
+        else if (result.get() == PrePushHandler.Result.ABORT) {
+          // cancel push and leave the push dialog open
+        }
+      }
+
+      private void doPush() {
+        myController.push(forcePush);
+        close(OK_EXIT_CODE);
+      }
+
+      @Override
+      public void onThrowable(@NotNull Throwable error) {
+        if (error instanceof PushController.HandlerException) {
+          super.onThrowable(error.getCause());
+
+          String handlerName = ((PushController.HandlerException)error).getHandlerName();
+          suggestToSkipOrPush(handlerName + " has failed. See log for more details.\n" +
+                              "Would you like to skip pre-push checking and continue or cancel push completely?");
+        } else {
+          super.onThrowable(error);
+        }
+      }
+
+      @Override
+      public void onCancel() {
+        super.onCancel();
+        suggestToSkipOrPush("Would you like to skip pre-push checking and continue or cancel push completely?");
+      }
+
+      private void suggestToSkipOrPush(@NotNull String message) {
+        if (Messages.showOkCancelDialog(myProject,
+                                        message,
+                                        "Push",
+                                        "&Push Anyway",
+                                        "&Cancel",
+                                        UIUtil.getWarningIcon()) == Messages.OK) {
+          doPush();
+        }
+      }
+    }.queue();
+  }
+
   public void updateOkActions() {
     myPushAction.setEnabled(canPush());
     if (myForcePushAction != null) {
@@ -205,8 +272,7 @@ public class VcsPushDialog extends DialogWrapper {
     @Override
     public void actionPerformed(ActionEvent e) {
       if (myController.ensureForcePushIsNeeded()) {
-        myController.push(true);
-        close(OK_EXIT_CODE);
+        push(true);
       }
     }
   }
@@ -221,8 +287,7 @@ public class VcsPushDialog extends DialogWrapper {
 
     @Override
     public void actionPerformed(ActionEvent e) {
-      myController.push(false);
-      close(OK_EXIT_CODE);
+      push(false);
     }
 
     @Override