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
+ *
+ * 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 pushDetails, @NotNull ProgressIndicator indicator);
+
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
index 27ea806bbe44..c651e6cee30d 100644
--- a/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushController.java
@@ -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 myExcludedRepositoryRoots;
@Nullable private final Repository myCurrentlyOpenedRepository;
+ private final List 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 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 List extends VcsFullCommitDetails> loadCommits(@NotNull MyRepoModel model) {
+ PushSupport support = model.getSupport();
+ R repository = model.getRepository();
+ S source = model.getSource();
+ T target = model.getTarget();
+ if (target == null) {
+ return ContainerUtil.emptyList();
+ }
+ OutgoingCommitsProvider outgoingCommitsProvider = support.getOutgoingCommitsProvider();
+ return outgoingCommitsProvider.getOutgoingCommits(repository, new PushSpec<>(source, target), true).getCommits();
+ }
+
+ @NotNull
+ private List preparePushDetails() {
+ List allDetails = ContainerUtil.newArrayList();
+ Collection> repoModels = getSelectedRepoNode();
+
+ for (MyRepoModel, ?, ?> model : repoModels) {
+ PushTarget target = model.getTarget();
+ if (target == null) {
+ continue;
+ }
+ PushSpec pushSpec = new PushSpec<>(model.getSource(), target);
+
+ List 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 Map> collectPushSpecsForVcs(@NotNull PushSupport pushSupport) {
Map> 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 myPushSpec;
+ private final List myCommits;
+
+ private PushDetailImpl(@NotNull Repository repository,
+ @NotNull PushSpec spec,
+ @NotNull List commits) {
+ myRepository = repository;
+ myPushSpec = spec;
+ myCommits = commits;
+ }
+
+ @NotNull
+ @Override
+ public Repository repository() {
+ return myRepository;
+ }
+
+ @NotNull
+ @Override
+ public PushSpec pushSpec() {
+ return myPushSpec;
+ }
+
+ @NotNull
+ @Override
+ public List commits() {
+ return myCommits;
+ }
+ }
+
private static class MyRepoModel {
@NotNull private final Repo myRepository;
@NotNull private final PushSupport 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
index 000000000000..471c08b9cf1e
--- /dev/null
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/PushDetail.java
@@ -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 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 commits();
+}
diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java
index 2249f0fe23d1..2b9fa61e80bc 100644
--- a/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java
+++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/ui/VcsPushDialog.java
@@ -16,20 +16,27 @@
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 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