/******************************************************************************* * Copyright (c) 2016, 2017 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Obeo - initial API and implementation * Martin Fleck - bug 512562 *******************************************************************************/ package org.eclipse.emf.compare.ide.ui.internal.logical; import static org.eclipse.emf.compare.ConflictKind.PSEUDO; import static org.eclipse.emf.compare.ConflictKind.REAL; import static org.eclipse.emf.compare.DifferenceKind.DELETE; import static org.eclipse.emf.compare.DifferenceSource.LEFT; import static org.eclipse.emf.compare.DifferenceState.DISCARDED; import static org.eclipse.emf.compare.DifferenceState.MERGED; import static org.eclipse.emf.compare.merge.AbstractMerger.getMergerDelegate; import static org.eclipse.emf.compare.merge.AbstractMerger.isInTerminalState; import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasDirectOrIndirectConflict; import static org.eclipse.emf.compare.utils.EMFComparePredicates.isAdditiveConflict; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.LinkedHashSet; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IStorage; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.mapping.ResourceMapping; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.emf.common.util.BasicMonitor; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.Monitor; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.compare.Comparison; import org.eclipse.emf.compare.Conflict; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.EMFCompare; import org.eclipse.emf.compare.EMFCompare.Builder; import org.eclipse.emf.compare.ide.IAdditiveResourceMappingMerger; import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages; import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin; import org.eclipse.emf.compare.ide.ui.logical.IModelMinimizer; import org.eclipse.emf.compare.ide.ui.logical.SynchronizationModel; import org.eclipse.emf.compare.ide.utils.ResourceUtil; import org.eclipse.emf.compare.internal.utils.DiffUtil; import org.eclipse.emf.compare.merge.AdditiveMergeCriterion; import org.eclipse.emf.compare.merge.ComputeDiffsToMerge; import org.eclipse.emf.compare.merge.DelegatingMerger; import org.eclipse.emf.compare.merge.MergeBlockedByConflictException; import org.eclipse.emf.compare.rcp.internal.extension.impl.EMFCompareBuilderConfigurator; import org.eclipse.emf.compare.scope.IComparisonScope; import org.eclipse.team.core.diff.IDiff; import org.eclipse.team.core.mapping.IMergeContext; public class AdditiveResourceMappingMerger extends EMFResourceMappingMerger implements IAdditiveResourceMappingMerger { @Override protected void mergeMapping(ResourceMapping mapping, IMergeContext mergeContext, Set<ResourceMapping> failingMappings, IProgressMonitor monitor) throws CoreException { final SubMonitor subMonitor = SubMonitor.convert(monitor, 10); // validateMappings() has made sure we only have EMFResourceMappings final SynchronizationModel syncModel = ((EMFResourceMapping)mapping).getLatestModel(); // we may have non-existing storages in the left traversal, so let's get rid of them removeNonExistingStorages(syncModel.getLeftTraversal()); // get the involved resources before we run the minimizer final Set<IResource> resources = Sets.newLinkedHashSet(syncModel.getResources()); final IModelMinimizer minimizer = EMFCompareIDEUIPlugin.getDefault().getModelMinimizerRegistry() .getCompoundMinimizer(); minimizer.minimize(syncModel, subMonitor.newChild(1)); // 10% final IComparisonScope scope = ComparisonScopeBuilder.create(syncModel, subMonitor.newChild(3)); // 40% final Builder builder = EMFCompare.builder(); EMFCompareBuilderConfigurator.createDefault().configure(builder); final Comparison comparison = builder.build().compare(scope, BasicMonitor.toMonitor(SubMonitor.convert(subMonitor.newChild(1), 10))); // 50% final ResourceAdditionAndDeletionTracker resourceTracker = new ResourceAdditionAndDeletionTracker(); final Set<URI> conflictingURIs = performPreMerge(comparison, subMonitor.newChild(3)); // 80% save(scope.getLeft(), syncModel.getLeftTraversal(), syncModel.getRightTraversal(), syncModel.getOriginTraversal()); if (!conflictingURIs.isEmpty()) { failingMappings.add(mapping); markResourcesAsMerged(mergeContext, resources, conflictingURIs, subMonitor.newChild(2)); // 100% } else { delegateMergeOfUnmergedResourcesAndMarkDiffsAsMerged(syncModel, mergeContext, resourceTracker, subMonitor.newChild(2)); // 100% } scope.getLeft().eAdapters().remove(resourceTracker); subMonitor.setWorkRemaining(0); } private Set<URI> performPreMerge(Comparison comparison, SubMonitor subMonitor) { final Monitor emfMonitor = BasicMonitor.toMonitor(subMonitor); final Set<URI> conflictingURIs = new LinkedHashSet<URI>(); ComputeDiffsToMerge computer = new ComputeDiffsToMerge(true, MERGER_REGISTRY, AdditiveMergeCriterion.INSTANCE).failOnRealConflictUnless(isAdditiveConflict()); for (Diff next : comparison.getDifferences()) { doMergeForDiff(next, computer, emfMonitor, conflictingURIs); } return conflictingURIs; } private void doMergeForDiff(final Diff diff, final ComputeDiffsToMerge computer, final Monitor emfMonitor, final Set<URI> conflictingURIs) { if (isInTerminalState(diff)) { return; } try { Set<Diff> diffsToMerge = computer.getAllDiffsToMerge(diff); for (Diff toMerge : diffsToMerge) { atomicMerge(toMerge, emfMonitor); } } catch (MergeBlockedByConflictException e) { conflictingURIs.addAll(collectConflictingResources(e.getConflictingDiffs().iterator())); } } private void atomicMerge(final Diff diff, final Monitor emfMonitor) { if (hasDirectOrIndirectConflict(REAL).apply(diff)) { if (isOnlyInAdditiveConflicts(diff)) { if (diff.getSource() == LEFT) { if (isRequiredByDeletion(diff)) { // Deletion from left side must be overriden by right changes DelegatingMerger delegatingMerger = getMergerDelegate(diff, MERGER_REGISTRY, AdditiveMergeCriterion.INSTANCE); delegatingMerger.copyRightToLeft(diff, emfMonitor); } else { // other left changes have to be kept. Mark them as merged diff.setState(MERGED); } } else { if (isRequiredByDeletion(diff)) { // Deletion from right side must not be merged. Mark them as merged diff.setState(DISCARDED); } else { // Copy all other changes to left side getMergerDelegate(diff, MERGER_REGISTRY, AdditiveMergeCriterion.INSTANCE) .copyRightToLeft(diff, emfMonitor); } } } else { throw new IllegalStateException(); } } else if (isPseudoConflicting(diff)) { EList<Diff> conflictingDiffs = diff.getConflict().getDifferences(); // FIXME This doesn't seem useful for (Diff conflictingDiff : conflictingDiffs) { conflictingDiff.setState(MERGED); } } else if (diff.getSource() == LEFT) { if (isRequiredByDeletion(diff)) { getMergerDelegate(diff, MERGER_REGISTRY, AdditiveMergeCriterion.INSTANCE) .copyRightToLeft(diff, emfMonitor); } else { diff.setState(MERGED); } } else { // Diff from RIGHT if (isRequiredByDeletion(diff)) { diff.setState(DISCARDED); } else { getMergerDelegate(diff, MERGER_REGISTRY, AdditiveMergeCriterion.INSTANCE) .copyRightToLeft(diff, emfMonitor); } } } @Override protected void delegateMergeOfUnmergedResourcesAndMarkDiffsAsMerged(SynchronizationModel syncModel, IMergeContext mergeContext, ResourceAdditionAndDeletionTracker resourceTracker, SubMonitor subMonitor) throws CoreException { // mark already deleted files as merged for (IFile deletedFile : resourceTracker.getDeletedIFiles()) { final IDiff diff = mergeContext.getDiffTree().getDiff(deletedFile); markAsMerged(diff, mergeContext, subMonitor); } // for all left storages, delegate the merge of a deletion that has not been performed yet and mark // all already performed diffs as merged for (IStorage storage : syncModel.getLeftTraversal().getStorages()) { final IPath fullPath = ResourceUtil.getFixedPath(storage); if (fullPath == null) { EMFCompareIDEUIPlugin.getDefault().getLog().log(new Status(IStatus.WARNING, EMFCompareIDEUIPlugin.PLUGIN_ID, EMFCompareIDEUIMessages.getString("EMFResourceMappingMerger.mergeIncomplete"))); //$NON-NLS-1$ } else { final IDiff diff = mergeContext.getDiffTree().getDiff(fullPath); if (diff != null) { markAsMerged(diff, mergeContext, subMonitor.newChild(1)); } } } // delegate all additions from the right storages that have not been performed yet // or, if they have been merged, mark the diff as merged for (IStorage rightStorage : syncModel.getRightTraversal().getStorages()) { final IPath fullPath = ResourceUtil.getFixedPath(rightStorage); if (fullPath != null) { final IDiff diff = mergeContext.getDiffTree().getDiff(fullPath); if (diff != null && IDiff.ADD == diff.getKind()) { if (!resourceTracker.containsAddedResource(fullPath)) { IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath); IProject project = file.getProject(); if (project.isAccessible()) { merge(diff, mergeContext, subMonitor.newChild(1)); } else { // The project that will contain the resource is not accessible. // We have to copy the file "manually" from the right side to the left side. try (InputStream inputStream = rightStorage.getContents(); FileOutputStream outputStream = new FileOutputStream( ResourceUtil.getAbsolutePath(rightStorage).toFile())) { ByteStreams.copy(inputStream, outputStream); } catch (IOException e) { EMFCompareIDEUIPlugin.getDefault().log(e); // TODO Should we throw the exception here to interrupt the merge ? } } } else { markAsMerged(diff, mergeContext, subMonitor.newChild(1)); } } } } } /** * Test if a diff or one of the diffs that require it are delete diffs. * * @param diff * The given diff * @return <code>true</code> if the diff or one of the diff that require it is a deletion */ private boolean isRequiredByDeletion(Diff diff) { if (diff.getKind() == DELETE) { return true; } else { EList<Diff> requiredBy = diff.getRequiredBy(); for (Diff requiredDiff : requiredBy) { if (isRequiredByDeletion(requiredDiff)) { return true; } } } return false; } /** * Test if a conflicting diff is part of a pseudo-conflict. * * @param diff * The given diff * @return <code>true</code> if the diff is part of a psudo conflict */ private boolean isPseudoConflicting(Diff diff) { Conflict conflict = diff.getConflict(); if (conflict != null && conflict.getKind() == PSEUDO) { return true; } return false; } /** * Test a conflicting diff to determine if it is contained in a conflict that can be considered as an * additive conflict. * * @param diff * The given diff * @return <code>true</code> if the diff is part of an additive conflict */ private boolean isOnlyInAdditiveConflicts(Diff diff) { Set<Conflict> checkedConflicts = new LinkedHashSet<Conflict>(); Conflict conflict = diff.getConflict(); boolean result = false; if (conflict != null && conflict.getKind() == REAL && checkedConflicts.add(conflict)) { if (isAdditiveConflict().apply(conflict)) { result = true; } else { // Short-circuit as soon as non-additive conflict is found return false; } } Set<Diff> allRefiningDiffs = DiffUtil.getAllRefiningDiffs(diff); for (Diff refiningDiff : allRefiningDiffs) { conflict = refiningDiff.getConflict(); if (conflict != null && conflict.getKind() == REAL && checkedConflicts.add(conflict)) { if (isAdditiveConflict().apply(conflict)) { result = true; } else { // Short-circuit as soon as non-additive conflict is found return false; } } } return result; } }