blob: 5fa282c06e14989451f3e229a55fb98a38b58fda [file] [log] [blame]
/*
* Copyright 2000-2009 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.AnActionEvent;
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.RemoteRevisionsCache;
import com.intellij.openapi.vcs.changes.VcsAnnotationRefresher;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManagerImpl;
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.VfsUtil;
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.ui.OptionsDialog;
import com.intellij.vcsUtil.VcsUtil;
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));
}
protected void actionPerformed(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 e1) {
//ignore
}
}
}
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));
dialogOrStatus.show();
if (!dialogOrStatus.isOK()) {
throw new ProcessCanceledException();
}
}
}
private LinkedHashMap<Configurable, AbstractVcs> createConfigurableToEnvMap(Map<AbstractVcs, Collection<FilePath>> updateEnvToVirtualFiles) {
LinkedHashMap<Configurable, AbstractVcs> envToConfMap = new LinkedHashMap<Configurable, AbstractVcs>();
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(FilePath[] roots, Project project) {
HashMap<AbstractVcs, Collection<FilePath>> resultPrep = new HashMap<AbstractVcs, Collection<FilePath>>();
for (FilePath file : roots) {
AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
if (vcs != null) {
UpdateEnvironment updateEnvironment = myActionInfo.getEnvironment(vcs);
if (updateEnvironment != null) {
if (!resultPrep.containsKey(vcs)) resultPrep.put(vcs, new HashSet<FilePath>());
resultPrep.get(vcs).add(file);
}
}
}
final Map<AbstractVcs, Collection<FilePath>> result = new HashMap<AbstractVcs, Collection<FilePath>>();
for (Map.Entry<AbstractVcs, Collection<FilePath>> entry : resultPrep.entrySet()) {
final AbstractVcs vcs = entry.getKey();
final List<FilePath> paths = new ArrayList<FilePath>(entry.getValue());
result.put(vcs, vcs.filterUniqueRoots(paths, ObjectsConvertor.FILEPATH_TO_VIRTUAL));
}
return result;
}
@NotNull
private FilePath[] filterRoots(FilePath[] roots, VcsContext vcsContext) {
final ArrayList<FilePath> result = new ArrayList<FilePath>();
final Project project = vcsContext.getProject();
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 (VfsUtil.isAncestor(virtualFile, vcsRoot, false)) {
result.add(file);
}
}
}
}
}
}
return result.toArray(new FilePath[result.size()]);
}
protected abstract boolean filterRootsBeforeAction();
protected void update(VcsContext vcsContext, 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);
}
}
protected boolean forceSyncUpdate(final AnActionEvent e) {
return true;
}
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 Project myProject;
private final ProjectLevelVcsManagerEx myProjectLevelVcsManager;
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;
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);
myRoots = roots;
myVcsToVirtualFiles = vcsToVirtualFiles;
myUpdatedFiles = UpdatedFiles.create();
myGroupedExceptions = new HashMap<HotfixData, List<VcsException>>();
myUpdateSessions = new ArrayList<UpdateSession>();
// create from outside without any context; context is created by vcses
myContextInfo = new HashMap<AbstractVcs, SequentialUpdatesContext>();
myUpdateNumber = 1;
}
private void reset() {
myUpdatedFiles = UpdatedFiles.create();
myGroupedExceptions.clear();
myUpdateSessions.clear();
++ myUpdateNumber;
}
private void suspendIfNeeded() {
if (! myActionInfo.canChangeFileStatus()) {
// i.e. for update but not for integrate or status
((VcsDirtyScopeManagerImpl) myDirtyScopeManager).suspendMe();
}
}
private void releaseIfNeeded() {
if (! myActionInfo.canChangeFileStatus()) {
// i.e. for update but not for integrate or status
((VcsDirtyScopeManagerImpl) myDirtyScopeManager).reanimate();
}
}
public void run(@NotNull final ProgressIndicator indicator) {
suspendIfNeeded();
try {
runImpl();
} catch (Throwable t) {
releaseIfNeeded();
if (t instanceof Error) {
throw ((Error) t);
} else if (t instanceof RuntimeException) {
throw ((RuntimeException) t);
}
throw new RuntimeException(t);
}
}
private void runImpl() {
ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges();
myProjectLevelVcsManager.startBackgroundVcsOperation();
myBefore = LocalHistory.getInstance().putSystemLabel(myProject, "Before update");
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<SequentialUpdatesContext>(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;
List<VcsException> exceptionList = myGroupedExceptions.get(key);
if (exceptionList == null) {
exceptionList = new ArrayList<VcsException>();
myGroupedExceptions.put(key, exceptionList);
}
exceptionList.addAll(list);
}
private void doVfsRefresh() {
final String actionName = VcsBundle.message("local.history.update.from.vcs");
final LocalHistoryAction action = LocalHistory.getInstance().startAction(actionName);
try {
LOG.info("Calling refresh files after update for roots: " + Arrays.toString(myRoots));
RefreshVFsSynchronously.updateAllChanged(myUpdatedFiles);
notifyAnnotations();
}
finally {
action.finish();
if ((! myProject.isOpen()) || myProject.isDisposed()) {
LocalHistory.getInstance().putSystemLabel(myProject, actionName);
}
}
}
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);
}
}
public void onSuccess() {
try {
onSuccessImpl(false);
} finally {
releaseIfNeeded();
}
}
private void onSuccessImpl(final boolean wasCanceled) {
if ((! myProject.isOpen()) || myProject.isDisposed()) {
ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges();
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
myAfter = LocalHistory.getInstance().putSystemLabel(myProject, "After update");
if (myActionInfo.canChangeFileStatus()) {
final List<VirtualFile> files = new ArrayList<VirtualFile>();
final RemoteRevisionsCache revisionsCache = RemoteRevisionsCache.getInstance(myProject);
revisionsCache.invalidate(myUpdatedFiles);
UpdateFilesHelper.iterateFileGroupFiles(myUpdatedFiles, new UpdateFilesHelper.Callback() {
public void onFile(final String filePath, final String groupId) {
@NonNls final String path = VfsUtil.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() {
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;
}
public void onCancel() {
try {
onSuccessImpl(true);
} finally {
releaseIfNeeded();
}
}
}
}