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
/******************************************************************************* * 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 IResourceMappingMerger. 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 Also:
  • IResourceMappingMerger
Since:3.2
/** * 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.
Params:
  • manager – the manager that defines the scope of the synchronization
  • type – the type of synchronization (ONE_WAY or TWO_WAY)
  • deltaTree – the sync info tree that contains all out-of-sync resources
/** * 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 IStorageMerger instances registered with the storageMergers extension point. Note that the ancestor of the given diff may be missing. Some IStorageMerger instances can still merge without an ancestor so we need to consult the appropriate merger to find out.
Params:
  • diff – the diff
  • monitor – a progress monitor
Returns:a status indicating the results of the merge
/** * 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 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 makeInSync(IDiff, IProgressMonitor) to give subclasses a change to make the file associated with the diff in-sync.
Params:
  • diff – the diff whose local is to be replaced
  • monitor – a progress monitor
Throws:
/** * 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 */
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 performReplace(IDiff, IProgressMonitor) after the local has been changed to match the remote. Subclasses may override performReplace(IDiff, IProgressMonitor) or this method in order to properly reconcile the synchronization state. This method is also invoked from 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.
Params:
  • diff – the diff whose local is now in-sync
  • monitor – a progress monitor
Throws:
/** * 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 */
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 performReplace(IDiff, IProgressMonitor) for files that are being merged that do not exist locally. By default, this method creates the parents using IFolder.create(boolean, boolean, IProgressMonitor). Subclasses may override.
Params:
  • resource – a resource
  • monitor – a progress monitor
Throws:
/** * 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 run that invokes the corresponding run on IWorkspace.
See Also:
/** * 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 Also:
  • getMergeRule.getMergeRule(IDiff)
/** * 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); } }