/* 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:
* Johnathan Garrett (LMN Solutions) - initial implementation
*/
package org.locationtech.geogig.api.porcelain;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.Remote;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.plumbing.RefParse;
import org.locationtech.geogig.api.plumbing.UpdateRef;
import org.locationtech.geogig.api.porcelain.MergeOp.MergeReport;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
/**
* Incorporates changes from a remote repository into the current branch.
*
*/
public class PullOp extends AbstractGeoGigOp<PullResult> {
private boolean all;
private boolean rebase;
private boolean fullDepth = false;
private Supplier<Optional<Remote>> remote;
private List<String> refSpecs = new ArrayList<String>();
private Optional<Integer> depth = Optional.absent();
private Optional<String> authorName = Optional.absent();
private Optional<String> authorEmail = Optional.absent();
/**
* @param all if {@code true}, pull from all remotes.
* @return {@code this}
*/
public PullOp setAll(final boolean all) {
this.all = all;
return this;
}
public boolean isAll() {
return all;
}
/**
* @param rebase if {@code true}, perform a rebase on the remote branch instead of a merge
* @return {@code this}
*/
public PullOp setRebase(final boolean rebase) {
this.rebase = rebase;
return this;
}
public boolean isRebase() {
return rebase;
}
/**
* If no depth is specified, fetch will pull all history from the specified ref(s). If the
* repository is shallow, it will maintain the existing depth.
*
* @param depth maximum commit depth to pull
* @return {@code this}
*/
public PullOp setDepth(final int depth) {
if (depth > 0) {
this.depth = Optional.of(depth);
}
return this;
}
public Integer getDepth() {
return depth.orNull();
}
/**
* If full depth is set on a shallow clone, then the full history will be pulled.
*
* @param fulldepth whether or not to pull the full history
* @return {@code this}
*/
public PullOp setFullDepth(boolean fullDepth) {
this.fullDepth = fullDepth;
return this;
}
public boolean isFullDepth() {
return fullDepth;
}
/**
* @param refSpec the refspec of a remote branch
* @return {@code this}
*/
public PullOp addRefSpec(final String refSpec) {
refSpecs.add(refSpec);
return this;
}
public List<String> getRefSpecs() {
return refSpecs;
}
/**
* @param remoteName the name or URL of a remote repository to fetch from
* @return {@code this}
*/
public PullOp setRemote(final String remoteName) {
Preconditions.checkNotNull(remoteName);
return setRemote(command(RemoteResolve.class).setName(remoteName));
}
public String getRemoteName() {
if (remote == null) {
return null;
}
String name = null;
Optional<Remote> remote = this.remote.get();
if (remote.isPresent()) {
name = remote.get().getName();
}
return name;
}
/**
* @param remoteSupplier the remote repository to fetch from
* @return {@code this}
*/
public PullOp setRemote(Supplier<Optional<Remote>> remoteSupplier) {
Preconditions.checkNotNull(remoteSupplier);
remote = remoteSupplier;
return this;
}
/**
*
* @param author the author of the commit
* @param email email of author
* @return {@code this}
*/
public PullOp setAuthor(@Nullable String authorName, @Nullable String authorEmail) {
this.authorName = Optional.fromNullable(authorName);
this.authorEmail = Optional.fromNullable(authorEmail);
return this;
}
public String getAuthor() {
return authorName.orNull();
}
public String getAuthorEmail() {
return authorEmail.orNull();
}
/**
* Executes the pull operation.
*
* @return {@code null}
* @see org.locationtech.geogig.api.AbstractGeoGigOp#call()
*/
@Override
protected PullResult _call() {
if (remote == null) {
setRemote("origin");
}
PullResult result = new PullResult();
Optional<Remote> remoteRepo = remote.get();
Preconditions.checkArgument(remoteRepo.isPresent(), "Remote could not be resolved.");
getProgressListener().started();
TransferSummary fetchResult = command(FetchOp.class).addRemote(remote).setDepth(depth.or(0))
.setFullDepth(fullDepth).setAll(all).setProgressListener(subProgress(80.f)).call();
result.setFetchResult(fetchResult);
if (refSpecs.size() == 0) {
// pull current branch
final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call();
Preconditions.checkState(currHead.isPresent(), "Repository has no HEAD, can't pull.");
Preconditions.checkState(currHead.get() instanceof SymRef,
"Can't pull from detached HEAD");
final SymRef headRef = (SymRef) currHead.get();
final String currentBranch = Ref.localName(headRef.getTarget());
refSpecs.add(currentBranch + ":" + currentBranch);
}
for (String refspec : refSpecs) {
String[] refs = refspec.split(":");
Preconditions.checkArgument(refs.length < 3,
"Invalid refspec, please use [+]<remoteref>[:<localref>].");
boolean force = refspec.length() > 0 && refspec.charAt(0) == '+';
String remoteref = refs[0].substring(force ? 1 : 0);
Optional<Ref> sourceRef = findRemoteRef(remoteref);
if (!sourceRef.isPresent()) {
continue;
}
String destinationref = "";
if (refs.length == 2) {
destinationref = refs[1];
} else {
// pull into current branch
final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call();
Preconditions.checkState(currHead.isPresent(),
"Repository has no HEAD, can't pull.");
Preconditions.checkState(currHead.get() instanceof SymRef,
"Can't pull from detached HEAD");
final SymRef headRef = (SymRef) currHead.get();
destinationref = headRef.getTarget();
}
Optional<Ref> destRef = command(RefParse.class).setName(destinationref).call();
if (destRef.isPresent()) {
if (destRef.get().getObjectId().equals(sourceRef.get().getObjectId())
|| sourceRef.get().getObjectId().equals(ObjectId.NULL)) {
// Already up to date.
result.setOldRef(destRef.get());
result.setNewRef(destRef.get());
continue;
}
result.setOldRef(destRef.get());
if (destRef.get().getObjectId().equals(ObjectId.NULL)) {
command(UpdateRef.class).setName(destRef.get().getName())
.setNewValue(sourceRef.get().getObjectId()).call();
} else {
command(CheckoutOp.class).setSource(destinationref).call();
if (rebase) {
command(RebaseOp.class).setUpstream(
Suppliers.ofInstance(sourceRef.get().getObjectId())).call();
} else {
try {
MergeReport report = command(MergeOp.class)
.setAuthor(authorName.orNull(), authorEmail.orNull())
.addCommit(Suppliers.ofInstance(sourceRef.get().getObjectId()))
.call();
result.setMergeReport(Optional.of(report));
} catch (NothingToCommitException e) {
// the branch that we are trying to pull has less history than the
// branch we are pulling into
}
}
}
destRef = command(RefParse.class).setName(destinationref).call();
result.setNewRef(destRef.get());
} else {
// make a new branch
Ref newRef = command(BranchCreateOp.class).setAutoCheckout(true)
.setName(destinationref)
.setSource(sourceRef.get().getObjectId().toString()).call();
result.setNewRef(newRef);
}
}
getProgressListener().complete();
result.setRemoteName(remote.get().get().getFetchURL());
return result;
}
/**
* @param ref the ref to find
* @return an {@link Optional} of the ref, or {@link Optional#absent()} if it wasn't found
*/
public Optional<Ref> findRemoteRef(String ref) {
String remoteRef = Ref.REMOTES_PREFIX + remote.get().get().getName() + "/" + ref;
return command(RefParse.class).setName(remoteRef).call();
}
}