Package org.locationtech.geogig.remote

Source Code of org.locationtech.geogig.remote.AbstractMappedRemoteRepo$FetchCommitGatherer

/* 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:
* Johnathan Garrett (LMN Solutions) - initial implementation
*/
package org.locationtech.geogig.remote;

import static com.google.common.base.Preconditions.checkState;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.locationtech.geogig.api.CommitBuilder;
import org.locationtech.geogig.api.DefaultProgressListener;
import org.locationtech.geogig.api.IniRepositoryFilter;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RepositoryFilter;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevObject;
import org.locationtech.geogig.api.RevObject.TYPE;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.plumbing.FindCommonAncestor;
import org.locationtech.geogig.api.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.api.plumbing.ResolveTreeish;
import org.locationtech.geogig.api.plumbing.WriteTree;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.porcelain.ConfigOp;
import org.locationtech.geogig.api.porcelain.ConfigOp.ConfigAction;
import org.locationtech.geogig.api.porcelain.SynchronizationException;
import org.locationtech.geogig.api.porcelain.SynchronizationException.StatusCode;
import org.locationtech.geogig.repository.Repository;
import org.locationtech.geogig.storage.GraphDatabase;
import org.locationtech.geogig.storage.ObjectDatabase;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;

/**
* Abstract base implementation for mapped (sparse) clone.
*/
public abstract class AbstractMappedRemoteRepo implements IRemoteRepo {

    public static String PLACEHOLDER_COMMIT_MESSAGE = "Placeholder Sparse Commit";

    protected Repository localRepository;

    protected RepositoryFilter filter;

    /**
     * Constructs a new {@code AbstractMappedRemoteRepo} with the provided reference repository.
     *
     * @param localRepository the local repository.
     */
    public AbstractMappedRemoteRepo(Repository localRepository) {
        this.localRepository = localRepository;
        Optional<Map<String, String>> filterResult = localRepository.command(ConfigOp.class)
                .setAction(ConfigAction.CONFIG_GET).setName("sparse.filter").call();
        Preconditions.checkState(filterResult.isPresent(), "No filter found for sparse clone.");
        String filterFile = filterResult.get().get("sparse.filter");
        Preconditions.checkState(filterFile != null, "No filter found for sparse clone.");
        try {
            Optional<URL> envHome = localRepository.command(ResolveGeogigDir.class).call();
            checkState(envHome.isPresent(), "Not inside a geogig directory");
            final URL envLocation = envHome.get();
            if (!"file".equals(envLocation.getProtocol())) {
                throw new UnsupportedOperationException(
                        "Sparse clone works only against file system repositories. "
                                + "Repository location: " + envLocation.toExternalForm());
            }
            File repoDir;
            try {
                repoDir = new File(envLocation.toURI());
            } catch (URISyntaxException e) {
                throw Throwables.propagate(e);
            }
            File newFilterFile = new File(repoDir, filterFile);
            filter = new IniRepositoryFilter(newFilterFile.getAbsolutePath());
        } catch (FileNotFoundException e) {
            Throwables.propagate(e);
        }
    }

    /**
     * CommitTraverser for gathering all of the commits that I need to fetch.
     */
    protected class FetchCommitGatherer extends CommitTraverser {

        RepositoryWrapper source;

        Repository destination;

        public FetchCommitGatherer(RepositoryWrapper source, Repository destination) {
            this.source = source;
            this.destination = destination;
        }

        @Override
        protected Evaluation evaluate(CommitNode commitNode) {
            if (destination.graphDatabase().exists(commitNode.getObjectId())) {
                return Evaluation.EXCLUDE_AND_PRUNE;
            }
            return Evaluation.INCLUDE_AND_CONTINUE;
        }

        @Override
        protected ImmutableList<ObjectId> getParentsInternal(ObjectId commitId) {
            return source.getParents(commitId);
        }

