/* Copyright (c) 2013 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: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.api.plumbing.diff; import static com.google.common.collect.Maps.difference; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newTreeMap; import java.util.Map; import java.util.SortedMap; import java.util.SortedSet; import javax.annotation.Nullable; import org.locationtech.geogig.api.Node; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import com.google.common.base.Optional; import com.google.common.collect.MapDifference.ValueDifference; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.SortedMapDifference; /** * Finds the differences between two trees given by two {@link MutableTree} */ public class TreeDifference { private MutableTree leftTree; private MutableTree rightTree; public TreeDifference(MutableTree leftTree, MutableTree rightTree) { this.leftTree = leftTree; this.rightTree = rightTree; } public static TreeDifference create(MutableTree leftTree, MutableTree rightTree) { return new TreeDifference(leftTree, rightTree); } public MutableTree getLeftTree() { return leftTree; } public MutableTree getRightTree() { return rightTree; } public TreeDifference inverse() { return new TreeDifference(rightTree, leftTree); } /** * Finds node references that represent a renamed tree. * <p> * A renamed tree is identified when a ref in the right points to the same tree than a ref in * the left, with a different name, and no other ref in the right has the same name than the one * in the left. * * @return */ public SortedMap<NodeRef, NodeRef> findRenames() { SortedMap<String, MutableTree> leftEntries = leftTree.getChildrenAsMap(); SortedMap<String, MutableTree> rightEntries = rightTree.getChildrenAsMap(); SortedMapDifference<String, MutableTree> difference; difference = difference(leftEntries, rightEntries); return findRenames(difference); } private SortedMap<NodeRef, NodeRef> findRenames( SortedMapDifference<String, MutableTree> difference) { SortedMap<String, MutableTree> entriesOnlyOnLeft = difference.entriesOnlyOnLeft(); SortedMap<String, MutableTree> entriesOnlyOnRight = difference.entriesOnlyOnRight(); SortedMap<NodeRef, NodeRef> matches = newTreeMap(); for (Map.Entry<String, MutableTree> right : entriesOnlyOnRight.entrySet()) { for (Map.Entry<String, MutableTree> left : entriesOnlyOnLeft.entrySet()) { Node leftNode = left.getValue().getNode(); Node rightNode = right.getValue().getNode(); if (rightNode.getObjectId().equals(leftNode.getObjectId())) { String leftParent = NodeRef.parentPath(left.getKey()); String rightParent = NodeRef.parentPath(right.getKey()); NodeRef leftRef = new NodeRef(leftNode, leftParent, ObjectId.NULL); NodeRef rightRef = new NodeRef(rightNode, rightParent, ObjectId.NULL); matches.put(leftRef, rightRef); } } } return matches; } /** * Finds child refs that exist on the right root tree, don't exist on the left root tree, and * are not renames. * * @return */ public SortedSet<NodeRef> findNewTrees() { SortedMap<String, MutableTree> leftEntries = leftTree.getChildrenAsMap(); SortedMap<String, MutableTree> rightEntries = rightTree.getChildrenAsMap(); SortedMapDifference<String, MutableTree> difference; difference = difference(leftEntries, rightEntries); Map<String, MutableTree> entriesOnlyOnRight; entriesOnlyOnRight = newHashMap(difference.entriesOnlyOnRight()); // ignore renames Map<NodeRef, NodeRef> pureRenames = findRenames(difference); for (NodeRef renamedTo : pureRenames.values()) { entriesOnlyOnRight.remove(renamedTo.path()); } SortedSet<NodeRef> newTreeRefs = Sets.newTreeSet(); for (Map.Entry<String, MutableTree> newTree : entriesOnlyOnRight.entrySet()) { Node node = newTree.getValue().getNode(); String parentPath = NodeRef.parentPath(newTree.getKey()); // pass NULL to the NodeRef metadataId, to it defers to the one in the Node in case it // has one (see NodeRef.getMetadataId()) ObjectId metadataId = ObjectId.NULL; NodeRef ref = new NodeRef(node, parentPath, metadataId); newTreeRefs.add(ref); } return newTreeRefs; } /** * Finds child refs that exist on the left root tree, don't exist in the right root tree, and * are not renames. * * @return */ public SortedSet<NodeRef> findDeletes() { return inverse().findNewTrees(); } /** * Finds child refs that are named the same, point to different trees, but are not pure metadata * changes * * @return a sorted map of old/new references to a trees that have changed, deepest paths first */ public SortedMap<NodeRef, NodeRef> findChanges() { SortedMap<String, MutableTree> leftEntries = leftTree.getChildrenAsMap(); SortedMap<String, MutableTree> rightEntries = rightTree.getChildrenAsMap(); final Map<NodeRef, NodeRef> pureMetadataChanges = findPureMetadataChanges(); SortedMapDifference<String, MutableTree> difference; difference = difference(leftEntries, rightEntries); SortedMap<String, ValueDifference<MutableTree>> entriesDiffering; entriesDiffering = difference.entriesDiffering(); SortedMap<NodeRef, NodeRef> matches = Maps.newTreeMap(MutableTree.DEEPEST_FIRST_COMPARATOR); for (Map.Entry<String, ValueDifference<MutableTree>> e : entriesDiffering.entrySet()) { String nodePath = e.getKey(); String parentPath = NodeRef.parentPath(nodePath); ValueDifference<MutableTree> vd = e.getValue(); MutableTree left = vd.leftValue(); MutableTree right = vd.rightValue(); NodeRef lref = new NodeRef(left.getNode(), parentPath, ObjectId.NULL); NodeRef rref = new NodeRef(right.getNode(), parentPath, ObjectId.NULL); if (!pureMetadataChanges.containsKey(lref)) { matches.put(lref, rref); } } return matches; } /** * Finds tree pointers that point to the same tree (path and object id) on the left and right * sides of the comparison but have different {@link NodeRef#getMetadataId() metadata ids} */ public Map<NodeRef, NodeRef> findPureMetadataChanges() { SortedMap<String, MutableTree> leftEntries = leftTree.getChildrenAsMap(); SortedMap<String, MutableTree> rightEntries = rightTree.getChildrenAsMap(); Map<NodeRef, NodeRef> matches = Maps.newTreeMap(); for (Map.Entry<String, MutableTree> e : leftEntries.entrySet()) { final String nodePath = e.getKey(); final MutableTree leftTree = e.getValue(); final Node leftNode = leftTree.getNode(); @Nullable final MutableTree rightTree = rightEntries.get(nodePath); final Node rightNode = rightTree == null ? null : rightTree.getNode(); if (leftNode.equals(rightNode)) { final Optional<ObjectId> leftMetadata = leftNode.getMetadataId(); final Optional<ObjectId> rightMetadata = rightNode.getMetadataId(); if (!leftMetadata.equals(rightMetadata)) { String parentPath = NodeRef.parentPath(nodePath); NodeRef leftRef = new NodeRef(leftNode, parentPath, ObjectId.NULL); NodeRef rightRef = new NodeRef(rightNode, parentPath, ObjectId.NULL); matches.put(leftRef, rightRef); } } } return matches; } public boolean areEqual() { return leftTree.equals(rightTree); } }