/* 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.io.IOException; import java.util.ArrayList; import java.util.List; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.GlobalContextBuilder; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.ProgressListener; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.Remote; import org.locationtech.geogig.api.SymRef; import org.locationtech.geogig.api.plumbing.LsRemote; import org.locationtech.geogig.api.plumbing.RefParse; import org.locationtech.geogig.api.plumbing.UpdateRef; import org.locationtech.geogig.api.plumbing.UpdateSymRef; import org.locationtech.geogig.api.porcelain.ConfigOp.ConfigAction; import org.locationtech.geogig.api.porcelain.ConfigOp.ConfigScope; import org.locationtech.geogig.api.porcelain.TransferSummary.ChangedRef; import org.locationtech.geogig.api.porcelain.TransferSummary.ChangedRef.ChangeTypes; import org.locationtech.geogig.remote.IRemoteRepo; import org.locationtech.geogig.remote.RemoteUtils; import org.locationtech.geogig.repository.Hints; import org.locationtech.geogig.repository.Repository; import org.locationtech.geogig.storage.DeduplicationService; import com.google.common.base.Function; 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.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; /** * Fetches named heads or tags from one or more other repositories, along with the objects necessary * to complete them. */ public class FetchOp extends AbstractGeoGigOp<TransferSummary> { private boolean all; private boolean prune; private boolean fullDepth = false; private List<Remote> remotes = new ArrayList<Remote>(); private Optional<Integer> depth = Optional.absent(); /** * @param all if {@code true}, fetch from all remotes. * @return {@code this} */ public FetchOp setAll(final boolean all) { this.all = all; return this; } public boolean isAll() { return all; } /** * @param prune if {@code true}, remote tracking branches that no longer exist will be removed * locally. * @return {@code this} */ public FetchOp setPrune(final boolean prune) { this.prune = prune; return this; } public boolean isPrune() { return prune; } /** * 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 fetch * @return {@code this} */ public FetchOp setDepth(final int depth) { if (depth > 0) { this.depth = Optional.of(depth); } return this; } public Integer getDepth() { return this.depth.orNull(); } /** * If full depth is set on a shallow clone, then the full history will be fetched. * * @param fulldepth whether or not to fetch the full history * @return {@code this} */ public FetchOp setFullDepth(boolean fullDepth) { this.fullDepth = fullDepth; return this; } public boolean isFullDepth() { return fullDepth; } /** * @param remoteName the name or URL of a remote repository to fetch from * @return {@code this} */ public FetchOp addRemote(final String remoteName) { Preconditions.checkNotNull(remoteName); return addRemote(command(RemoteResolve.class).setName(remoteName)); } public List<String> getRemoteNames() { return Lists.transform(this.remotes, new Function<Remote, String>() { @Override public String apply(Remote remote) { return remote.getName(); } }); } /** * @param remoteSupplier the remote repository to fetch from * @return {@code this} */ public FetchOp addRemote(Supplier<Optional<Remote>> remoteSupplier) { Preconditions.checkNotNull(remoteSupplier); Optional<Remote> remote = remoteSupplier.get(); Preconditions.checkState(remote.isPresent(), "Remote could not be resolved."); remotes.add(remote.get()); return this; } public List<Remote> getRemotes() { return ImmutableList.copyOf(remotes); } /** * Executes the fetch operation. * * @return {@code null} * @see org.locationtech.geogig.api.AbstractGeoGigOp#call() */ @Override protected TransferSummary _call() { if (all) { // Add all remotes to list. ImmutableList<Remote> localRemotes = command(RemoteListOp.class).call(); for (Remote remote : localRemotes) { if (!remotes.contains(remote)) { remotes.add(remote); } } } else if (remotes.size() == 0) { // If no remotes are specified, default to the origin remote addRemote("origin"); } final ProgressListener progressListener = getProgressListener(); progressListener.started(); Optional<Integer> repoDepth = repository().getDepth(); if (repoDepth.isPresent()) { if (fullDepth) { depth = Optional.of(Integer.MAX_VALUE); } if (depth.isPresent()) { if (depth.get() > repoDepth.get()) { command(ConfigOp.class).setAction(ConfigAction.CONFIG_SET) .setScope(ConfigScope.LOCAL).setName(Repository.DEPTH_CONFIG_KEY) .setValue(depth.get().toString()).call(); repoDepth = depth; } } } else if (depth.isPresent() || fullDepth) { // Ignore depth, this is a full repository depth = Optional.absent(); fullDepth = false; } TransferSummary result = new TransferSummary(); for (Remote remote : remotes) { final ImmutableSet<Ref> remoteRemoteRefs = command(LsRemote.class) .setRemote(Suppliers.ofInstance(Optional.of(remote))) .retrieveTags(!remote.getMapped() && (!repoDepth.isPresent() || fullDepth)) .call(); final ImmutableSet<Ref> localRemoteRefs = command(LsRemote.class) .retrieveLocalRefs(true).setRemote(Suppliers.ofInstance(Optional.of(remote))) .call(); // If we have specified a depth to pull, we may have more history to pull from existing // refs. List<ChangedRef> needUpdate = findOutdatedRefs(remote, remoteRemoteRefs, localRemoteRefs, depth); if (prune) { // Delete local refs that aren't in the remote List<Ref> locals = new ArrayList<Ref>(); // only branches, not tags, appear in the remoteRemoteRefs list so we will not catch // any tags in this check. However, we do not track which remote originally // provided a tag so it makes sense not to prune them anyway. for (Ref remoteRef : remoteRemoteRefs) { Optional<Ref> localRef = findLocal(remoteRef, localRemoteRefs); if (localRef.isPresent()) { locals.add(localRef.get()); } } for (Ref localRef : localRemoteRefs) { if (!locals.contains(localRef)) { // Delete the ref ChangedRef changedRef = new ChangedRef(localRef, null, ChangeTypes.REMOVED_REF); needUpdate.add(changedRef); command(UpdateRef.class).setDelete(true).setName(localRef.getName()).call(); } } } Optional<IRemoteRepo> remoteRepo = getRemoteRepo(remote, repository() .deduplicationService()); Preconditions.checkState(remoteRepo.isPresent(), "Failed to connect to the remote."); IRemoteRepo remoteRepoInstance = remoteRepo.get(); try { remoteRepoInstance.open(); } catch (IOException e) { Throwables.propagate(e); } try { int refCount = 0; for (ChangedRef ref : needUpdate) { if (ref.getType() != ChangeTypes.REMOVED_REF) { refCount++; Optional<Integer> newFetchLimit = depth; // If we haven't specified a depth, but this is a shallow repository, set // the // fetch limit to the current repository depth. if (!newFetchLimit.isPresent() && repoDepth.isPresent() && ref.getType() == ChangeTypes.ADDED_REF) { newFetchLimit = repoDepth; } // Fetch updated data from this ref Ref newRef = ref.getNewRef(); remoteRepoInstance.fetchNewData(newRef, newFetchLimit, progressListener); if (repoDepth.isPresent() && !fullDepth) { // Update the repository depth if it is deeper than before. int newDepth; try { newDepth = repository().graphDatabase().getDepth( newRef.getObjectId()); } catch (IllegalStateException e) { throw new RuntimeException(ref.toString(), e); } if (newDepth > repoDepth.get()) { command(ConfigOp.class).setAction(ConfigAction.CONFIG_SET) .setScope(ConfigScope.LOCAL) .setName(Repository.DEPTH_CONFIG_KEY) .setValue(Integer.toString(newDepth)).call(); repoDepth = Optional.of(newDepth); } } // Update the ref Ref updatedRef = updateLocalRef(newRef, remote, localRemoteRefs); ref.setNewRef(updatedRef); } } if (needUpdate.size() > 0) { result.addAll(remote.getFetchURL(), needUpdate); } // Update HEAD ref if (!remote.getMapped()) { Ref remoteHead = remoteRepoInstance.headRef(); if (remoteHead != null) { updateLocalRef(remoteHead, remote, localRemoteRefs); } } } finally { try { remoteRepoInstance.close(); } catch (IOException e) { Throwables.propagate(e); } } } if (fullDepth) { // The full history was fetched, this is no longer a shallow clone command(ConfigOp.class).setAction(ConfigAction.CONFIG_UNSET) .setScope(ConfigScope.LOCAL).setName(Repository.DEPTH_CONFIG_KEY).call(); } progressListener.complete(); return result; } /** * @param remote the remote to get * @return an interface for the remote repository */ public Optional<IRemoteRepo> getRemoteRepo(Remote remote, DeduplicationService deduplicationService) { return RemoteUtils.newRemote(GlobalContextBuilder.builder.build(Hints.readOnly()), remote, repository(), deduplicationService); } private Ref updateLocalRef(Ref remoteRef, Remote remote, ImmutableSet<Ref> localRemoteRefs) { final String refName; if (remoteRef.getName().startsWith(Ref.TAGS_PREFIX)) { refName = remoteRef.getName(); } else { refName = Ref.REMOTES_PREFIX + remote.getName() + "/" + remoteRef.localName(); } Ref updatedRef = remoteRef; if (remoteRef instanceof SymRef) { String targetBranch = Ref.localName(((SymRef) remoteRef).getTarget()); String newTarget = Ref.REMOTES_PREFIX + remote.getName() + "/" + targetBranch; command(UpdateSymRef.class).setName(refName).setNewValue(newTarget).call(); } else { ObjectId effectiveId = remoteRef.getObjectId(); if (remote.getMapped() && !repository().commitExists(remoteRef.getObjectId())) { effectiveId = graphDatabase().getMapping(effectiveId); updatedRef = new Ref(remoteRef.getName(), effectiveId); } command(UpdateRef.class).setName(refName).setNewValue(effectiveId).call(); } return updatedRef; } /** * Filters the remote references for the given remote that are not present or outdated in the * local repository */ private List<ChangedRef> findOutdatedRefs(Remote remote, ImmutableSet<Ref> remoteRefs, ImmutableSet<Ref> localRemoteRefs, Optional<Integer> depth) { List<ChangedRef> changedRefs = Lists.newLinkedList(); for (Ref remoteRef : remoteRefs) {// refs/heads/xxx or refs/tags/yyy, though we don't handle // tags yet if (remote.getMapped() && !remoteRef.localName().equals(Ref.localName(remote.getMappedBranch()))) { // for a mapped remote, we are only interested in the branch we are mapped to continue; } Optional<Ref> local = findLocal(remoteRef, localRemoteRefs); if (local.isPresent()) { if (!local.get().getObjectId().equals(remoteRef.getObjectId())) { ChangedRef changedRef = new ChangedRef(local.get(), remoteRef, ChangeTypes.CHANGED_REF); changedRefs.add(changedRef); } else if (depth.isPresent()) { int commitDepth = graphDatabase().getDepth(local.get().getObjectId()); if (depth.get() > commitDepth) { ChangedRef changedRef = new ChangedRef(local.get(), remoteRef, ChangeTypes.DEEPENED_REF); changedRefs.add(changedRef); } } } else { ChangedRef changedRef = new ChangedRef(null, remoteRef, ChangeTypes.ADDED_REF); changedRefs.add(changedRef); } } return changedRefs; } /** * Finds the corresponding local reference in {@code localRemoteRefs} for the given remote ref * * @param remoteRef a ref in the {@code refs/heads} or {@code refs/tags} namespace as given by * {@link LsRemote} when querying a remote repository * @param localRemoteRefs the list of locally known references of the given remote in the * {@code refs/remotes/<remote name>/} namespace */ private Optional<Ref> findLocal(Ref remoteRef, ImmutableSet<Ref> localRemoteRefs) { if (remoteRef.getName().startsWith(Ref.TAGS_PREFIX)) { return command(RefParse.class).setName(remoteRef.getName()).call(); } else { for (Ref localRef : localRemoteRefs) { if (localRef.localName().equals(remoteRef.localName())) { return Optional.of(localRef); } } return Optional.absent(); } } }