/* Copyright (c) 2012-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: * Johnathan Garrett (LMN Solutions) - initial implementation */ package org.locationtech.geogig.api.plumbing; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; 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.storage.GraphDatabase; import org.locationtech.geogig.storage.GraphDatabase.Direction; import org.locationtech.geogig.storage.GraphDatabase.GraphEdge; import org.locationtech.geogig.storage.GraphDatabase.GraphNode; import com.google.common.base.Optional; import com.google.common.base.Preconditions; /** * Finds the common {@link RevCommit commit} ancestor of two commits. */ public class FindCommonAncestor extends AbstractGeoGigOp<Optional<ObjectId>> { private ObjectId left; private ObjectId right; /** * @param left the left {@link ObjectId} */ public FindCommonAncestor setLeftId(ObjectId left) { this.left = left; return this; } /** * @param right the right {@link ObjectId} */ public FindCommonAncestor setRightId(ObjectId right) { this.right = right; return this; } /** * @param left the left {@link RevCommit} */ public FindCommonAncestor setLeft(RevCommit left) { this.left = left.getId(); return this; } /** * @param right the right {@link RevCommit} */ public FindCommonAncestor setRight(RevCommit right) { this.right = right.getId(); return this; } /** * Finds the common {@link RevCommit commit} ancestor of two commits. * * @return an {@link Optional} of the ancestor commit, or {@link Optional#absent()} if no common * ancestor was found */ @Override protected Optional<ObjectId> _call() { Preconditions.checkState(left != null, "Left commit has not been set."); Preconditions.checkState(right != null, "Right commit has not been set."); if (left.equals(right)) { // They are the same commit return Optional.of(left); } getProgressListener().started(); Optional<ObjectId> ancestor = findLowestCommonAncestor(left, right); getProgressListener().complete(); return ancestor; } /** * Finds the lowest common ancestor of two commits. * * @param leftId the commit id of the left commit * @param rightId the commit id of the right commit * @return An {@link Optional} of the lowest common ancestor of the two commits, or * {@link Optional#absent()} if a common ancestor could not be found. */ public Optional<ObjectId> findLowestCommonAncestor(ObjectId leftId, ObjectId rightId) { Set<GraphNode> leftSet = new HashSet<GraphNode>(); Set<GraphNode> rightSet = new HashSet<GraphNode>(); Queue<GraphNode> leftQueue = new LinkedList<GraphNode>(); Queue<GraphNode> rightQueue = new LinkedList<GraphNode>(); GraphDatabase graphDb = graphDatabase(); GraphNode leftNode = graphDb.getNode(leftId); leftQueue.add(leftNode); GraphNode rightNode = graphDb.getNode(rightId); rightQueue.add(rightNode); List<GraphNode> potentialCommonAncestors = new LinkedList<GraphNode>(); while (!leftQueue.isEmpty() || !rightQueue.isEmpty()) { if (!leftQueue.isEmpty()) { GraphNode commit = leftQueue.poll(); if (processCommit(commit, leftQueue, leftSet, rightQueue, rightSet)) { potentialCommonAncestors.add(commit); } } if (!rightQueue.isEmpty()) { GraphNode commit = rightQueue.poll(); if (processCommit(commit, rightQueue, rightSet, leftQueue, leftSet)) { potentialCommonAncestors.add(commit); } } } verifyAncestors(potentialCommonAncestors, leftSet, rightSet); Optional<ObjectId> ancestor = Optional.absent(); if (potentialCommonAncestors.size() > 0) { ancestor = Optional.of(potentialCommonAncestors.get(0).getIdentifier()); } return ancestor; } /** * Process a commit to see if it has already been seen. If it has, prevent unnecessary work from * continuing on the other traversal queue. If it hasn't, add it's parents to the traversal * queue. * * @param commit commit to process * @param myQueue my traversal queue * @param mySet my visited nodes * @param theirQueue other traversal queue * @param theirSet other traversal's visited nodes * @return */ private boolean processCommit(GraphNode commit, Queue<GraphNode> myQueue, Set<GraphNode> mySet, Queue<GraphNode> theirQueue, Set<GraphNode> theirSet) { if (mySet.add(commit)) { if (theirSet.contains(commit)) { stopAncestryPath(commit, theirQueue, theirSet); return true; } Iterator<GraphEdge> edges = commit.getEdges(Direction.OUT); while (edges.hasNext()) { GraphEdge parentEdge = edges.next(); GraphNode parent = parentEdge.getToNode(); myQueue.add(parent); } } return false; } /** * This function is called when a common ancestor is found and the other traversal queue should * stop traversing down the history of that particular commit. Any ancestors caught after this * one will be an older ancestor. This function follows the ancestry of the common ancestor * until it has been removed from the opposite traversal queue. * * @param commit the common ancestor * @param theirQueue the opposite traversal queue * @param theirSet the opposite visited nodes */ private void stopAncestryPath(GraphNode commit, Queue<GraphNode> theirQueue, Set<GraphNode> theirSet) { Queue<GraphNode> ancestorQueue = new LinkedList<GraphNode>(); ancestorQueue.add(commit); Set<GraphNode> processed = new HashSet<GraphNode>(); while (!ancestorQueue.isEmpty()) { GraphNode ancestor = ancestorQueue.poll(); Iterator<GraphEdge> edges = ancestor.getEdges(Direction.OUT); while (edges.hasNext()) { GraphEdge relationship = edges.next(); GraphNode parentNode = relationship.getToNode(); if (theirSet.contains(parentNode)) { if (!processed.contains(parentNode)) { ancestorQueue.add(parentNode); processed.add(parentNode); } } else { theirQueue.remove(parentNode); } } } } /** * This function is called at the end of the traversal to make sure none of our results have a * more recent ancestor in the result list. * * @param potentialCommonAncestors the result list * @param leftSet the visited nodes of the left traversal * @param rightSet the visited nodes of the right traversal */ private void verifyAncestors(List<GraphNode> potentialCommonAncestors, Set<GraphNode> leftSet, Set<GraphNode> rightSet) { Queue<GraphNode> ancestorQueue = new LinkedList<GraphNode>(); List<GraphNode> falseAncestors = new LinkedList<GraphNode>(); List<GraphNode> processed = new LinkedList<GraphNode>(); for (GraphNode v : potentialCommonAncestors) { if (falseAncestors.contains(v)) { continue; } ancestorQueue.add(v); while (!ancestorQueue.isEmpty()) { GraphNode ancestor = ancestorQueue.poll(); Iterator<GraphEdge> edges = ancestor.getEdges(Direction.OUT); while (edges.hasNext()) { GraphEdge parent = edges.next(); GraphNode parentNode = parent.getToNode(); if (parentNode.getIdentifier() != ancestor.getIdentifier()) { if (leftSet.contains(parentNode) || rightSet.contains(parentNode)) { if (!processed.contains(parentNode)) { ancestorQueue.add(parentNode); processed.add(parentNode); } if (potentialCommonAncestors.contains(parentNode)) { falseAncestors.add(parentNode); } } } } } } potentialCommonAncestors.removeAll(falseAncestors); } }