/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Victor Olaya (Boundless) - initial implementation */ package org.locationtech.geogig.api.plumbing.merge; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.RevObject.TYPE; import org.locationtech.geogig.api.plumbing.DiffTree; import org.locationtech.geogig.api.plumbing.FindCommonAncestor; import org.locationtech.geogig.api.plumbing.ResolveObjectType; import org.locationtech.geogig.api.plumbing.diff.DiffEntry; import org.locationtech.geogig.api.plumbing.diff.DiffEntry.ChangeType; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; /** * Checks for conflicts between changes introduced by different histories, or features that have to * be merged. * * This operation analyzes a merge scenario and returns true if there are conflicts or some features * have to be merged. This last case happens when a feature has been edited by more than one branch, * and the changes introduced are not the same in all of them. This usually implies creating a * feature not already contained in the repo, but not necessarily. * * This return value indicates an scenario where the merge operation has to be handled differently. * * It returns false in case there are no such issues, and the branches to be merged are completely * independent in their edits. */ public class CheckMergeScenarioOp extends AbstractGeoGigOp<Boolean> { private List<RevCommit> commits; /** * @param commits the commits to check {@link RevCommit} */ public CheckMergeScenarioOp setCommits(List<RevCommit> commits) { this.commits = commits; return this; } @Override protected Boolean _call() { if (commits.size() < 2) { return Boolean.FALSE; } Optional<ObjectId> ancestor = command(FindCommonAncestor.class).setLeft(commits.get(0)) .setRight(commits.get(1)).call(); Preconditions.checkState(ancestor.isPresent(), "No ancestor commit could be found."); for (int i = 2; i < commits.size(); i++) { ancestor = command(FindCommonAncestor.class).setLeft(commits.get(i)) .setRightId(ancestor.get()).call(); Preconditions.checkState(ancestor.isPresent(), "No ancestor commit could be found."); } Map<String, List<DiffEntry>> diffs = Maps.newHashMap(); Set<String> removedPaths = Sets.newTreeSet(); // we organize the changes made for each path for (RevCommit commit : commits) { Iterator<DiffEntry> toMergeDiffs = command(DiffTree.class).setReportTrees(true) .setOldTree(ancestor.get()).setNewTree(commit.getId()).call(); while (toMergeDiffs.hasNext()) { DiffEntry diff = toMergeDiffs.next(); String path = diff.oldPath() == null ? diff.newPath() : diff.oldPath(); if (diffs.containsKey(path)) { diffs.get(path).add(diff); } else { diffs.put(path, Lists.newArrayList(diff)); } if (ChangeType.REMOVED.equals(diff.changeType())) { removedPaths.add(path); } } } // now we check that, for any path, changes are compatible Collection<List<DiffEntry>> values = diffs.values(); for (List<DiffEntry> list : values) { for (int i = 0; i < list.size(); i++) { for (int j = i + 1; j < list.size(); j++) { if (hasConflicts(list.get(i), list.get(j))) { return true; } } if (!ChangeType.REMOVED.equals(list.get(i).changeType())) { if (removedPaths.contains(list.get(i).getNewObject().getParentPath())) { return true; } } } } return false; } private boolean hasConflicts(DiffEntry diff, DiffEntry diff2) { if (!diff.changeType().equals(diff2.changeType())) { return true; } switch (diff.changeType()) { case ADDED: TYPE type = command(ResolveObjectType.class) .setObjectId(diff.getNewObject().objectId()).call(); if (TYPE.TREE.equals(type)) { return !diff.getNewObject().getMetadataId() .equals(diff2.getNewObject().getMetadataId()); } return !diff.getNewObject().objectId().equals(diff2.getNewObject().objectId()); case REMOVED: break; case MODIFIED: type = command(ResolveObjectType.class).setObjectId(diff.getNewObject().objectId()) .call(); if (TYPE.TREE.equals(type)) { return !diff.getNewObject().getMetadataId() .equals(diff2.getNewObject().getMetadataId()); } else { return !diff.newObjectId().equals(diff2.newObjectId()); } } return false; } }