        @Override
        protected boolean existsInDestination(ObjectId commitId) {
            return destination.graphDatabase().exists(commitId);
        }

    };

    /**
     * CommitTraverser for gathering all of the commits that I need to push.
     */
    protected class PushCommitGatherer extends CommitTraverser {

        Repository source;

        public PushCommitGatherer(Repository source) {
            this.source = source;
        }

        @Override
        protected Evaluation evaluate(CommitNode commitNode) {
            if (!source.graphDatabase().getMapping(commitNode.getObjectId()).equals(ObjectId.NULL)) {
                return Evaluation.EXCLUDE_AND_PRUNE;
            }
            return Evaluation.INCLUDE_AND_CONTINUE;
        }

        @Override
        protected ImmutableList<ObjectId> getParentsInternal(ObjectId commitId) {
            return source.graphDatabase().getParents(commitId);
        }

        @Override
        protected boolean existsInDestination(ObjectId commitId) {
            // If the commit has not been mapped, it hasn't been pushed to the remote yet
            return !source.graphDatabase().getMapping(commitId).equals(ObjectId.NULL);
        }

    };

    /**
     * @return the {@link RepositoryWrapper} for this remote
     */
    protected abstract RepositoryWrapper getRemoteWrapper();

    /**
     * Fetch all new objects from the specified {@link Ref} from the remote.
     *
     * @param ref the remote ref that points to new commit data
     * @param fetchLimit the maximum depth to fetch, note, a sparse clone cannot be a shallow clone
     */
    @Override
    public final void fetchNewData(Ref ref, Optional<Integer> fetchLimit, ProgressListener progress) {
        Preconditions.checkState(!fetchLimit.isPresent(), "A sparse clone cannot be shallow.");
        FetchCommitGatherer gatherer = new FetchCommitGatherer(getRemoteWrapper(), localRepository);

        try {
            gatherer.traverse(ref.getObjectId());
            Stack<ObjectId> needed = gatherer.commits;
            while (!needed.empty()) {
                ObjectId commitId = needed.pop();
                // If the last commit is empty, add it anyways to preserve parentage of new commits.
                boolean allowEmpty = needed.isEmpty();
                fetchSparseCommit(commitId, allowEmpty);
            }

        } catch (Exception e) {
            Throwables.propagate(e);
        } finally {
        }
    }

