/* 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.repository; 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.Context; 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.DiffCount; import org.locationtech.geogig.api.plumbing.DiffIndex; import org.locationtech.geogig.api.plumbing.FindOrCreateSubtree; import org.locationtech.geogig.api.plumbing.FindTreeChild; import org.locationtech.geogig.api.plumbing.ResolveTreeish; import org.locationtech.geogig.api.plumbing.UpdateRef; import org.locationtech.geogig.api.plumbing.WriteBack; import org.locationtech.geogig.api.plumbing.diff.DiffEntry; import org.locationtech.geogig.api.plumbing.diff.DiffObjectCount; import org.locationtech.geogig.api.plumbing.merge.Conflict; import org.locationtech.geogig.di.Singleton; import org.locationtech.geogig.storage.StagingDatabase; 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.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; /** * The Index keeps track of the changes that have been staged, but not yet committed to the * repository. * <p> * The Index uses an {@link StagingDatabase object database} as storage for the staged changes. This * allows for really large operations not to eat up too much heap, and also works better and allows * for easier implementation of operations that need to manipulate the index. * <p> * The Index database is a composite of its own ObjectDatabase and the repository's. Object look ups * against the index first search on the index db, and if not found defer to the repository object * db. * <p> * Internally, finding out what changes are unstaged is a matter of comparing (through a diff tree * walk) the working tree and the staged changes tree. And finding out what changes are staged to be * committed is performed through a diff tree walk comparing the staged changes tree and the * repository's head tree. * */ @Singleton public class Index implements StagingArea { private Context context; @Inject public Index(final Context context) { Preconditions.checkNotNull(context); this.context = context; } /** * @return the staging database. */ @Override public StagingDatabase getDatabase() { return context.stagingDatabase(); } /** * Updates the STAGE_HEAD ref to the specified tree. * * @param newTree the tree to set as the new STAGE_HEAD */ @Override public void updateStageHead(ObjectId newTree) { context.command(UpdateRef.class).setName(Ref.STAGE_HEAD).setNewValue(newTree).call(); getDatabase().removeConflicts(null); } /** * @return the tree represented by STAGE_HEAD. If there is no tree set at STAGE_HEAD, it will * return the HEAD tree (no unstaged changes). */ @Override public RevTree getTree() { Optional<ObjectId> stageTreeId = context.command(ResolveTreeish.class) .setTreeish(Ref.STAGE_HEAD).call(); RevTree stageTree = RevTree.EMPTY; if (stageTreeId.isPresent()) { if (!stageTreeId.get().equals(RevTree.EMPTY_TREE_ID)) { stageTree = context.stagingDatabase().getTree(stageTreeId.get()); } } else { // Stage tree was not resolved, update it to the head. Optional<ObjectId> headTreeId = context.command(ResolveTreeish.class) .setTreeish(Ref.HEAD).call(); if (headTreeId.isPresent() && !headTreeId.get().equals(RevTree.EMPTY_TREE_ID)) { stageTree = context.objectDatabase().getTree(headTreeId.get()); updateStageHead(stageTree.getId()); } } return stageTree; } /** * @return a supplier for the index. */ private Supplier<RevTreeBuilder> getTreeSupplier() { Supplier<RevTreeBuilder> supplier = new Supplier<RevTreeBuilder>() { @Override public RevTreeBuilder get() { return getTree().builder(getDatabase()); } }; return Suppliers.memoize(supplier); } /** * @param path the path of the {@link Node} to find * @return the {@code Node} for the feature at the specified path if it exists in the index, * otherwise {@link Optional#absent()} */ @Override public Optional<Node> findStaged(final String path) { Optional<NodeRef> entry = context.command(FindTreeChild.class).setIndex(true) .setParent(getTree()).setChildPath(path).call(); if (entry.isPresent()) { return Optional.of(entry.get().getNode()); } else { return Optional.absent(); } } /** * Returns true if there are no unstaged changes, false otherwise */ public boolean isClean() { Optional<ObjectId> resolved = context.command(ResolveTreeish.class).setTreeish(Ref.HEAD) .call(); ObjectId indexTreeId = resolved.get(); return getTree().getId().equals(indexTreeId); } /** * Stages the changes indicated by the {@link DiffEntry} iterator. * * @param progress the progress listener for the process * @param unstaged an iterator for the unstaged changes * @param numChanges number of unstaged changes */ @Override public void stage(final ProgressListener progress, final Iterator<DiffEntry> unstaged, final long numChanges) { int i = 0; progress.started(); final RevTree currentIndexHead = getTree(); Map<String, RevTreeBuilder> parentTress = Maps.newHashMap(); Map<String, ObjectId> parentMetadataIds = Maps.newHashMap(); Set<String> removedTrees = Sets.newHashSet(); StagingDatabase database = getDatabase(); while (unstaged.hasNext()) { final DiffEntry diff = unstaged.next(); final String fullPath = diff.oldPath() == null ? diff.newPath() : diff.oldPath(); final String parentPath = NodeRef.parentPath(fullPath); /* * TODO: revisit, ideally the list of diff entries would come with one single entry for * the whole removed tree instead of that one and every single children of it. */ if (removedTrees.contains(parentPath)) { continue; } if (null == parentPath) { // it is the root tree that's been changed, update head and ignore anything else ObjectId newRoot = diff.newObjectId(); updateStageHead(newRoot); progress.setProgress(100f); progress.complete(); return; } RevTreeBuilder parentTree = getParentTree(currentIndexHead, parentPath, parentTress, parentMetadataIds); i++; progress.setProgress((float) (i * 100) / numChanges); NodeRef oldObject = diff.getOldObject(); NodeRef newObject = diff.getNewObject(); if (newObject == null) { // Delete parentTree.remove(oldObject.name()); if (TYPE.TREE.equals(oldObject.getType())) { removedTrees.add(oldObject.path()); } } else if (oldObject == null) { // Add Node node = newObject.getNode(); parentTree.put(node); parentMetadataIds.put(newObject.path(), newObject.getMetadataId()); } else { // Modify Node node = newObject.getNode(); parentTree.put(node); } database.removeConflict(null, fullPath); } ObjectId newRootTree = currentIndexHead.getId(); for (Map.Entry<String, RevTreeBuilder> entry : parentTress.entrySet()) { String changedTreePath = entry.getKey(); RevTreeBuilder changedTreeBuilder = entry.getValue(); RevTree changedTree = changedTreeBuilder.build(); ObjectId parentMetadataId = parentMetadataIds.get(changedTreePath); if (NodeRef.ROOT.equals(changedTreePath)) { // root database.put(changedTree); newRootTree = changedTree.getId(); } else { // parentMetadataId = parentMetadataId == null ? Supplier<RevTreeBuilder> rootTreeSupplier = getTreeSupplier(); newRootTree = context.command(WriteBack.class).setAncestor(rootTreeSupplier) .setChildPath(changedTreePath).setMetadataId(parentMetadataId) .setToIndex(true).setTree(changedTree).call(); } updateStageHead(newRootTree); } progress.complete(); } /** * @param currentIndexHead * @param diffEntry * @param parentTress * @param parentMetadataIds * @return */ private RevTreeBuilder getParentTree(RevTree currentIndexHead, String parentPath, Map<String, RevTreeBuilder> parentTress, Map<String, ObjectId> parentMetadataIds) { RevTreeBuilder parentBuilder = parentTress.get(parentPath); if (parentBuilder == null) { ObjectId parentMetadataId = null; if (NodeRef.ROOT.equals(parentPath)) { parentBuilder = currentIndexHead.builder(getDatabase()); } else { Optional<NodeRef> parentRef = context.command(FindTreeChild.class).setIndex(true) .setParent(currentIndexHead).setChildPath(parentPath).call(); if (parentRef.isPresent()) { parentMetadataId = parentRef.get().getMetadataId(); } parentBuilder = context.command(FindOrCreateSubtree.class) .setParent(Suppliers.ofInstance(Optional.of(getTree()))).setIndex(true) .setChildPath(parentPath).call().builder(getDatabase()); } parentTress.put(parentPath, parentBuilder); if (parentMetadataId != null) { parentMetadataIds.put(parentPath, parentMetadataId); } } return parentBuilder; } /** * @param pathFilter if specified, only changes that match the filter will be returned * @return an iterator for all of the differences between STAGE_HEAD and HEAD based on the path * filter. */ @Override public Iterator<DiffEntry> getStaged(final @Nullable List<String> pathFilters) { Iterator<DiffEntry> unstaged = context.command(DiffIndex.class).setFilter(pathFilters) .setReportTrees(true).call(); return unstaged; } /** * @param pathFilter if specified, only changes that match the filter will be returned * @return the number differences between STAGE_HEAD and HEAD based on the path filter. */ @Override public DiffObjectCount countStaged(final @Nullable List<String> pathFilters) { DiffObjectCount count = context.command(DiffCount.class).setOldVersion(Ref.HEAD) .setNewVersion(Ref.STAGE_HEAD).setFilter(pathFilters).call(); return count; } @Override public int countConflicted(String pathFilter) { return getDatabase().getConflicts(null, pathFilter).size(); } @Override public List<Conflict> getConflicted(@Nullable String pathFilter) { return getDatabase().getConflicts(null, pathFilter); } }