/*
* Copyright 2000-2016 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.openapi.vcs.update;
import com.intellij.history.Label;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.errorTreeView.HotfixData;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.progress.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.actions.AbstractVcsAction;
import com.intellij.openapi.vcs.actions.DescindingFilesFilter;
import com.intellij.openapi.vcs.actions.VcsContext;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.committed.CommittedChangesCache;
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.util.Consumer;
import com.intellij.util.WaitForProgressToShow;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.OptionsDialog;
import com.intellij.vcsUtil.VcsUtil;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.*;
public abstract class AbstractCommonUpdateAction extends AbstractVcsAction {
private final boolean myAlwaysVisible;
private final static Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.update.AbstractCommonUpdateAction");
private final ActionInfo myActionInfo;
private final ScopeInfo myScopeInfo;
protected AbstractCommonUpdateAction(ActionInfo actionInfo, ScopeInfo scopeInfo, boolean alwaysVisible) {
myActionInfo = actionInfo;
myScopeInfo = scopeInfo;
myAlwaysVisible = alwaysVisible;
}
private String getCompleteActionName(VcsContext dataContext) {
return myActionInfo.getActionName(myScopeInfo.getScopeName(dataContext, myActionInfo));
}
@Override
protected void actionPerformed(@NotNull final VcsContext context) {
final Project project = context.getProject();
boolean showUpdateOptions = myActionInfo.showOptions(project);
LOG.debug(String.format("project: %s, show update options: %s", project, showUpdateOptions));
if (project != null) {
try {
final FilePath[] filePaths = myScopeInfo.getRoots(context, myActionInfo);
final FilePath[] roots = DescindingFilesFilter.filterDescindingFiles(filterRoots(filePaths, context), project);
if (roots.length == 0) {
LOG.debug("No roots found.");
return;
}
final Map<AbstractVcs, Collection<FilePath>> vcsToVirtualFiles = createVcsToFilesMap(roots, project);
for (AbstractVcs vcs : vcsToVirtualFiles.keySet()) {
final UpdateEnvironment updateEnvironment = myActionInfo.getEnvironment(vcs);
if ((updateEnvironment != null) && (! updateEnvironment.validateOptions(vcsToVirtualFiles.get(vcs)))) {
// messages already shown
LOG.debug("Options not valid for files: " + vcsToVirtualFiles);
return;
}
}
if (showUpdateOptions || OptionsDialog.shiftIsPressed(context.getModifiers())) {
showOptionsDialog(vcsToVirtualFiles, project, context);
}
if (ApplicationManager.getApplication().isDispatchThread()) {
ApplicationManager.getApplication().saveAll();
}
Task.Backgroundable task = new Updater(project, roots, vcsToVirtualFiles);
if (ApplicationManager.getApplication().isUnitTestMode()) {
task.run(new EmptyProgressIndicator());
}
else {
ProgressManager.getInstance().run(task);
}
}
catch (ProcessCanceledException ignored) {
}
}
}
private boolean canGroupByChangelist(final Set<AbstractVcs> abstractVcses) {
if (myActionInfo.canGroupByChangelist()) {
for(AbstractVcs vcs: abstractVcses) {
if (vcs.getCachingCommittedChangesProvider() != null) {
return true;
}
}
}
return false;
}
private static boolean someSessionWasCanceled(List<UpdateSession> updateSessions) {
for (UpdateSession updateSession : updateSessions) {
if (updateSession.isCanceled()) {
return true;
}
}
return false;
}
private static String getAllFilesAreUpToDateMessage(FilePath[] roots) {
if (roots.length == 1 && !roots[0].isDirectory()) {
return VcsBundle.message("message.text.file.is.up.to.date");
}
else {
return VcsBundle.message("message.text.all.files.are.up.to.date");
}
}
private void showOptionsDialog(final Map<AbstractVcs, Collection<FilePath>> updateEnvToVirtualFiles, final Project project,
final VcsContext dataContext) {
LinkedHashMap<Configurable, AbstractVcs> envToConfMap = createConfigurableToEnvMap(updateEnvToVirtualFiles);
LOG.debug("configurables map: " + envToConfMap);
if (!envToConfMap.isEmpty()) {
UpdateOrStatusOptionsDialog dialogOrStatus = myActionInfo.createOptionsDialog(project, envToConfMap,
myScopeInfo.getScopeName(dataContext,
myActionInfo));
if (!dialogOrStatus.showAndGet()) {
throw new ProcessCanceledException();
}
}
}
private LinkedHashMap<Configurable, AbstractVcs> createConfigurableToEnvMap(Map<AbstractVcs, Collection<FilePath>> updateEnvToVirtualFiles) {
LinkedHashMap<Configurable, AbstractVcs> envToConfMap = new LinkedHashMap<>();
for (AbstractVcs vcs : updateEnvToVirtualFiles.keySet()) {
Configurable configurable = myActionInfo.getEnvironment(vcs).createConfigurable(updateEnvToVirtualFiles.get(vcs));
if (configurable != null) {
envToConfMap.put(configurable, vcs);
}
}
return envToConfMap;
}
private Map<AbstractVcs, Collection<FilePath>> createVcsToFilesMap(@NotNull FilePath[] roots, @NotNull Project project) {
MultiMap<AbstractVcs, FilePath> resultPrep = MultiMap.createSet();
for (FilePath file : roots) {
AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
if (vcs != null) {
UpdateEnvironment updateEnvironment = myActionInfo.getEnvironment(vcs);
if (updateEnvironment != null) {
resultPrep.putValue(vcs, file);
}
}
}
final Map<AbstractVcs, Collection<FilePath>> result = new THashMap<>();
for (Map.Entry<AbstractVcs, Collection<FilePath>> entry : resultPrep.entrySet()) {
AbstractVcs vcs = entry.getKey();
result.put(vcs, vcs.filterUniqueRoots(new ArrayList<>(entry.getValue()), ObjectsConvertor.FILEPATH_TO_VIRTUAL));
}
return result;
}
@NotNull
private FilePath[] filterRoots(FilePath[] roots, VcsContext vcsContext) {
final ArrayList<FilePath> result = new ArrayList<>();
final Project project = vcsContext.getProject();
assert project != null;
for (FilePath file : roots) {
AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
if (vcs != null) {
if (!myScopeInfo.filterExistsInVcs() || AbstractVcs.fileInVcsByFileStatus(project, file)) {
UpdateEnvironment updateEnvironment = myActionInfo.getEnvironment(vcs);
if (updateEnvironment != null) {
result.add(file);
}
}
else {
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null && virtualFile.isDirectory()) {
final VirtualFile[] vcsRoots = ProjectLevelVcsManager.getInstance(vcsContext.getProject()).getAllVersionedRoots();
for(VirtualFile vcsRoot: vcsRoots) {
if (VfsUtilCore.isAncestor(virtualFile, vcsRoot, false)) {
result.add(file);
}
}
}
}
}
}
return result.toArray(new FilePath[result.size()]);
}
protected abstract boolean filterRootsBeforeAction();
@Override
protected void update(@NotNull VcsContext vcsContext, @NotNull Presentation presentation) {
Project project = vcsContext.getProject();
if (project != null) {
final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(project);
final boolean underVcs = vcsManager.hasActiveVcss();
if (! underVcs) {
presentation.setVisible(false);
return;
}
String actionName = getCompleteActionName(vcsContext);
if (myActionInfo.showOptions(project) || OptionsDialog.shiftIsPressed(vcsContext.getModifiers())) {
actionName += "...";
}
presentation.setText(actionName);
presentation.setVisible(true);
presentation.setEnabled(true);
if (supportingVcsesAreEmpty(vcsManager, myActionInfo)) {
presentation.setVisible(myAlwaysVisible);
presentation.setEnabled(false);
return;
}
if (filterRootsBeforeAction()) {
FilePath[] roots = filterRoots(myScopeInfo.getRoots(vcsContext, myActionInfo), vcsContext);
if (roots.length == 0) {
presentation.setVisible(myAlwaysVisible);
presentation.setEnabled(false);
return;
}
}
if (presentation.isVisible() && presentation.isEnabled() &&
vcsManager.isBackgroundVcsOperationRunning()) {
presentation.setEnabled(false);
}
} else {
presentation.setVisible(false);
presentation.setEnabled(false);
}
}
private static boolean supportingVcsesAreEmpty(final ProjectLevelVcsManager vcsManager, final ActionInfo actionInfo) {
final AbstractVcs[] allActiveVcss = vcsManager.getAllActiveVcss();
for (AbstractVcs activeVcs : allActiveVcss) {
if (actionInfo.getEnvironment(activeVcs) != null) return false;
}
return true;
}
private class Updater extends Task.Backgroundable {
private final String LOCAL_HISTORY_ACTION = VcsBundle.message("local.history.update.from.vcs");
private final Project myProject;
private final ProjectLevelVcsManagerEx myProjectLevelVcsManager;
private final ChangeListManagerEx myChangeListManager;
private UpdatedFiles myUpdatedFiles;
private final FilePath[] myRoots;
private final Map<AbstractVcs, Collection<FilePath>> myVcsToVirtualFiles;
private final Map<HotfixData, List<VcsException>> myGroupedExceptions;
private final List<UpdateSession> myUpdateSessions;
private int myUpdateNumber;
// vcs name, context object
private final Map<AbstractVcs, SequentialUpdatesContext> myContextInfo;
private final VcsDirtyScopeManager myDirtyScopeManager;
private Label myBefore;
private Label myAfter;
private LocalHistoryAction myLocalHistoryAction;
public Updater(final Project project, final FilePath[] roots, final Map<AbstractVcs, Collection<FilePath>> vcsToVirtualFiles) {
super(project, getTemplatePresentation().getText(), true, VcsConfiguration.getInstance(project).getUpdateOption());
myProject = project;
myProjectLevelVcsManager = ProjectLevelVcsManagerEx.getInstanceEx(project);
myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
myChangeListManager = (ChangeListManagerEx)ChangeListManager.getInstance(myProject);
myRoots = roots;
myVcsToVirtualFiles = vcsToVirtualFiles;
myUpdatedFiles = UpdatedFiles.create();
myGroupedExceptions = new HashMap<>();
myUpdateSessions = new ArrayList<>();
// create from outside without any context; context is created by vcses
myContextInfo = new HashMap<>();
myUpdateNumber = 1;
}
private void reset() {
myUpdatedFiles = UpdatedFiles.create();
myGroupedExceptions.clear();
myUpdateSessions.clear();
++ myUpdateNumber;
}
@Override
public void run(@NotNull final ProgressIndicator indicator) {
runImpl();
}
private void runImpl() {
ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges();
myProjectLevelVcsManager.startBackgroundVcsOperation();
myBefore = LocalHistory.getInstance().putSystemLabel(myProject, "Before update");
myLocalHistoryAction = LocalHistory.getInstance().startAction(LOCAL_HISTORY_ACTION);
ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
try {
int toBeProcessed = myVcsToVirtualFiles.size();
int processed = 0;
for (AbstractVcs vcs : myVcsToVirtualFiles.keySet()) {
final UpdateEnvironment updateEnvironment = myActionInfo.getEnvironment(vcs);
updateEnvironment.fillGroups(myUpdatedFiles);
Collection<FilePath> files = myVcsToVirtualFiles.get(vcs);
final SequentialUpdatesContext context = myContextInfo.get(vcs);
final Ref<SequentialUpdatesContext> refContext = new Ref<>(context);
// actual update
UpdateSession updateSession =
updateEnvironment.updateDirectories(files.toArray(new FilePath[files.size()]), myUpdatedFiles, progressIndicator, refContext);
myContextInfo.put(vcs, refContext.get());
processed++;
if (progressIndicator != null) {
progressIndicator.setFraction((double)processed / (double)toBeProcessed);
progressIndicator.setText2("");
}
final List<VcsException> exceptionList = updateSession.getExceptions();
gatherExceptions(vcs, exceptionList);
myUpdateSessions.add(updateSession);
}
} finally {
try {
ProgressManager.progress(VcsBundle.message("progress.text.synchronizing.files"));
doVfsRefresh();
} finally {
myProjectLevelVcsManager.stopBackgroundVcsOperation();
if (!myProject.isDisposed()) {
myProject.getMessageBus().syncPublisher(UpdatedFilesListener.UPDATED_FILES).
consume(UpdatedFilesReverseSide.getPathsFromUpdatedFiles(myUpdatedFiles));
}
}
}
}
private void gatherExceptions(final AbstractVcs vcs, final List<VcsException> exceptionList) {
final VcsExceptionsHotFixer fixer = vcs.getVcsExceptionsHotFixer();
if (fixer == null) {
putExceptions(null, exceptionList);
} else {
putExceptions(fixer.groupExceptions(ActionType.update, exceptionList));
}
}
private void putExceptions(final Map<HotfixData, List<VcsException>> map) {
for (Map.Entry<HotfixData, List<VcsException>> entry : map.entrySet()) {
putExceptions(entry.getKey(), entry.getValue());
}
}
private void putExceptions(final HotfixData key, @NotNull final List<VcsException> list) {
if (list.isEmpty()) return;
myGroupedExceptions.computeIfAbsent(key, k -> new ArrayList<>()).addAll(list);
}
private void doVfsRefresh() {
LOG.info("Calling refresh files after update for roots: " + Arrays.toString(myRoots));
RefreshVFsSynchronously.updateAllChanged(myUpdatedFiles);
notifyAnnotations();
}
private void notifyAnnotations() {
final VcsAnnotationRefresher refresher = myProject.getMessageBus().syncPublisher(VcsAnnotationRefresher.LOCAL_CHANGES_CHANGED);
UpdateFilesHelper.iterateFileGroupFilesDeletedOnServerFirst(myUpdatedFiles, new UpdateFilesHelper.Callback() {
@Override
public void onFile(String filePath, String groupId) {
refresher.dirty(filePath);
}
});
}
private String prepareNotificationWithUpdateInfo() {
StringBuffer text = new StringBuffer();
final List<FileGroup> groups = myUpdatedFiles.getTopLevelGroups();
for (FileGroup group : groups) {
appendGroup(text, group);
}
return text.toString();
}
private void appendGroup(final StringBuffer text, final FileGroup group) {
final int s = group.getFiles().size();
if (s > 0) {
text.append("\n");
text.append(s).append(" ").append(StringUtil.pluralize("File", s)).append(" ").append(group.getUpdateName());
}
final List<FileGroup> list = group.getChildren();
for (FileGroup g : list) {
appendGroup(text, g);
}
}
@Override
public void onSuccess() {
onSuccessImpl(false);
}
private void onSuccessImpl(final boolean wasCanceled) {
if (!myProject.isOpen() || myProject.isDisposed()) {
ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges();
LocalHistory.getInstance().putSystemLabel(myProject, LOCAL_HISTORY_ACTION); // TODO check why this label is needed
return;
}
boolean continueChain = false;
for (SequentialUpdatesContext context : myContextInfo.values()) {
continueChain |= (context != null) && (context.shouldFail());
}
final boolean continueChainFinal = continueChain;
final boolean someSessionWasCancelled = wasCanceled || someSessionWasCanceled(myUpdateSessions);
// here text conflicts might be interactively resolved
for (final UpdateSession updateSession : myUpdateSessions) {
updateSession.onRefreshFilesCompleted();
}
// only after conflicts are resolved, put a label
if (myLocalHistoryAction != null) {
myLocalHistoryAction.finish();
}
myAfter = LocalHistory.getInstance().putSystemLabel(myProject, "After update");
if (myActionInfo.canChangeFileStatus()) {
final List<VirtualFile> files = new ArrayList<>();
final RemoteRevisionsCache revisionsCache = RemoteRevisionsCache.getInstance(myProject);
revisionsCache.invalidate(myUpdatedFiles);
UpdateFilesHelper.iterateFileGroupFiles(myUpdatedFiles, new UpdateFilesHelper.Callback() {
@Override
public void onFile(final String filePath, final String groupId) {
@NonNls final String path = VfsUtilCore.pathToUrl(filePath.replace(File.separatorChar, '/'));
final VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(path);
if (file != null) {
files.add(file);
}
}
});
myDirtyScopeManager.filesDirty(files, null);
}
final boolean updateSuccess = !someSessionWasCancelled && myGroupedExceptions.isEmpty();
WaitForProgressToShow.runOrInvokeLaterAboveProgress(new Runnable() {
@Override
public void run() {
if (myProject.isDisposed()) {
ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges();
return;
}
if (!myGroupedExceptions.isEmpty()) {
if (continueChainFinal) {
gatherContextInterruptedMessages();
}
AbstractVcsHelper.getInstance(myProject).showErrors(myGroupedExceptions, VcsBundle.message("message.title.vcs.update.errors",
getTemplatePresentation().getText()));
}
else if (someSessionWasCancelled) {
ProgressManager.progress(VcsBundle.message("progress.text.updating.canceled"));
}
else {
ProgressManager.progress(VcsBundle.message("progress.text.updating.done"));
}
final boolean noMerged = myUpdatedFiles.getGroupById(FileGroup.MERGED_WITH_CONFLICT_ID).isEmpty();
if (myUpdatedFiles.isEmpty() && myGroupedExceptions.isEmpty()) {
if (someSessionWasCancelled) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, VcsBundle.message("progress.text.updating.canceled"), MessageType.WARNING);
}
else {
VcsBalloonProblemNotifier.showOverChangesView(myProject, getAllFilesAreUpToDateMessage(myRoots), MessageType.INFO);
}
}
else if (!myUpdatedFiles.isEmpty()) {
final UpdateInfoTree tree = showUpdateTree(continueChainFinal && updateSuccess && noMerged, someSessionWasCancelled);
final CommittedChangesCache cache = CommittedChangesCache.getInstance(myProject);
cache.processUpdatedFiles(myUpdatedFiles, new Consumer<List<CommittedChangeList>>() {
@Override
public void consume(List<CommittedChangeList> incomingChangeLists) {
tree.setChangeLists(incomingChangeLists);
}
});
if (someSessionWasCancelled) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, "VCS Update Incomplete" + prepareNotificationWithUpdateInfo(), MessageType.WARNING);
}
else {
VcsBalloonProblemNotifier.showOverChangesView(myProject, "VCS Update Finished" + prepareNotificationWithUpdateInfo(), MessageType.INFO);
}
}
ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges();
if (continueChainFinal && updateSuccess) {
if (!noMerged) {
showContextInterruptedError();
}
else {
// trigger next update; for CVS when updating from several branches simultaneously
reset();
ProgressManager.getInstance().run(Updater.this);
}
}
}
}, null, myProject);
}
private void showContextInterruptedError() {
gatherContextInterruptedMessages();
AbstractVcsHelper.getInstance(myProject).showErrors(myGroupedExceptions,
VcsBundle.message("message.title.vcs.update.errors", getTemplatePresentation().getText()));
}
private void gatherContextInterruptedMessages() {
for (Map.Entry<AbstractVcs, SequentialUpdatesContext> entry : myContextInfo.entrySet()) {
final SequentialUpdatesContext context = entry.getValue();
if ((context == null) || (! context.shouldFail())) continue;
final VcsException exception = new VcsException(context.getMessageWhenInterruptedBeforeStart());
gatherExceptions(entry.getKey(), Collections.singletonList(exception));
}
}
@NotNull
private UpdateInfoTree showUpdateTree(final boolean willBeContinued, final boolean wasCanceled) {
RestoreUpdateTree restoreUpdateTree = RestoreUpdateTree.getInstance(myProject);
restoreUpdateTree.registerUpdateInformation(myUpdatedFiles, myActionInfo);
final String text = getTemplatePresentation().getText() + ((willBeContinued || (myUpdateNumber > 1)) ? ("#" + myUpdateNumber) : "");
final UpdateInfoTree updateInfoTree = myProjectLevelVcsManager.showUpdateProjectInfo(myUpdatedFiles, text, myActionInfo, wasCanceled);
updateInfoTree.setBefore(myBefore);
updateInfoTree.setAfter(myAfter);
updateInfoTree.setCanGroupByChangeList(canGroupByChangelist(myVcsToVirtualFiles.keySet()));
return updateInfoTree;
}
@Override
public void onCancel() {
onSuccessImpl(true);
}
}
}