[push]: add pre-push handlers that can be run on changes before the push

* 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;
This commit is contained in:
Sergey Patrikeev
2017-02-01 17:16:11 +03:00
committed by Nadya Zabrodina
parent ca0bd80263
commit 31c84aa902
5 changed files with 343 additions and 7 deletions
+3
View 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"/>
@@ -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);
}
@@ -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;
@@ -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();
}
@@ -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<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