mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
Nadya Zabrodina
parent
ca0bd80263
commit
31c84aa902
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user