blob: eea80dc1a9f2e8bd40e69dfaf5eef274a8c303b1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.internal.ccvs.ui.subscriber;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.subscribers.SyncInfo;
import org.eclipse.team.core.sync.IRemoteResource;
import org.eclipse.team.internal.ccvs.core.CVSException;
import org.eclipse.team.internal.ccvs.core.client.Command.LocalOption;
import org.eclipse.team.internal.ccvs.ui.Policy;
import org.eclipse.team.internal.ccvs.ui.operations.UpdateOnlyMergableOperation;
import org.eclipse.team.ui.sync.AndSyncInfoFilter;
import org.eclipse.team.ui.sync.OrSyncInfoFilter;
import org.eclipse.team.ui.sync.SyncInfoDirectionFilter;
import org.eclipse.team.ui.sync.SyncInfoFilter;
import org.eclipse.team.ui.sync.SyncInfoSet;
/**
* This update action will update all mergable resources first and then prompt the
* user to overwrite any resources that failed the safe update.
*
* Subclasses should determine how the update should handle conflicts by implementing
* the getOverwriteLocalChanges() method.
*/
public abstract class SafeUpdateAction extends CVSSubscriberAction {
private List skippedFiles = new ArrayList();
/* (non-Javadoc)
* @see org.eclipse.team.internal.ccvs.ui.subscriber.CVSSubscriberAction#run(org.eclipse.team.ui.sync.SyncInfoSet, org.eclipse.core.runtime.IProgressMonitor)
*/
public void run(SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
try {
// First, remove any known failure cases
SyncInfoFilter failFilter = getKnownFailureCases();
SyncInfo[] willFail = syncSet.getNodes(failFilter);
syncSet.rejectNodes(failFilter);
skippedFiles.clear();
monitor.beginTask(null, (syncSet.size() + willFail.length) * 100);
// Run the update on the remaining nodes in the set
// The update will fail for conflicts that turn out to be non-automergable
safeUpdate(syncSet, Policy.subMonitorFor(monitor, syncSet.size() * 100));
// It is possible that some of the conflicting changes were not auto-mergable.
// Accumulate all resources that have not been updated so far
final SyncInfoSet failedSet = createFailedSet(syncSet, willFail, (IFile[]) skippedFiles.toArray(new IFile[skippedFiles.size()]));
// Remove all these from the original sync set
syncSet.rejectNodes(new SyncInfoFilter() {
public boolean select(SyncInfo info) {
return failedSet.getNodeFor(info.getLocal()) != null;
}
});
// Handle conflicting files that can't be merged, ask the user what should be done.
if(! failedSet.isEmpty()) {
if(getOverwriteLocalChanges()) {
// Ask the user if a replace should be performed on the remaining nodes
if(promptForOverwrite(failedSet)) {
overwriteUpdate(failedSet, Policy.subMonitorFor(monitor, willFail.length * 100));
syncSet.addAll(failedSet);
}
} else {
// Warn the user that some nodes could not be updated. This can happen if there are
// files with conflicts that are not auto-mergeable.
warnAboutFailedResources(failedSet);
}
}
updated(syncSet.getResources());
} finally {
monitor.done();
}
}
protected boolean getOverwriteLocalChanges(){
return false;
}
/**
* Perform a safe update on the resources in the provided set. Any included resources
* that cannot be updated safely wil be added to the skippedFiles list.
* @param syncSet the set containing the resources to be updated
* @param monitor
*/
protected void safeUpdate(SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
SyncInfo[] changed = syncSet.getSyncInfos();
if (changed.length == 0) return;
// The list of sync resources to be updated using "cvs update"
List updateShallow = new ArrayList();
// A list of sync resource folders which need to be created locally
// (incoming addition or previously pruned)
Set parentCreationElements = new HashSet();
// A list of sync resources that are incoming deletions.
// We do these first to avoid case conflicts
List updateDeletions = new ArrayList();
for (int i = 0; i < changed.length; i++) {
SyncInfo changedNode = changed[i];
// Make sure that parent folders exist
SyncInfo parent = getParent(changedNode);
if (parent != null && isOutOfSync(parent)) {
// We need to ensure that parents that are either incoming folder additions
// or previously pruned folders are recreated.
parentCreationElements.add(parent);
}
IResource resource = changedNode.getLocal();
int kind = changedNode.getKind();
boolean willBeAttempted = false;
if (resource.getType() == IResource.FILE) {
// Not all change types will require a "cvs update"
// Some can be deleted locally without performing an update
switch (kind & SyncInfo.DIRECTION_MASK) {
case SyncInfo.INCOMING:
switch (kind & SyncInfo.CHANGE_MASK) {
case SyncInfo.DELETION:
// Incoming deletions can just be deleted instead of updated
updateDeletions.add(changedNode);
willBeAttempted = true;
break;
default:
// add the file to the list of files to be updated
updateShallow.add(changedNode);
willBeAttempted = true;
break;
}
break;
case SyncInfo.CONFLICTING:
switch (kind & SyncInfo.CHANGE_MASK) {
case SyncInfo.CHANGE:
// add the file to the list of files to be updated
updateShallow.add(changedNode);
willBeAttempted = true;
break;
}
break;
}
if (!willBeAttempted) {
skippedFiles.add(resource);
}
} else {
// Special handling for folders to support shallow operations on files
// (i.e. folder operations are performed using the sync info already
// contained in the sync info.
if (isOutOfSync(changedNode)) {
parentCreationElements.add(changedNode);
}
}
}
try {
// Calculate the total amount of work needed
int work = (updateDeletions.size() + updateShallow.size()) * 100;
monitor.beginTask(null, work);
if (parentCreationElements.size() > 0) {
makeInSync((SyncInfo[]) parentCreationElements.toArray(new SyncInfo[parentCreationElements.size()]));
}
if (updateDeletions.size() > 0) {
runUpdateDeletions((SyncInfo[])updateDeletions.toArray(new SyncInfo[updateDeletions.size()]), Policy.subMonitorFor(monitor, updateDeletions.size() * 100));
}
if (updateShallow.size() > 0) {
runSafeUpdate((SyncInfo[])updateShallow.toArray(new SyncInfo[updateShallow.size()]), Policy.subMonitorFor(monitor, updateShallow.size() * 100));
}
} finally {
monitor.done();
}
return;
}
/**
* Perform an overwrite (unsafe) update on the resources in the provided set.
* @param syncSet the set containing the resources to be updated
* @param monitor
*/
protected abstract void overwriteUpdate(SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException;
/*
* Return a filter which selects the cases that we know ahead of time
* will fail on an update
*/
protected SyncInfoFilter getKnownFailureCases() {
return new OrSyncInfoFilter(new SyncInfoFilter[] {
// Conflicting additions of files will fail
new AndSyncInfoFilter(new SyncInfoFilter[] {
SyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.ADDITION),
new SyncInfoFilter() {
public boolean select(SyncInfo info) {
return info.getLocal().getType() == IResource.FILE;
}
}
}),
// Conflicting changes involving a deletion on one side will aways fail
new AndSyncInfoFilter(new SyncInfoFilter[] {
SyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
new SyncInfoFilter() {
public boolean select(SyncInfo info) {
IRemoteResource remote = info.getRemote();
IRemoteResource base = info.getBase();
if (info.getLocal().exists()) {
// local != base and no remote will fail
return (base != null && remote == null);
} else {
// no local and base != remote
return (base != null && remote != null && !base.equals(remote));
}
}
}
}),
// Outgoing changes may not fail but they are skipped as well
new SyncInfoDirectionFilter(SyncInfo.OUTGOING)
});
}
/*
* Return the complete set of selected resources that failed to update safely
*/
private SyncInfoSet createFailedSet(SyncInfoSet syncSet, SyncInfo[] willFail, IFile[] files) {
List result = new ArrayList();
for (int i = 0; i < files.length; i++) {
IFile file = files[i];
SyncInfo resource = syncSet.getNodeFor(file);
if (resource != null) result.add(resource);
}
for (int i = 0; i < willFail.length; i++) {
result.add(willFail[i]);
}
return new SyncInfoSet((SyncInfo[]) result.toArray(new SyncInfo[result.size()]));
}
/**
* Prompt to overwrite those resources that could not be safely updated
* Note: This method is designed to be overridden by test cases.
*
* @return whether to perform the overwrite
*/
protected boolean promptForOverwrite(final SyncInfoSet syncSet) {
final int[] result = new int[] {Dialog.CANCEL};
final Shell shell = getShell();
shell.getDisplay().syncExec(new Runnable() {
public void run() {
UpdateDialog dialog = new UpdateDialog(shell, syncSet);
result[0] = dialog.open();
}
});
return (result[0] == UpdateDialog.YES);
}
/**
* Warn user that some files could not be updated.
* Note: This method is designed to be overridden by test cases.
*/
protected void warnAboutFailedResources(final SyncInfoSet syncSet) {
final int[] result = new int[] {Dialog.CANCEL};
final Shell shell = getShell();
shell.getDisplay().syncExec(new Runnable() {
public void run() {
MessageDialog.openInformation(shell,
Policy.bind("SafeUpdateAction.warnFilesWithConflictsTitle"), //$NON-NLS-1$
Policy.bind("SafeUpdateAction.warnFilesWithConflictsDescription")); //$NON-NLS-1$
}
});
}
/**
* This method is invoked for all resources in the sync set that are incoming deletions.
* It is done separately to allow deletions to be performed before additions that may
* be the same name with different letter case.
* @param nodes the SyncInfo nodes that are incoming deletions
* @param monitor
* @throws TeamException
*/
protected abstract void runUpdateDeletions(SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
/**
* This method is invoked for all resources in the sync set that are incoming changes
* (but not deletions: @see runUpdateDeletions) or conflicting changes.
* This method should only update those conflicting resources that are automergable.
* @param nodes the incoming or conflicting SyncInfo nodes
* @param monitor
* @throws TeamException
*/
protected abstract void runSafeUpdate(SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
protected void safeUpdate(IResource[] resources, LocalOption[] localOptions, IProgressMonitor monitor) throws TeamException {
try {
UpdateOnlyMergableOperation operation = new UpdateOnlyMergableOperation(getShell(), resources, localOptions);
operation.run(monitor);
addSkippedFiles(operation.getSkippedFiles());
} catch (InvocationTargetException e) {
throw CVSException.wrapException(e);
} catch (InterruptedException e) {
Policy.cancelOperation();
}
}
/**
* Notification of all resource that were updated (either safely or othrwise)
*/
protected abstract void updated(IResource[] resources) throws TeamException;
private void addSkippedFiles(IFile[] files) {
skippedFiles.addAll(Arrays.asList(files));
}
protected String getErrorTitle() {
return Policy.bind("UpdateAction.update"); //$NON-NLS-1$
}
/* (non-Javadoc)
* @see org.eclipse.team.internal.ccvs.ui.subscriber.CVSSubscriberAction#getJobName(org.eclipse.team.ui.sync.SyncInfoSet)
*/
protected String getJobName(SyncInfoSet syncSet) {
return Policy.bind("UpdateAction.jobName", new Integer(syncSet.size()).toString()); //$NON-NLS-1$
}
}