blob: cd0cc7a1ef7a6279ae676aa39e6ce6a9501d3270 [file] [log] [blame]
/*
* Copyright 2000-2014 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.DvcsUtil;
import com.intellij.dvcs.push.ui.*;
import com.intellij.dvcs.repo.Repository;
import com.intellij.dvcs.repo.RepositoryManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.ui.popup.util.PopupUtil;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.VcsFullCommitDetails;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
public class PushController implements Disposable {
@NotNull private final Project myProject;
@NotNull private final List<PushSupport<? extends Repository, ? extends PushSource, ? extends PushTarget>> myPushSupports;
@NotNull private final PushLog myPushLog;
@NotNull private final VcsPushDialog myDialog;
private boolean mySingleRepoProject;
private static final int DEFAULT_CHILDREN_PRESENTATION_NUMBER = 20;
private final Map<PushSupport, MyPushOptionValueModel> myAdditionalValuesMap;
private final ExecutorService myExecutorService = Executors.newSingleThreadExecutor();
private final Map<RepositoryNode, MyRepoModel> myView2Model = new TreeMap<RepositoryNode, MyRepoModel>();
//todo need to sort repositories in ui tree using natural order
public PushController(@NotNull Project project,
@NotNull VcsPushDialog dialog,
@NotNull List<? extends Repository> preselectedRepositories) {
myProject = project;
//todo what would be in case of null
myPushSupports = Arrays.asList(Extensions.getExtensions(PushSupport.PUSH_SUPPORT_EP, myProject));
CheckedTreeNode rootNode = new CheckedTreeNode(null);
mySingleRepoProject = createTreeModel(rootNode, preselectedRepositories);
myPushLog = new PushLog(myProject, rootNode);
myAdditionalValuesMap = new HashMap<PushSupport, MyPushOptionValueModel>();
myDialog = dialog;
myDialog.updateButtons();
startLoadingCommits();
Disposer.register(dialog.getDisposable(), this);
selectFirstChecked();
}
private void selectFirstChecked() {
Map.Entry<RepositoryNode, MyRepoModel> selected =
ContainerUtil.find(myView2Model.entrySet(), new Condition<Map.Entry<RepositoryNode, MyRepoModel>>() {
@Override
public boolean value(Map.Entry<RepositoryNode, MyRepoModel> entry) {
return entry.getValue().isSelected();
}
});
if (selected != null) {
myPushLog.selectNode(selected.getKey());
}
}
@Nullable
public ValidationInfo validate() {
ValidationInfo validInfo = new ValidationInfo("There are no selected repository to push!");
for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
MyRepoModel model = entry.getValue();
if (model.isSelected()) {
if (model.hasError()) return new ValidationInfo(model.getError().getText());
validInfo = null;
}
}
return validInfo;
}
private void startLoadingCommits() {
//todo should be reworked
Map<RepositoryNode, MyRepoModel> priorityLoading = new HashMap<RepositoryNode, MyRepoModel>();
Map<RepositoryNode, MyRepoModel> others = new HashMap<RepositoryNode, MyRepoModel>();
for (Map.Entry<RepositoryNode, MyRepoModel> entry : myView2Model.entrySet()) {
MyRepoModel model = entry.getValue();
if (model.isSelected()) {
priorityLoading.put(entry.getKey(), model);
}
else {
others.put(entry.getKey(), model);
}
}
loadCommitsFromMap(priorityLoading);
loadCommitsFromMap(others);
}
private void loadCommitsFromMap(@NotNull Map<RepositoryNode, MyRepoModel> items) {
for (Map.Entry<RepositoryNode, MyRepoModel> entry : items.entrySet()) {
RepositoryNode node = entry.getKey();
loadCommits(entry.getValue(), node, true);
}
}
//return is single repository project or not
private boolean createTreeModel(@NotNull CheckedTreeNode rootNode, @NotNull List<? extends Repository> preselectedRepositories) {
if (myPushSupports.isEmpty()) return true;
int repoCount = 0;
for (PushSupport<? extends Repository, ? extends PushSource, ? extends PushTarget> support : myPushSupports) {
repoCount += createNodesForVcs(support, rootNode, preselectedRepositories);
}
return repoCount == 1;
}
private <R extends Repository, S extends PushSource, T extends PushTarget> int createNodesForVcs(
@NotNull PushSupport<R, S, T> pushSupport,
@NotNull CheckedTreeNode rootNode,
@NotNull List<? extends Repository> preselectedRepositories) {
RepositoryManager<R> repositoryManager = pushSupport.getRepositoryManager();
List<R> repositories = repositoryManager.getRepositories();
for (R repository : repositories) {
createRepoNode(pushSupport, repository, rootNode, preselectedRepositories.contains(repository), repositories.size() == 1);
}
return repositories.size();
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void createRepoNode(@NotNull final PushSupport<R, S, T> support,
@NotNull final R repository,
@NotNull CheckedTreeNode rootNode,
boolean isSelected,
boolean isSingleRepositoryProject) {
T target = support.getDefaultTarget(repository);
String repoName = DvcsUtil.getShortRepositoryName(repository);
final MyRepoModel<R, S, T> model = new MyRepoModel<R, S, T>(repository, support, isSingleRepositoryProject || isSelected,
support.getSource(repository), target,
DEFAULT_CHILDREN_PRESENTATION_NUMBER);
if (target == null) {
model.setError(VcsError.createEmptyTargetError(repoName));
}
final PushTargetPanel<T> pushTargetPanel = support.createTargetPanel(repository, target);
RepositoryWithBranchPanel<T> repoPanel =
new RepositoryWithBranchPanel<T>(repoName, support.getSource(repository).getPresentation(), pushTargetPanel);
repoPanel.setInputVerifier(new InputVerifier() {
@Override
public boolean verify(JComponent input) {
ValidationInfo error = pushTargetPanel.verify();
if (error != null) {
//noinspection ConstantConditions
PopupUtil.showBalloonForComponent(error.component, error.message, MessageType.WARNING, false, myProject);
}
return error == null;
}
});
final RepositoryNode repoNode = isSingleRepositoryProject
? new SingleRepositoryNode(repoPanel)
: new RepositoryNode(repoPanel);
myView2Model.put(repoNode, model);
repoNode.setChecked(model.isSelected());
repoPanel.addRepoNodeListener(new RepositoryNodeListener<T>() {
@Override
public void onTargetChanged(T newTarget) {
model.setTarget(newTarget);
model.clearErrors();
loadCommits(model, repoNode, false);
myDialog.updateButtons();
}
@Override
public void onSelectionChanged(boolean isSelected) {
model.setSelected(isSelected);
repoNode.setChecked(isSelected);
myDialog.updateButtons();
}
});
rootNode.add(repoNode);
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void loadCommits(@NotNull final MyRepoModel<R, S, T> model,
@NotNull final RepositoryNode node,
final boolean initial) {
node.stopLoading();
final T target = model.getTarget();
if (target == null) return; //todo should be removed when commit loader executor will be modified
myPushLog.startLoading(node);
final PushSupport<R, S, T> support = model.getSupport();
final AtomicReference<OutgoingResult> result = new AtomicReference<OutgoingResult>();
Runnable task = new Runnable() {
@Override
public void run() {
OutgoingResult outgoing = support.getOutgoingCommitsProvider()
.getOutgoingCommits(model.getRepository(), new PushSpec<S, T>(model.getSource(), model.getTarget()), initial);
result.compareAndSet(null, outgoing);
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
OutgoingResult outgoing = result.get();
List<VcsError> errors = outgoing.getErrors();
if (!errors.isEmpty()) {
myPushLog.setChildren(node, ContainerUtil.map(errors, new Function<VcsError, DefaultMutableTreeNode>() {
@Override
public DefaultMutableTreeNode fun(final VcsError error) {
VcsLinkedText errorLinkText = new VcsLinkedText(error.getText(), new VcsLinkListener() {
@Override
public void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode) {
error.handleError(new CommitLoader() {
@Override
public void reloadCommits() {
loadCommits(model, node, false);
}
});
}
});
return new TextWithLinkNode(errorLinkText);
}
}), model.isSelected());
}
else {
model.setLoadedCommits(outgoing.getCommits());
myPushLog.setChildren(node,
getPresentationForCommits(PushController.this.myProject, model.getLoadedCommits(),
model.getNumberOfShownCommits()), model.isSelected());
}
}
});
}
};
node.startLoading(myExecutorService.submit(task, result));
}
public PushLog getPushPanelLog() {
return myPushLog;
}
public void push(final boolean force) {
Task.Backgroundable task = new Task.Backgroundable(myProject, "Pushing...", false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
for (PushSupport support : myPushSupports) {
doPush(support, force);
}
}
};
task.queue();
}
private <R extends Repository, S extends PushSource, T extends PushTarget> void doPush(@NotNull PushSupport<R, S, T> support,
boolean force) {
MyPushOptionValueModel additionalOptionsModel = myAdditionalValuesMap.get(support);
VcsPushOptionValue options = additionalOptionsModel == null ? null : additionalOptionsModel.getCurrentValue();
Pusher<R, S, T> pusher = support.getPusher();
pusher.push(collectPushSpecsForVcs(support), options, force);
}
@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();
Collection<MyRepoModel> repositoriesInformation = getSelectedRepoNode();
for (MyRepoModel repoModel : repositoriesInformation) {
if (pushSupport.equals(repoModel.getSupport())) {
//todo improve generics: unchecked casts
T target = (T)repoModel.getTarget();
if (target != null) {
pushSpecs.put((R)repoModel.getRepository(), new PushSpec<S, T>((S)repoModel.getSource(), target));
}
}
}
return pushSpecs;
}
public Collection<MyRepoModel> getSelectedRepoNode() {
if (mySingleRepoProject) {
return myView2Model.values();
}
return ContainerUtil.filter(myView2Model.values(), new Condition<MyRepoModel>() {
@Override
public boolean value(MyRepoModel model) {
return model.isSelected();
}
});
}
@Override
public void dispose() {
myExecutorService.shutdownNow();
}
private void addMoreCommits(RepositoryNode repositoryNode) {
MyRepoModel repoModel = myView2Model.get(repositoryNode);
repoModel.increaseShownCommits();
myPushLog.setChildren(repositoryNode,
getPresentationForCommits(
myProject,
repoModel.getLoadedCommits(),
repoModel.getNumberOfShownCommits()
));
}
@NotNull
public List<DefaultMutableTreeNode> getPresentationForCommits(@NotNull final Project project,
@NotNull List<? extends VcsFullCommitDetails> commits,
int commitsNum) {
Function<VcsFullCommitDetails, DefaultMutableTreeNode> commitToNode = new Function<VcsFullCommitDetails, DefaultMutableTreeNode>() {
@Override
public DefaultMutableTreeNode fun(VcsFullCommitDetails commit) {
return new VcsFullCommitDetailsNode(project, commit);
}
};
List<DefaultMutableTreeNode> childrenToShown = new ArrayList<DefaultMutableTreeNode>();
for (int i = 0; i < commits.size(); ++i) {
if (i >= commitsNum) {
final VcsLinkedText moreCommitsLink = new VcsLinkedText("<a href='loadMore'>...</a>", new VcsLinkListener() {
@Override
public void hyperlinkActivated(@NotNull DefaultMutableTreeNode sourceNode) {
addMoreCommits((RepositoryNode)sourceNode);
}
});
childrenToShown.add(new TextWithLinkNode(moreCommitsLink));
break;
}
childrenToShown.add(commitToNode.fun(commits.get(i)));
}
return childrenToShown;
}
@NotNull
public List<VcsPushOptionsPanel> getAdditionalPanels() {
List<VcsPushOptionsPanel> additionalPanels = new ArrayList<VcsPushOptionsPanel>();
for (final PushSupport support : myPushSupports) {
if (hasRepoForPushSupport(support)) {
final VcsPushOptionsPanel panel = support.getVcsPushOptionsPanel();
if (panel != null) {
additionalPanels.add(panel);
myAdditionalValuesMap.put(support, new MyPushOptionValueModel(panel.getValue()));
panel.addValueChangeListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
myAdditionalValuesMap.get(support).setCurrentValue(panel.getValue());
}
});
}
}
}
return additionalPanels;
}
private boolean hasRepoForPushSupport(@NotNull final PushSupport support) {
return ContainerUtil.exists(myView2Model.values(), new Condition<MyRepoModel>() {
@Override
public boolean value(MyRepoModel model) {
return support.equals(model.getSupport());
}
});
}
private static class MyRepoModel<Repo extends Repository, S extends PushSource, T extends PushTarget> {
@NotNull final Repo myRepository;
@NotNull private PushSupport<Repo, S, T> mySupport;
@NotNull private final S mySource;
@Nullable private T myTarget;
@Nullable VcsError myTargetError;
int myNumberOfShownCommits;
List<? extends VcsFullCommitDetails> myLoadedCommits;
boolean myIsSelected;
public MyRepoModel(@NotNull Repo repository,
@NotNull PushSupport<Repo, S, T> supportForRepo,
boolean isSelected, @NotNull S source, @Nullable T target,
int num) {
myRepository = repository;
mySupport = supportForRepo;
myIsSelected = isSelected;
mySource = source;
myTarget = target;
myNumberOfShownCommits = num;
}
@NotNull
public Repo getRepository() {
return myRepository;
}
@NotNull
public PushSupport<Repo, S, T> getSupport() {
return mySupport;
}
@NotNull
public S getSource() {
return mySource;
}
@Nullable
public T getTarget() {
return myTarget;
}
public void setTarget(@Nullable T target) {
myTarget = target;
}
public boolean isSelected() {
return myIsSelected;
}
public AbstractVcs<?> getVcs() {
return myRepository.getVcs();
}
@Nullable
public VcsError getError() {
return myTargetError;
}
public void setError(@Nullable VcsError error) {
myTargetError = error;
}
public void clearErrors() {
myTargetError = null;
}
public boolean hasError() {
return myTargetError != null;
}
public void setSelected(boolean isSelected) {
myIsSelected = isSelected;
}
public int getNumberOfShownCommits() {
return myNumberOfShownCommits;
}
public void increaseShownCommits() {
myNumberOfShownCommits *= 2;
}
public List<? extends VcsFullCommitDetails> getLoadedCommits() {
return myLoadedCommits;
}
public void setLoadedCommits(List<? extends VcsFullCommitDetails> loadedCommits) {
myLoadedCommits = loadedCommits;
}
}
private static class MyPushOptionValueModel {
@NotNull private VcsPushOptionValue myCurrentValue;
public MyPushOptionValueModel(@NotNull VcsPushOptionValue currentValue) {
myCurrentValue = currentValue;
}
public void setCurrentValue(@NotNull VcsPushOptionValue currentValue) {
myCurrentValue = currentValue;
}
@NotNull
public VcsPushOptionValue getCurrentValue() {
return myCurrentValue;
}
}
}