    /**
     * This function takes all of the changes introduced by the specified commit and filters them
     * based on the repository filter. It then uses the filtered results to construct a new commit
     * that is the descendant of commits that the original's parents are mapped to.
     *
     * @param commitId the commit id of the original, non-sparse commit
     * @param allowEmpty allow the function to create an empty sparse commit
     */
    private void fetchSparseCommit(ObjectId commitId, boolean allowEmpty) {

        Optional<RevObject> object = getObject(commitId);
        if (object.isPresent() && object.get().getType().equals(TYPE.COMMIT)) {
            RevCommit commit = (RevCommit) object.get();

            FilteredDiffIterator changes = getFilteredChanges(commit);

            GraphDatabase graphDatabase = localRepository.graphDatabase();
            ObjectDatabase objectDatabase = localRepository.objectDatabase();
            graphDatabase.put(commit.getId(), commit.getParentIds());

            RevTree rootTree = RevTree.EMPTY;

            if (commit.getParentIds().size() > 0) {
                // Map this commit to the last "sparse" commit in my ancestry
                ObjectId mappedCommit = graphDatabase.getMapping(commit.getParentIds().get(0));
                graphDatabase.map(commit.getId(), mappedCommit);
                Optional<ObjectId> treeId = localRepository.command(ResolveTreeish.class)
                        .setTreeish(mappedCommit).call();
                if (treeId.isPresent()) {
                    rootTree = localRepository.getTree(treeId.get());
                }

            } else {
                graphDatabase.map(commit.getId(), ObjectId.NULL);
            }

            Iterator<DiffEntry> it = Iterators.filter(changes, new Predicate<DiffEntry>() {
                @Override
                public boolean apply(DiffEntry e) {
                    return true;
                }
            });

            if (it.hasNext()) {
                // Create new commit
                WriteTree writeTree = localRepository.command(WriteTree.class)
                        .setOldRoot(Suppliers.ofInstance(rootTree))
                        .setDiffSupplier(Suppliers.ofInstance((Iterator<DiffEntry>) it));

                if (changes.isAutoIngesting()) {
                    // the iterator already ingests objects into the ObjectDatabase
                    writeTree.dontMoveObjects();
                }

                ObjectId newTreeId = writeTree.call();

                CommitBuilder builder = new CommitBuilder(commit);
                List<ObjectId> newParents = new LinkedList<ObjectId>();
                for (ObjectId parentCommitId : commit.getParentIds()) {
                    newParents.add(graphDatabase.getMapping(parentCommitId));
                }
                builder.setParentIds(newParents);
                builder.setTreeId(newTreeId);

                RevCommit mapped = builder.build();
                objectDatabase.put(mapped);

                if (changes.wasFiltered()) {
                    graphDatabase.setProperty(mapped.getId(), GraphDatabase.SPARSE_FLAG, "true");
                }

                graphDatabase.map(mapped.getId(), commit.getId());
                // Replace the old mapping with the new commit Id.
                graphDatabase.map(commit.getId(), mapped.getId());
            } else if (allowEmpty) {
                CommitBuilder builder = new CommitBuilder(commit);
                List<ObjectId> newParents = new LinkedList<ObjectId>();
                for (ObjectId parentCommitId : commit.getParentIds()) {
                    newParents.add(graphDatabase.getMapping(parentCommitId));
                }
                builder.setParentIds(newParents);
                builder.setTreeId(rootTree.getId());
                builder.setMessage(PLACEHOLDER_COMMIT_MESSAGE);

                RevCommit mapped = builder.build();
                objectDatabase.put(mapped);

                graphDatabase.setProperty(mapped.getId(), GraphDatabase.SPARSE_FLAG, "true");

                graphDatabase.map(mapped.getId(), commit.getId());
                // Replace the old mapping with the new commit Id.
                graphDatabase.map(commit.getId(), mapped.getId());
            } else {
                // Mark the mapped commit as sparse, since it wont have these changes
                graphDatabase.setProperty(graphDatabase.getMapping(commit.getId()),
                        GraphDatabase.SPARSE_FLAG, "true");
            }
        }
    }

    /**
     * Retrieves an object with the specified id from the remote.
     *
     * @param objectId the object to get
     * @return the fetched object
     */
    protected abstract Optional<RevObject> getObject(ObjectId objectId);

    /**
     * Gets all of the changes from the target commit that should be applied to the sparse clone.
     *
     * @param commit the commit to get changes from
     * @return an iterator for changes that match the repository filter
     */
    protected abstract FilteredDiffIterator getFilteredChanges(RevCommit commit);

    /**
     * Push all new objects from the specified {@link Ref} to the remote.
     *
     * @param ref the local ref that points to new commit data
     */
    @Override
    public void pushNewData(Ref ref, ProgressListener progress) throws SynchronizationException {
        pushNewData(ref, ref.getName(), progress);
    }

    /**
     * Push all new objects from the specified {@link Ref} to the given refspec.
     *
     * @param ref the local ref that points to new commit data
     * @param refspec the refspec to push to
     */
    @Override
    public void pushNewData(Ref ref, String refspec, ProgressListener progress)
            throws SynchronizationException {
        Optional<Ref> remoteRef = getRemoteRef(refspec);
        checkPush(ref, remoteRef);
        beginPush();

        PushCommitGatherer gatherer = new PushCommitGatherer(localRepository);

        try {
            gatherer.traverse(ref.getObjectId());
            Stack<ObjectId> needed = gatherer.commits;

            while (!needed.isEmpty()) {
                ObjectId commitToPush = needed.pop();

                pushSparseCommit(commitToPush);
            }

            ObjectId newCommitId = localRepository.graphDatabase().getMapping(ref.getObjectId());

            ObjectId originalRemoteRefValue = ObjectId.NULL;
            if (remoteRef.isPresent()) {
                originalRemoteRefValue = remoteRef.get().getObjectId();
            }

            endPush(refspec, newCommitId, originalRemoteRefValue.toString());

        } catch (Exception e) {
            Throwables.propagate(e);
        } finally {
        }
    }

