/* 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:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.api.plumbing;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.Node;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevObject.TYPE;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.RevTreeBuilder;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry.ChangeType;
import org.locationtech.geogig.storage.ObjectDatabase;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Creates a new root tree in the {@link ObjectDatabase object database} from the current index,
* based on the current {@code HEAD} and returns the new root tree id.
* <p>
* This command creates a tree object using the current index. The id of the new root tree object is
* returned. No {@link Ref ref} is updated as a result of this operation, so the resulting root tree
* is "orphan". It's up to the calling code to update any needed reference.
*
* The index must be in a fully merged state.
*
* Conceptually, write-tree sync()s the current index contents into a set of tree objects on the
* {@link ObjectDatabase}. In order to have that match what is actually in your directory right now,
* you need to have done a {@link UpdateIndex} phase before you did the write-tree.
*
* @see FindOrCreateSubtree
* @see DeepMove
* @see ResolveTreeish
* @see CreateTree
* @see RevObjectParse
*/
public class WriteTree extends AbstractGeoGigOp<ObjectId> {
private Supplier<RevTree> oldRoot;
private final List<String> pathFilters = Lists.newLinkedList();
private Supplier<Iterator<DiffEntry>> diffSupplier = null;
/**
* Flag indicating whether or not to move objects from the staging to the objects database.
* Defaults to true. See {@link #dontMoveObjects()}
*/
private boolean moveObjects = true;
/**
* @param oldRoot a supplier for the old root tree
* @return {@code this}
*/
public WriteTree setOldRoot(Supplier<RevTree> oldRoot) {
this.oldRoot = oldRoot;
return this;
}
/**
*
* @param pathFilter the pathfilter to pass on to the index
* @return {@code this}
*/
public WriteTree addPathFilter(String pathFilter) {
if (pathFilter != null) {
this.pathFilters.add(pathFilter);
}
return this;
}
public WriteTree setPathFilter(@Nullable List<String> pathFilters) {
this.pathFilters.clear();
if (pathFilters != null) {
this.pathFilters.addAll(pathFilters);
}
return this;
}
public WriteTree setDiffSupplier(@Nullable Supplier<Iterator<DiffEntry>> diffSupplier) {
this.diffSupplier = diffSupplier;
return this;
}
/**
* Executes the write tree operation.
*
* @return the new root tree id, the current HEAD tree id if there are no differences between
* the index and the HEAD, or {@code null} if the operation has been cancelled (as
* indicated by the {@link #getProgressListener() progress listener}.
*/
@Override
protected ObjectId _call() {
final ProgressListener progress = getProgressListener();
final RevTree oldRootTree = resolveRootTree();
final ObjectDatabase repositoryDatabase = objectDatabase();
Iterator<DiffEntry> diffs = null;
long numChanges = 0;
if (diffSupplier == null) {
diffs = index().getStaged(pathFilters);
numChanges = index().countStaged(pathFilters).count();
} else {
diffs = diffSupplier.get();
}
if (!diffs.hasNext()) {
return oldRootTree.getId();
}
if (progress.isCanceled()) {
return null;
}
Map<String, RevTreeBuilder> repositoryChangedTrees = Maps.newHashMap();
Map<String, NodeRef> indexChangedTrees = Maps.newHashMap();
Map<String, ObjectId> changedTreesMetadataId = Maps.newHashMap();
Set<String> deletedTrees = Sets.newHashSet();
final boolean moveObjects = this.moveObjects;
NodeRef ref;
int i = 0;
RevTree stageHead = index().getTree();
while (diffs.hasNext()) {
if (numChanges != 0) {
progress.setProgress((float) (++i * 100) / numChanges);
}
if (progress.isCanceled()) {
return null;
}
DiffEntry diff = diffs.next();
// ignore the root entry
if (NodeRef.ROOT.equals(diff.newName()) || NodeRef.ROOT.equals(diff.oldName())) {
continue;
}
ref = diff.getNewObject();
if (ref == null) {
ref = diff.getOldObject();
}
final String parentPath = ref.getParentPath();
final boolean isDelete = ChangeType.REMOVED.equals(diff.changeType());
final TYPE type = ref.getType();
if (isDelete && deletedTrees.contains(parentPath)) {
// this is to avoid re-creating the parentTree for a feature delete after its parent
// tree delete entry was processed
continue;
}
RevTreeBuilder parentTree = resolveTargetTree(oldRootTree, parentPath,
repositoryChangedTrees, changedTreesMetadataId, ObjectId.NULL,
repositoryDatabase);
if (type == TYPE.TREE && !isDelete) {
// cache the tree
resolveTargetTree(oldRootTree, ref.name(), repositoryChangedTrees,
changedTreesMetadataId, ref.getMetadataId(), repositoryDatabase);
}
resolveSourceTreeRef(parentPath, indexChangedTrees, changedTreesMetadataId, stageHead);
Preconditions.checkState(parentTree != null);
if (isDelete) {
String oldName = diff.getOldObject().getNode().getName();
parentTree.remove(oldName);
if (TYPE.TREE.equals(type)) {
deletedTrees.add(ref.path());
}
} else {
if (moveObjects && ref.getType().equals(TYPE.TREE)) {
RevTree tree = stagingDatabase().getTree(ref.objectId());
if (!ref.getMetadataId().isNull()) {
repositoryDatabase.put(stagingDatabase()
.getFeatureType(ref.getMetadataId()));
}
if (tree.isEmpty()) {
repositoryDatabase.put(tree);
} else {
continue;
}
} else if (moveObjects) {
deepMove(ref.getNode());
}
parentTree.put(ref.getNode());
}
}
if (progress.isCanceled()) {
return null;
}
// now write back all changed trees
ObjectId newTargetRootId = oldRootTree.getId();
RevTreeBuilder directRootEntries = repositoryChangedTrees.remove(NodeRef.ROOT);
if (directRootEntries != null) {
RevTree newRoot = directRootEntries.build();
repositoryDatabase.put(newRoot);
newTargetRootId = newRoot.getId();
}
for (Map.Entry<String, RevTreeBuilder> e : repositoryChangedTrees.entrySet()) {
String treePath = e.getKey();
ObjectId metadataId = changedTreesMetadataId.get(treePath);
RevTreeBuilder treeBuilder = e.getValue();
RevTree newRoot = getTree(newTargetRootId);
RevTree tree = treeBuilder.build();
newTargetRootId = writeBack(newRoot.builder(repositoryDatabase), tree, treePath,
metadataId);
}
progress.complete();
return newTargetRootId;
}
private void resolveSourceTreeRef(String parentPath, Map<String, NodeRef> indexChangedTrees,
Map<String, ObjectId> metadataCache, RevTree stageHead) {
if (NodeRef.ROOT.equals(parentPath)) {
return;
}
NodeRef indexTreeRef = indexChangedTrees.get(parentPath);
if (indexTreeRef == null) {
Optional<NodeRef> treeRef = Optional.absent();
if (!stageHead.isEmpty()) {// slight optimization, may save a lot of processing on
// large first commits
treeRef = command(FindTreeChild.class).setIndex(true).setParent(stageHead)
.setChildPath(parentPath).call();
}
if (treeRef.isPresent()) {// may not be in case of a delete
indexTreeRef = treeRef.get();
indexChangedTrees.put(parentPath, indexTreeRef);
metadataCache.put(parentPath, indexTreeRef.getMetadataId());
}
} else {
metadataCache.put(parentPath, indexTreeRef.getMetadataId());
}
}
private RevTreeBuilder resolveTargetTree(final RevTree root, String treePath,
Map<String, RevTreeBuilder> treeCache, Map<String, ObjectId> metadataCache,
ObjectId fallbackMetadataId, ObjectDatabase repositoryDatabase) {
RevTreeBuilder treeBuilder = treeCache.get(treePath);
if (treeBuilder == null) {
if (NodeRef.ROOT.equals(treePath)) {
treeBuilder = root.builder(repositoryDatabase);
} else {
Optional<NodeRef> treeRef = command(FindTreeChild.class).setIndex(false)
.setParent(root).setChildPath(treePath).call();
if (treeRef.isPresent()) {
metadataCache.put(treePath, treeRef.get().getMetadataId());
treeBuilder = command(RevObjectParse.class)
.setObjectId(treeRef.get().objectId()).call(RevTree.class).get()
.builder(repositoryDatabase);
} else {
metadataCache.put(treePath, fallbackMetadataId);
treeBuilder = new RevTreeBuilder(repositoryDatabase);
}
}
treeCache.put(treePath, treeBuilder);
}
return treeBuilder;
}
private RevTree getTree(ObjectId treeId) {
return stagingDatabase().getTree(treeId);
}
private void deepMove(Node ref) {
Supplier<Node> objectRef = Suppliers.ofInstance(ref);
command(DeepMove.class).setObjectRef(objectRef).setToIndex(false).call();
}
/**
* @return the resolved root tree id
*/
private ObjectId resolveRootTreeId() {
if (oldRoot != null) {
RevTree rootTree = oldRoot.get();
return rootTree.getId();
}
ObjectId targetTreeId = command(ResolveTreeish.class).setTreeish(Ref.HEAD).call().get();
return targetTreeId;
}
/**
* @return the resolved root tree
*/
private RevTree resolveRootTree() {
if (oldRoot != null) {
return oldRoot.get();
}
final ObjectId targetTreeId = resolveRootTreeId();
return stagingDatabase().getTree(targetTreeId);
}
private ObjectId writeBack(RevTreeBuilder root, final RevTree tree, final String pathToTree,
final ObjectId metadataId) {
return command(WriteBack.class).setAncestor(root).setAncestorPath("").setTree(tree)
.setChildPath(pathToTree).setToIndex(false).setMetadataId(metadataId).call();
}
/**
* Indicates that the WriteTree operation shall not attempt to move the objects from the staging
* to the objects database, since they're known to already be present in the objects database.
* Used usually when {@link #setDiffSupplier(Supplier)} is also set and the calling code takes
* care of storing the features, types, and trees in the objectdatabase.
*/
public WriteTree dontMoveObjects() {
this.moveObjects = false;
return this;
}
}