diff --git a/platform/dvcs-impl/src/META-INF/dvcs.xml b/platform/dvcs-impl/src/META-INF/dvcs.xml index 6a6de37de05a..0cd11c6abb9f 100644 --- a/platform/dvcs-impl/src/META-INF/dvcs.xml +++ b/platform/dvcs-impl/src/META-INF/dvcs.xml @@ -5,6 +5,9 @@ + + + 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 index 000000000000..b0096db7675b --- /dev/null +++ b/platform/dvcs-impl/src/com/intellij/dvcs/push/PrePushHandler.java @@ -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 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 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 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 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