    /**
     * Gets the remote ref that matches the provided ref spec.
     *
     * @param refspec the refspec to parse
     * @return the matching {@link Ref} or {@link Optional#absent()} if the ref could not be found
     */
    protected abstract Optional<Ref> getRemoteRef(String refspec);

    /**
     * Perform pre-push actions.
     */
    protected void beginPush() {
        // do nothing
    }

    /**
     * Perform post-push actions, this includes verification that the remote wasn't changed while we
     * were pushing.
     *
     * @param refspec the refspec that we are pushing to
     * @param newCommitId the new commit id
     * @param originalRefValue the original value of the ref before pushing
     */
    protected void endPush(String refspec, ObjectId newCommitId, String originalRefValue) {
        updateRemoteRef(refspec, newCommitId, false);
    }

    /**
     * Updates the remote ref that matches the given refspec.
     *
     * @param refspec the ref to update
     * @param commitId the new value of the ref
     * @param delete if true, the remote ref will be deleted
     * @return the updated ref, or {@link Optional#absent() absent} if it didn't exist
     */
    protected abstract Optional<Ref> updateRemoteRef(String refspec, ObjectId commitId,
            boolean delete);

    /**
     * Pushes a sparse commit to a remote repository and updates all mappings.
     *
     * @param commitId the commit to push
     */
    protected abstract void pushSparseCommit(ObjectId commitId);

    /**
     * Determine if it is safe to push to the remote repository.
     *
     * @param ref the ref to push
     * @param remoteRef the ref to push to
     * @throws SynchronizationException
     */
    protected void checkPush(Ref ref, Optional<Ref> remoteRef) throws SynchronizationException {
        if (remoteRef.isPresent()) {
            if (remoteRef.get() instanceof SymRef) {
                throw new SynchronizationException(StatusCode.CANNOT_PUSH_TO_SYMBOLIC_REF);
            }
            ObjectId mappedId = localRepository.graphDatabase().getMapping(
                    remoteRef.get().getObjectId());
            if (mappedId.equals(ref.getObjectId())) {
                // The branches are equal, no need to push.
                throw new SynchronizationException(StatusCode.NOTHING_TO_PUSH);
            } else if (localRepository.blobExists(mappedId)) {
                Optional<ObjectId> ancestor = localRepository.command(FindCommonAncestor.class)
                        .setLeftId(mappedId).setRightId(ref.getObjectId()).call();
                if (!ancestor.isPresent()) {
                    // There is no common ancestor, a push will overwrite history
                    throw new SynchronizationException(StatusCode.REMOTE_HAS_CHANGES);
                } else if (ancestor.get().equals(ref.getObjectId())) {
                    // My last commit is the common ancestor, the remote already has my data.
                    throw new SynchronizationException(StatusCode.NOTHING_TO_PUSH);
                } else if (!ancestor.get().equals(mappedId)) {
                    // The remote branch's latest commit is not my ancestor, a push will cause a
                    // loss of history.
                    throw new SynchronizationException(StatusCode.REMOTE_HAS_CHANGES);
                }
            } else {
                // The remote has data that I do not, a push will cause this data to be lost.
                throw new SynchronizationException(StatusCode.REMOTE_HAS_CHANGES);
            }
        }
    }
}
TOP

Related Classes of org.locationtech.geogig.remote.AbstractMappedRemoteRepo$FetchCommitGatherer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.