blob: 37e15e353c182dc05d75293d603dd24fd1449a3e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.core.mapping.provider;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.mapping.DelegatingStorageMerger;
import org.eclipse.team.core.mapping.IMergeContext;
import org.eclipse.team.core.mapping.IMergeStatus;
import org.eclipse.team.core.mapping.IResourceDiff;
import org.eclipse.team.core.mapping.IResourceDiffTree;
import org.eclipse.team.core.mapping.IResourceMappingMerger;
import org.eclipse.team.core.mapping.IStorageMerger;
import org.eclipse.team.core.mapping.ISynchronizationScopeManager;
import org.eclipse.team.internal.core.Messages;
import org.eclipse.team.internal.core.Policy;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter;
/**
* Provides the context for an <code>IResourceMappingMerger</code>.
* It provides access to the ancestor and remote resource mapping contexts
* so that resource mapping mergers can attempt head-less auto-merges.
* The ancestor context is only required for merges while the remote
* is required for both merge and replace.
*
* @see IResourceMappingMerger
* @since 3.2
*/
public abstract class MergeContext extends SynchronizationContext implements IMergeContext {
/**
* Create a merge context.
*
* @param manager the manager that defines the scope of the synchronization
* @param type the type of synchronization (ONE_WAY or TWO_WAY)
* @param deltaTree the sync info tree that contains all out-of-sync resources
*/
protected MergeContext(ISynchronizationScopeManager manager, int type, IResourceDiffTree deltaTree) {
super(manager, type, deltaTree);
}
@Override
public void reject(final IDiff[] diffs, IProgressMonitor monitor) throws CoreException {
run(monitor1 -> {
for (IDiff node : diffs) {
reject(node, monitor1);
}
}, getMergeRule(diffs), IResource.NONE, monitor);
}
@Override
public void markAsMerged(final IDiff[] nodes, final boolean inSyncHint, IProgressMonitor monitor) throws CoreException {
run(monitor1 -> {
for (IDiff node : nodes) {
markAsMerged(node, inSyncHint, monitor1);
}
}, getMergeRule(nodes), IResource.NONE, monitor);
}
@Override
public IStatus merge(final IDiff[] deltas, final boolean force, IProgressMonitor monitor) throws CoreException {
final List<IFile> failedFiles = new ArrayList<>();
run(monitor1 -> {
try {
monitor1.beginTask(null, deltas.length * 100);
for (IDiff delta : deltas) {
IStatus s = merge(delta, force, Policy.subMonitorFor(monitor1, 100));
if (!s.isOK()) {
if (s.getCode() == IMergeStatus.CONFLICTS) {
failedFiles.addAll(Arrays.asList(((IMergeStatus)s).getConflictingFiles()));
} else {
throw new CoreException(s);
}
}
}
} finally {
monitor1.done();
}
}, getMergeRule(deltas), IWorkspace.AVOID_UPDATE, monitor);
if (failedFiles.isEmpty()) {
return Status.OK_STATUS;
} else {
return new MergeStatus(TeamPlugin.ID, Messages.MergeContext_0, failedFiles.toArray(new IFile[failedFiles.size()]));
}
}
@Override
public IStatus merge(IDiff diff, boolean ignoreLocalChanges, IProgressMonitor monitor) throws CoreException {
Policy.checkCanceled(monitor);
IResource resource = getDiffTree().getResource(diff);
if (resource.getType() != IResource.FILE) {
if (diff instanceof IThreeWayDiff) {
IThreeWayDiff twd = (IThreeWayDiff) diff;
if ((ignoreLocalChanges || getMergeType() == TWO_WAY)
&& resource.getType() == IResource.FOLDER
&& twd.getKind() == IDiff.ADD
&& twd.getDirection() == IThreeWayDiff.OUTGOING
&& ((IFolder)resource).members().length == 0) {
// Delete the local folder addition
((IFolder)resource).delete(false, monitor);
} else if (resource.getType() == IResource.FOLDER
&& !resource.exists()
&& twd.getKind() == IDiff.ADD
&& twd.getDirection() == IThreeWayDiff.INCOMING) {
ensureParentsExist(resource, monitor);
((IFolder)resource).create(false, true, monitor);
makeInSync(diff, monitor);
}
}
return Status.OK_STATUS;
}
if (diff instanceof IThreeWayDiff && !ignoreLocalChanges && getMergeType() == THREE_WAY) {
IThreeWayDiff twDelta = (IThreeWayDiff) diff;
int direction = twDelta.getDirection();
if (direction == IThreeWayDiff.OUTGOING) {
// There's nothing to do so return OK
return Status.OK_STATUS;
}
if (direction == IThreeWayDiff.INCOMING) {
// Just copy the stream since there are no conflicts
performReplace(diff, monitor);
return Status.OK_STATUS;
}
// direction == SyncInfo.CONFLICTING
int type = twDelta.getKind();
if (type == IDiff.REMOVE) {
makeInSync(diff, monitor);
return Status.OK_STATUS;
}
// type == SyncInfo.CHANGE
IResourceDiff remoteChange = (IResourceDiff)twDelta.getRemoteChange();
IFileRevision remote = null;
if (remoteChange != null) {
remote = remoteChange.getAfterState();
}
if (remote == null || !getLocalFile(diff).exists()) {
// Nothing we can do so return a conflict status
// TODO: Should we handle the case where the local and remote have the same contents for a conflicting addition?
return new MergeStatus(TeamPlugin.ID, NLS.bind(Messages.MergeContext_1, new String[] { diff.getPath().toString() }), new IFile[] { getLocalFile(diff) });
}
// We have a conflict, a local, base and remote so we can do
// a three-way merge
return performThreeWayMerge(twDelta, monitor);
} else {
performReplace(diff, monitor);
return Status.OK_STATUS;
}
}
/**
* Perform a three-way merge on the given three-way diff that contains a content conflict.
* By default, this method makes use of {@link IStorageMerger} instances registered
* with the <code>storageMergers</code> extension point. Note that the ancestor
* of the given diff may be missing. Some {@link IStorageMerger} instances
* can still merge without an ancestor so we need to consult the
* appropriate merger to find out.
* @param diff the diff
* @param monitor a progress monitor
* @return a status indicating the results of the merge
*/
protected IStatus performThreeWayMerge(final IThreeWayDiff diff, IProgressMonitor monitor) throws CoreException {
final IStatus[] result = new IStatus[] { Status.OK_STATUS };
run(monitor1 -> {
monitor1.beginTask(null, 100);
IResourceDiff localDiff = (IResourceDiff)diff.getLocalChange();
IResourceDiff remoteDiff = (IResourceDiff)diff.getRemoteChange();
IStorageMerger merger = getAdapter(IStorageMerger.class);
if (merger == null)
merger = DelegatingStorageMerger.getInstance();
IFile file = (IFile)localDiff.getResource();
monitor1.subTask(NLS.bind(Messages.MergeContext_5, file.getFullPath().toString()));
String osEncoding = file.getCharset();
IFileRevision ancestorState = localDiff.getBeforeState();
IFileRevision remoteState = remoteDiff.getAfterState();
IStorage ancestorStorage;
if (ancestorState != null)
ancestorStorage = ancestorState.getStorage(Policy.subMonitorFor(monitor1, 30));
else
ancestorStorage = null;
IStorage remoteStorage = remoteState.getStorage(Policy.subMonitorFor(monitor1, 30));
OutputStream os = getTempOutputStream(file);
try {
IStatus status = merger.merge(os, osEncoding, ancestorStorage, file, remoteStorage, Policy.subMonitorFor(monitor1, 30));
if (status.isOK()) {
file.setContents(getTempInputStream(file, os), false, true, Policy.subMonitorFor(monitor1, 5));
markAsMerged(diff, false, Policy.subMonitorFor(monitor1, 5));
} else {
status = new MergeStatus(status.getPlugin(), status.getMessage(), new IFile[]{file});
}
result[0] = status;
} finally {
disposeTempOutputStream(file, os);
}
monitor1.done();
}, getMergeRule(diff), IWorkspace.AVOID_UPDATE, monitor);
return result[0];
}
private void disposeTempOutputStream(IFile file, OutputStream output) {
if (output instanceof ByteArrayOutputStream)
return;
// We created a temporary file so we need to clean it up
try {
// First make sure the output stream is closed
// so that file deletion will not fail because of that.
if (output != null)
output.close();
} catch (IOException e) {
// Ignore
}
File tmpFile = getTempFile(file);
if (tmpFile.exists())
tmpFile.delete();
}
private OutputStream getTempOutputStream(IFile file) throws CoreException {
File tmpFile = getTempFile(file);
if (tmpFile.exists())
tmpFile.delete();
File parent = tmpFile.getParentFile();
if (!parent.exists())
parent.mkdirs();
try {
return new BufferedOutputStream(new FileOutputStream(tmpFile));
} catch (FileNotFoundException e) {
TeamPlugin.log(IStatus.ERROR, NLS.bind("Could not open temporary file {0} for writing: {1}", new String[] { tmpFile.getAbsolutePath(), e.getMessage() }), e); //$NON-NLS-1$
return new ByteArrayOutputStream();
}
}
private InputStream getTempInputStream(IFile file, OutputStream output) throws CoreException {
if (output instanceof ByteArrayOutputStream) {
ByteArrayOutputStream baos = (ByteArrayOutputStream) output;
return new ByteArrayInputStream(baos.toByteArray());
}
// We created a temporary file so we need to open an input stream on it
try {
// First make sure the output stream is closed
if (output != null)
output.close();
} catch (IOException e) {
// Ignore
}
File tmpFile = getTempFile(file);
try {
return new BufferedInputStream(new FileInputStream(tmpFile));
} catch (FileNotFoundException e) {
throw new CoreException(new Status(IStatus.ERROR, TeamPlugin.ID, IMergeStatus.INTERNAL_ERROR, NLS.bind(Messages.MergeContext_4, new String[] { tmpFile.getAbsolutePath(), e.getMessage() }), e));
}
}
private File getTempFile(IFile file) {
return TeamPlugin.getPlugin().getStateLocation().append(".tmp").append(file.getName() + ".tmp").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
}
private IFile getLocalFile(IDiff delta) {
return ResourcesPlugin.getWorkspace().getRoot().getFile(delta.getPath());
}
/**
* Make the local state of the resource associated with the given diff match
* that of the remote. This method is invoked by the
* {@link #merge(IDiff, boolean, IProgressMonitor)} method. By default, it
* either overwrites the local contexts with the remote contents if both
* exist, deletes the local if the remote does not exists or adds the local
* if the local doesn't exist but the remote does. It then calls
* {@link #makeInSync(IDiff, IProgressMonitor)} to give subclasses a change
* to make the file associated with the diff in-sync.
*
* @param diff
* the diff whose local is to be replaced
* @param monitor
* a progress monitor
* @throws CoreException if an error occurs
*/
protected void performReplace(final IDiff diff, IProgressMonitor monitor) throws CoreException {
IResourceDiff d;
IFile file = getLocalFile(diff);
IFileRevision remote = null;
if (diff instanceof IResourceDiff) {
d = (IResourceDiff) diff;
remote = d.getAfterState();
} else {
d = (IResourceDiff)((IThreeWayDiff)diff).getRemoteChange();
if (d != null)
remote = d.getAfterState();
}
if (d == null) {
d = (IResourceDiff)((IThreeWayDiff)diff).getLocalChange();
if (d != null)
remote = d.getBeforeState();
}
// Only perform the replace if a local or remote change was found
if (d != null) {
performReplace(diff, file, remote, monitor);
}
}
/**
* Method that is invoked from
* {@link #performReplace(IDiff, IProgressMonitor)} after the local has been
* changed to match the remote. Subclasses may override
* {@link #performReplace(IDiff, IProgressMonitor)} or this method in order
* to properly reconcile the synchronization state. This method is also
* invoked from {@link #merge(IDiff, boolean, IProgressMonitor)} if deletion
* conflicts are encountered. It can also be invoked from that same method if
* a folder is created due to an incoming folder addition.
*
* @param diff
* the diff whose local is now in-sync
* @param monitor
* a progress monitor
* @throws CoreException if an error occurs
*/
protected abstract void makeInSync(IDiff diff, IProgressMonitor monitor) throws CoreException;
private void performReplace(final IDiff diff, final IFile file, final IFileRevision remote, IProgressMonitor monitor) throws CoreException {
run(monitor1 -> {
try {
monitor1.beginTask(null, 100);
monitor1.subTask(NLS.bind(Messages.MergeContext_6, file.getFullPath().toString()));
if ((remote == null || !remote.exists()) && file.exists()) {
file.delete(false, true, Policy.subMonitorFor(monitor1, 95));
} else if (remote != null) {
ensureParentsExist(file, monitor1);
InputStream stream = remote.getStorage(monitor1).getContents();
stream = new BufferedInputStream(stream);
try {
if (file.exists()) {
file.setContents(stream, false, true, Policy.subMonitorFor(monitor1, 95));
} else {
file.create(stream, false, Policy.subMonitorFor(monitor1, 95));
}
} finally {
try {
stream.close();
} catch (IOException e) {
// Ignore
}
}
}
// Performing a replace should leave the file in-sync
makeInSync(diff, Policy.subMonitorFor(monitor1, 5));
} finally {
monitor1.done();
}
}, getMergeRule(diff), IWorkspace.AVOID_UPDATE, monitor);
}
/**
* Ensure that the parent folders of the given resource exist.
* This method is invoked from {@link #performReplace(IDiff, IProgressMonitor)}
* for files that are being merged that do not exist locally.
* By default, this method creates the parents using
* {@link IFolder#create(boolean, boolean, IProgressMonitor)}.
* Subclasses may override.
* @param resource a resource
* @param monitor a progress monitor
* @throws CoreException if an error occurs
*/
protected void ensureParentsExist(IResource resource, IProgressMonitor monitor) throws CoreException {
IContainer parent = resource.getParent();
if (parent.getType() != IResource.FOLDER) {
// this method will only create folders
return;
}
if (!parent.exists()) {
ensureParentsExist(parent, monitor);
((IFolder)parent).create(false, true, monitor);
}
}
/**
* Default implementation of <code>run</code> that invokes the
* corresponding <code>run</code> on {@link org.eclipse.core.resources.IWorkspace}.
* @see org.eclipse.team.core.mapping.IMergeContext#run(org.eclipse.core.resources.IWorkspaceRunnable, org.eclipse.core.runtime.jobs.ISchedulingRule, int, org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public void run(IWorkspaceRunnable runnable, ISchedulingRule rule, int flags, IProgressMonitor monitor) throws CoreException {
ResourcesPlugin.getWorkspace().run(runnable, rule, flags, monitor);
}
/**
* Default implementation that returns the resource itself if it exists
* and the first existing parent if the resource does not exist.
* Subclass should override to provide the appropriate rule.
* @see org.eclipse.team.core.mapping.IMergeContext#getMergeRule(IDiff)
*/
@Override
public ISchedulingRule getMergeRule(IDiff diff) {
IResource resource = getDiffTree().getResource(diff);
IResourceRuleFactory ruleFactory = ResourcesPlugin.getWorkspace().getRuleFactory();
ISchedulingRule rule;
if (!resource.exists()) {
// for additions return rule for all parents that need to be created
IContainer parent = resource.getParent();
while (!parent.exists()) {
resource = parent;
parent = parent.getParent();
}
rule = ruleFactory.createRule(resource);
} else if (SyncInfoToDiffConverter.getRemote(diff) == null){
rule = ruleFactory.deleteRule(resource);
} else {
rule = ruleFactory.modifyRule(resource);
}
return rule;
}
@Override
public ISchedulingRule getMergeRule(IDiff[] deltas) {
ISchedulingRule result = null;
for (IDiff node : deltas) {
ISchedulingRule rule = getMergeRule(node);
if (result == null) {
result = rule;
} else {
result = MultiRule.combine(result, rule);
}
}
return result;
}
@Override
public int getMergeType() {
return getType();
}
@Override
@SuppressWarnings("unchecked")
public <T> T getAdapter(Class<T> adapter) {
if (adapter == IStorageMerger.class) {
return (T) DelegatingStorageMerger.getInstance();
}
return super.getAdapter(adapter);
}
}