/* 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 org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.plumbing.FindCommonAncestor;
import org.locationtech.geogig.api.porcelain.SynchronizationException;
import org.locationtech.geogig.api.porcelain.SynchronizationException.StatusCode;
import org.locationtech.geogig.repository.Repository;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
/**
* Provides a base implementation for different representations of the {@link IRemoteRepo}.
*
* @see IRemoteRepo
*/
abstract class AbstractRemoteRepo implements IRemoteRepo {
protected Repository localRepository;
/**
* Constructs a new {@code AbstractRemoteRepo} with the provided reference repository.
*
* @param localRepository the local repository
*/
public AbstractRemoteRepo(Repository localRepository) {
this.localRepository = localRepository;
}
/**
* CommitTraverser for transfers from a shallow clone to a full repository. This works just like
* a normal commit traverser, but will throw an appropriate synchronization exception when the
* history is not deep enough to perform the traversal.
*/
protected class ShallowFullCommitTraverser extends CommitTraverser {
private RepositoryWrapper source;
private RepositoryWrapper destination;
public ShallowFullCommitTraverser(RepositoryWrapper source, RepositoryWrapper destination) {
this.source = source;
this.destination = destination;
}
@Override
protected Evaluation evaluate(CommitNode commitNode) {
if (destination.objectExists(commitNode.getObjectId())) {
return Evaluation.EXCLUDE_AND_PRUNE;
}
if (!commitNode.getObjectId().equals(ObjectId.NULL)
&& !source.objectExists(commitNode.getObjectId())) {
// Source is too shallow
throw new SynchronizationException(StatusCode.HISTORY_TOO_SHALLOW);
}
return Evaluation.INCLUDE_AND_CONTINUE;
}
@Override
protected ImmutableList<ObjectId> getParentsInternal(ObjectId commitId) {
return source.getParents(commitId);
}
@Override
protected boolean existsInDestination(ObjectId commitId) {
return destination.objectExists(commitId);
}
}
/**
* CommitTraverser for transfering data to a shallow clone. This traverser will fetch data up to
* the fetch limit. If no fetch limit is defined, one will be calculated when a commit is
* fetched that I already have. The new fetch depth will be the depth from the starting commit
* to beginning of the orphaned branch.
*/
protected class ShallowCommitTraverser extends CommitTraverser {
Optional<Integer> limit;
private RepositoryWrapper source;
private RepositoryWrapper destination;
public ShallowCommitTraverser(RepositoryWrapper source, RepositoryWrapper destination,
Optional<Integer> limit) {
this.source = source;
this.destination = destination;
this.limit = limit;
}
@Override
protected Evaluation evaluate(CommitNode commitNode) {
if (limit.isPresent() && commitNode.getDepth() > limit.get()) {
return Evaluation.EXCLUDE_AND_PRUNE;
} else if (!source.objectExists(commitNode.getObjectId())) {
// remote history is shallow
return Evaluation.EXCLUDE_AND_PRUNE;
}
boolean exists = destination.objectExists(commitNode.getObjectId());
if (!limit.isPresent() && exists) {
// calculate the new fetch limit
limit = Optional.of(destination.getDepth(commitNode.getObjectId())
+ commitNode.getDepth() - 1);
}
if (exists) {
return Evaluation.EXCLUDE_AND_CONTINUE;
}
return Evaluation.INCLUDE_AND_CONTINUE;
}
@Override
protected ImmutableList<ObjectId> getParentsInternal(ObjectId commitId) {
return source.getParents(commitId);
}
@Override
protected boolean existsInDestination(ObjectId commitId) {
return destination.objectExists(commitId);
}
};
/**
* CommitTraverser for synchronizing data between two full (non-shallow) repositories. The
* traverser will copy data from the source to the destination until there is nothing left to
* copy.
*/
protected class FullCommitTraverser extends CommitTraverser {
private RepositoryWrapper source;
private RepositoryWrapper destination;
public FullCommitTraverser(RepositoryWrapper source, RepositoryWrapper destination) {
this.source = source;
this.destination = destination;
}
@Override
protected Evaluation evaluate(CommitNode commitNode) {
if (destination.objectExists(commitNode.getObjectId())) {
return Evaluation.EXCLUDE_AND_PRUNE;
} else {
return Evaluation.INCLUDE_AND_CONTINUE;
}
}
@Override
protected ImmutableList<ObjectId> getParentsInternal(ObjectId commitId) {
return source.getParents(commitId);
}
@Override
protected boolean existsInDestination(ObjectId commitId) {
return destination.objectExists(commitId);
}
};
/**
* @return the {@link RepositoryWrapper} for this remote
*/
public abstract RepositoryWrapper getRemoteWrapper();
/**
* Returns the appropriate commit traverser to use for the fetch operation.
*
* @param fetchLimit the fetch limit to use
* @return the {@link CommitTraverser} to use.
*/
protected CommitTraverser getFetchTraverser(Optional<Integer> fetchLimit) {
RepositoryWrapper localWrapper = new LocalRepositoryWrapper(localRepository);
RepositoryWrapper remoteWrapper = getRemoteWrapper();
CommitTraverser traverser;
if (localWrapper.getRepoDepth().isPresent()) {
traverser = new ShallowCommitTraverser(remoteWrapper, localWrapper, fetchLimit);
} else if (remoteWrapper.getRepoDepth().isPresent()) {
traverser = new ShallowFullCommitTraverser(remoteWrapper, localWrapper);
} else {
traverser = new FullCommitTraverser(remoteWrapper, localWrapper);
}
return traverser;
}
/**
* Returns the appropriate commit traverser to use for the push operation.
*
* @param remoteRef the remote ref to push to
* @return the {@link CommitTraverser} to use.
*/
protected CommitTraverser getPushTraverser(Optional<Ref> remoteRef)
throws SynchronizationException {
RepositoryWrapper localWrapper = new LocalRepositoryWrapper(localRepository);
RepositoryWrapper remoteWrapper = getRemoteWrapper();
CommitTraverser traverser;
if (remoteWrapper.getRepoDepth().isPresent()) {
Optional<Integer> pushDepth = Optional.absent();
if (!remoteRef.isPresent()) {
pushDepth = remoteWrapper.getRepoDepth();
}
traverser = new ShallowCommitTraverser(localWrapper, remoteWrapper, pushDepth);
} else if (localRepository.getDepth().isPresent()) {
traverser = new ShallowFullCommitTraverser(localWrapper, remoteWrapper);
} else {
traverser = new FullCommitTraverser(localWrapper, remoteWrapper);
}
return traverser;
}
/**
* 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);
}
/**
* Determine if it is safe to push to the remote repository.
*
* @param ref the ref to push
* @param remoteRefOpt the ref to push to
* @throws SynchronizationException if its not safe or possible to push to the given remote ref
* (see {@link StatusCode} for the possible reasons)
*/
protected void checkPush(Ref ref, Optional<Ref> remoteRefOpt) throws SynchronizationException {
if (!remoteRefOpt.isPresent()) {
return;// safe to push
}
final Ref remoteRef = remoteRefOpt.get();
if (remoteRef instanceof SymRef) {
throw new SynchronizationException(StatusCode.CANNOT_PUSH_TO_SYMBOLIC_REF);
}
final ObjectId remoteObjectId = remoteRef.getObjectId();
final ObjectId localObjectId = ref.getObjectId();
if (remoteObjectId.equals(localObjectId)) {
// The branches are equal, no need to push.
throw new SynchronizationException(StatusCode.NOTHING_TO_PUSH);
} else if (localRepository.blobExists(remoteObjectId)) {
Optional<ObjectId> ancestor = localRepository.command(FindCommonAncestor.class)
.setLeftId(remoteObjectId).setRightId(localObjectId).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(localObjectId)) {
// 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(remoteObjectId)) {
// 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 if (!remoteObjectId.isNull()) {
// The remote has data that I do not, a push will cause this data to be lost.
throw new SynchronizationException(StatusCode.REMOTE_HAS_CHANGES);
}
}
}