/* * 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.application.ModalityState; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.progress.impl.ProgressManagerImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ValidationInfo; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.ui.CheckedTreeNode; import com.intellij.ui.SimpleColoredText; import com.intellij.ui.SimpleTextAttributes; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.hash.HashMap; import com.intellij.vcs.log.VcsFullCommitDetails; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.tree.DefaultMutableTreeNode; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class PushController implements Disposable { @NotNull private final Project myProject; @NotNull private final List> 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 myAdditionalValuesMap; private final Map myView2Model = new TreeMap(); //todo need to sort repositories in ui tree using natural order public PushController(@NotNull Project project, @NotNull VcsPushDialog dialog, @NotNull List 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(); myDialog = dialog; myDialog.updateButtons(); startLoadingCommits(); Disposer.register(dialog.getDisposable(), this); selectFirstChecked(); } private void selectFirstChecked() { Map.Entry selected = ContainerUtil.find(myView2Model.entrySet(), new Condition>() { @Override public boolean value(Map.Entry 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 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 priorityLoading = new HashMap(); Map others = new HashMap(); for (Map.Entry 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 items) { for (Map.Entry 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 preselectedRepositories) { if (myPushSupports.isEmpty()) return true; int repoCount = 0; for (PushSupport support : myPushSupports) { repoCount += createNodesForVcs(support, rootNode, preselectedRepositories); } return repoCount == 1; } private int createNodesForVcs( @NotNull PushSupport pushSupport, @NotNull CheckedTreeNode rootNode, @NotNull List preselectedRepositories) { RepositoryManager repositoryManager = pushSupport.getRepositoryManager(); List repositories = repositoryManager.getRepositories(); for (R repository : repositories) { createRepoNode(pushSupport, repository, rootNode, preselectedRepositories.contains(repository), repositories.size() == 1); } return repositories.size(); } private void createRepoNode(@NotNull final PushSupport support, @NotNull final R repository, @NotNull CheckedTreeNode rootNode, boolean isSelected, boolean isSingleRepositoryProject) { T target = support.getDefaultTarget(repository); String repoName = DvcsUtil.getShortRepositoryName(repository); final MyRepoModel model = new MyRepoModel(repository, support, isSingleRepositoryProject || isSelected, support.getSource(repository), target, DEFAULT_CHILDREN_PRESENTATION_NUMBER); if (target == null) { model.setError(VcsError.createEmptyTargetError(repoName)); } RepositoryWithBranchPanel repoPanel = new RepositoryWithBranchPanel(myProject, repoName, support.getSource(repository).getPresentation(), target == null ? "" : target.getPresentation(), support.getTargetNames(repository)); final RepositoryNode repoNode = isSingleRepositoryProject ? new SingleRepositoryNode(repoPanel, support.renderTarget(target)) : new RepositoryNode(repoPanel, support.renderTarget(target)); myView2Model.put(repoNode, model); repoNode.setChecked(model.isSelected()); repoPanel.addRepoNodeListener(new RepositoryNodeListener() { @Override public void onTargetChanged(String newValue) { VcsError validationError = support.validate(model.getRepository(), newValue); if (validationError == null) { T newTarget = support.createTarget(repository, newValue); repoNode.setTargetPresentation(support.renderTarget(newTarget)); model.setTarget(newTarget); model.clearErrors(); loadCommits(model, repoNode, false); } else { repoNode.setTargetPresentation(StringUtil.isEmptyOrSpaces(newValue) ? support.renderTarget(null) : new SimpleColoredText(newValue, SimpleTextAttributes.ERROR_ATTRIBUTES)); model.setError(validationError); // todo may be should accept and store errors collection, now store one major target error model.setTarget(null); } myDialog.updateButtons(); } @Override public void onSelectionChanged(boolean isSelected) { model.setSelected(isSelected); repoNode.setChecked(isSelected); myDialog.updateButtons(); } }); rootNode.add(repoNode); } private void loadCommits(@NotNull final MyRepoModel 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 ProgressIndicator indicator = node.startLoading(); final PushSupport support = model.getSupport(); final AtomicReference result = new AtomicReference(); Task.Backgroundable task = new Task.Backgroundable(myProject, "Loading Commits", true) { @Override public void onCancel() { node.stopLoading(); } @Override public void onSuccess() { OutgoingResult outgoing = result.get(); List errors = outgoing.getErrors(); if (!errors.isEmpty()) { myPushLog.setChildren(node, ContainerUtil.map(errors, new Function() { @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()); } } @Override public void run(@NotNull ProgressIndicator indicator) { OutgoingResult outgoing = support.getOutgoingCommitsProvider() .getOutgoingCommits(model.getRepository(), new PushSpec(model.getSource(), model.getTarget()), initial); result.compareAndSet(null, outgoing); } }; ProgressManagerImpl.runProcessWithProgressAsynchronously(task, indicator, null, ModalityState.any()); } 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 void doPush(@NotNull PushSupport support, boolean force) { MyPushOptionValueModel additionalOptionsModel = myAdditionalValuesMap.get(support); VcsPushOptionValue options = additionalOptionsModel == null ? null : additionalOptionsModel.getCurrentValue(); Pusher pusher = support.getPusher(); pusher.push(collectPushSpecsForVcs(support), options, force); } @NotNull private Map> collectPushSpecsForVcs(@NotNull PushSupport pushSupport) { Map> pushSpecs = ContainerUtil.newHashMap(); Collection 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)repoModel.getSource(), target)); } } } return pushSpecs; } public Collection getSelectedRepoNode() { if (mySingleRepoProject) { return myView2Model.values(); } return ContainerUtil.filter(myView2Model.values(), new Condition() { @Override public boolean value(MyRepoModel model) { return model.isSelected(); } }); } @Override public void dispose() { for (RepositoryNode node : myView2Model.keySet()) { node.stopLoading(); } } private void addMoreCommits(RepositoryNode repositoryNode) { MyRepoModel repoModel = myView2Model.get(repositoryNode); repoModel.increaseShownCommits(); myPushLog.setChildren(repositoryNode, getPresentationForCommits( myProject, repoModel.getLoadedCommits(), repoModel.getNumberOfShownCommits() )); } @NotNull public List getPresentationForCommits(@NotNull final Project project, @NotNull List commits, int commitsNum) { Function commitToNode = new Function() { @Override public DefaultMutableTreeNode fun(VcsFullCommitDetails commit) { return new VcsFullCommitDetailsNode(project, commit); } }; List childrenToShown = new ArrayList(); for (int i = 0; i < commits.size(); ++i) { if (i >= commitsNum) { final VcsLinkedText moreCommitsLink = new VcsLinkedText("...", 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 getAdditionalPanels() { List additionalPanels = new ArrayList(); 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() { @Override public boolean value(MyRepoModel model) { return support.equals(model.getSupport()); } }); } private static class MyRepoModel { @NotNull final Repo myRepository; @NotNull private PushSupport mySupport; @NotNull private final S mySource; @Nullable private T myTarget; @Nullable VcsError myTargetError; int myNumberOfShownCommits; List myLoadedCommits; boolean myIsSelected; public MyRepoModel(@NotNull Repo repository, @NotNull PushSupport 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 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 getLoadedCommits() { return myLoadedCommits; } public void setLoadedCommits(List 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; } } }