blob: ae3f270cd9bb5b8de31b94193f69790bdca775a0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Obeo and others.
* 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 514415
*******************************************************************************/
package org.eclipse.emf.compare.merge;
import static org.eclipse.emf.compare.ConflictKind.REAL;
import com.google.common.base.Predicate;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.internal.merge.MergeMode;
/**
* This class computes the diffs to merge for a given diff in the correct order, taking into account the
* merger's indications about required and consequent diffs.
* <dl>
* <dt>Required diffs</dt>
* <dd>These are the diffs that need to be merged (from a structural point of view) before a given diff for
* this diff to be mergeable by the merger.</dd>
* <dt>Consequent diffs</dt>
* <dd>These are all the diffs that, for some reason, the merger considers necessary to merge along with a
* given diff, but which need not be merged before this diff for the merger to be able to merge it. This is
* used to define atomicity of merge operations: Each merger defines the set of diffs that must be merged
* together, and the engine makes surre all these diffs (required+consequent) are merged together or not at
* all in one 'transaction'.</dd>
* </dl>
*
* @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a>
* @since 3.5
*/
public class ComputeDiffsToMerge {
/**
* If this is not acting on a MergeMode (i.e. direction is on a diff-by-diff basis), this will define the
* global direction of the merge.
*/
private final boolean rightToLeft;
/**
* The current merging mode.
*/
private final MergeMode mergeMode;
/** Tells us whether the left side of the comparison we're operating on is editable. */
private final boolean isLeftEditable;
/** Tells us whether the right side of the comparison we're operating on is editable. */
private final boolean isRightEditable;
/**
* The ordered set of diffs to merge.
*/
private Set<Diff> result = new LinkedHashSet<Diff>();
/**
* The overall ordered set of diffs to merge as returned by {@link #getAllDiffsToMerge(Iterable)}.
*/
private Set<Diff> globalResult;
/** The unordered set of all diffs used to avoid infinite loops. */
private Set<Diff> computing = new HashSet<Diff>();
/**
* Whether the discovery of a (real) conflict in the differences needed must cause this to throw an
* Exception.
*/
private Predicate<? super Conflict> conflictChecker;
/** The relationship computer used to calculate dependencies and requirements of diffs. */
private IDiffRelationshipComputer relationshipComputer;
/**
* Constructor.
*
* @param mergeMode
* The kind of merging we're about to implement.
* @param isLeftEditable
* Whether the left side of the comparison we're operating on is editable.
* @param isRightEditable
* Whether the right side of the comparison we're operating on is editable.
* @param relationshipComputer
* The relationship computer used to calculate dependencies and requirements of diffs.
*/
public ComputeDiffsToMerge(MergeMode mergeMode, boolean isLeftEditable, boolean isRightEditable,
IDiffRelationshipComputer relationshipComputer) {
this(true, mergeMode, isLeftEditable, isRightEditable, relationshipComputer);
}
/**
* Constructor if there is no merge mode for this action.
*
* @param rightToLeft
* Direction of the merge.
* @param relationshipComputer
* The relationship computer used to calculate dependencies and requirements of diffs.
*/
public ComputeDiffsToMerge(boolean rightToLeft, IDiffRelationshipComputer relationshipComputer) {
this(rightToLeft, null, true, true, relationshipComputer);
}
/**
* @param rightToLeft
* Direction of the merge. Ignored if {@link MergeMode} is not null.
* @param mergeMode
* The kind of merging we're about to implement.
* @param isLeftEditable
* Whether the left side of the comparison we're operating on is editable.
* @param isRightEditable
* Whether the right side of the comparison we're operating on is editable.
* @param relationshipComputer
* The relationship computer used to calculate dependencies and requirements of diffs.
*/
private ComputeDiffsToMerge(boolean rightToLeft, MergeMode mergeMode, boolean isLeftEditable,
boolean isRightEditable, IDiffRelationshipComputer relationshipComputer) {
this.rightToLeft = rightToLeft;
this.mergeMode = mergeMode;
this.isLeftEditable = isLeftEditable;
this.isRightEditable = isRightEditable;
this.relationshipComputer = relationshipComputer;
}
/**
* Set the failOnConflict flag.
*
* @param predicate
* Predicate that will be used to check whether an exception must be thrown when encountering a
* diff that's involved in a conflict. This predicate will be applied to the conflict, and if
* it returns <code>false</code> then an exception will be thrown.
* @return This for a fluent API.
*/
public ComputeDiffsToMerge failOnRealConflictUnless(Predicate<? super Conflict> predicate) {
conflictChecker = predicate;
return this;
}
/**
* Compute the ordered set of diffs to merge for the given diffs.
*
* @param diffs
* The diffs to merge, along with its required diffs and its consequent diffs.
* @return An ordered Set that contains all the diffs to merge, in the correct order based on required
* diffs and consequent diffs.
*/
public Set<Diff> getAllDiffsToMerge(Iterable<? extends Diff> diffs) {
try {
globalResult = Sets.newLinkedHashSet();
Set<Diff> globalIgnoredDiffs = Sets.newHashSet();
Set<Diff> diffPath = Sets.newLinkedHashSet();
for (Diff diff : diffs) {
if (!globalIgnoredDiffs.contains(diff) && !globalResult.contains(diff)) {
try {
result.clear();
computing.clear();
diffPath.clear();
addDiffs(Collections.singleton(diff), diffPath);
globalResult.addAll(result);
} catch (MergeBlockedByConflictException ex) {
globalIgnoredDiffs.addAll(ex.getConflictingDiffs());
}
}
}
return globalResult;
} finally {
globalResult = null;
}
}
/**
* Compute the ordered set of diffs to merge for the given diff.
*
* @param diff
* The diff to merge, along with its required diffs and its consequent diffs.
* @return An ordered Set that contains all the diffs to merge, in the correct order.
*/
public Set<Diff> getAllDiffsToMerge(Diff diff) {
result.clear();
computing.clear();
addDiff(diff);
return result;
}
/**
* Compute the ordered set of diffs for the given diff.
*
* @param diff
* The diff to merge
*/
protected void addDiff(Diff diff) {
addDiffs(Collections.singleton(diff), Sets.<Diff> newLinkedHashSet());
}
/**
* Recursively add the given diffs to the result Set, starting by their requirements, then themselves,
* then their consequences, in the right order.
*
* @param diffs
* The diffs to merge at the current step of the computation
* @param diffPath
* The path that lead to a diff
*/
protected void addDiffs(Collection<Diff> diffs, Set<Diff> diffPath) {
if (diffs.isEmpty()) {
return;
}
Set<Diff> consequences = new LinkedHashSet<Diff>();
for (Diff diff : diffs) {
addDiff(diff, consequences, diffPath);
}
addDiffs(consequences, diffPath);
}
/**
* Recursively add the required diffs, then the diff itself, to the result. Also add the consequent diffs
* of all the required diffs and the provided one to the given set of consequences.
*
* @param diff
* The diff to add
* @param consequences
* The set of diffs that must be merged at the next step.
* @param diffPath
* The path that lead to the diff to add
*/
protected void addDiff(Diff diff, Set<Diff> consequences, Set<Diff> diffPath) {
if (!result.contains(diff) && (globalResult == null || !globalResult.contains(diff))
&& computing.add(diff)) {
boolean addedToPath = diffPath.add(diff);
if (conflictChecker != null) {
Conflict conflict = diff.getConflict();
if (conflict != null && conflict.getKind() == REAL && !conflictChecker.apply(conflict)) {
throw new MergeBlockedByConflictException(diffPath);
}
}
boolean mergeRightToLeft = rightToLeft;
if (mergeMode != null) {
mergeRightToLeft = !mergeMode.isLeftToRight(diff, isLeftEditable, isRightEditable);
}
Set<Diff> dependencies = relationshipComputer.getDirectMergeDependencies(diff, mergeRightToLeft);
for (Diff required : dependencies) {
addDiff(required, consequences, diffPath);
}
result.add(diff);
computing.remove(diff);
final Set<Diff> directResultingMerges = relationshipComputer.getDirectResultingMerges(diff,
mergeRightToLeft);
consequences.addAll(directResultingMerges);
if (addedToPath) {
diffPath.remove(diff);
}
}
}
}