package org.locationtech.geogig.api.plumbing; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static org.locationtech.geogig.api.porcelain.TransferSummary.ChangedRef.ChangeTypes.ADDED_REF; import static org.locationtech.geogig.api.porcelain.TransferSummary.ChangedRef.ChangeTypes.CHANGED_REF; import static org.locationtech.geogig.api.porcelain.TransferSummary.ChangedRef.ChangeTypes.REMOVED_REF; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.GlobalContextBuilder; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.Remote; import org.locationtech.geogig.api.hooks.Hookable; import org.locationtech.geogig.api.porcelain.SynchronizationException; import org.locationtech.geogig.api.porcelain.SynchronizationException.StatusCode; import org.locationtech.geogig.api.porcelain.TransferSummary; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; @Hookable(name = "send-pack") public class SendPack extends AbstractGeoGigOp<TransferSummary> { private static final Logger LOGGER = LoggerFactory.getLogger(SendPack.class); public static class TransferableRef { private final String localRef; private final String remoteRef; private final boolean forceUpdate; private boolean delete; public TransferableRef(final @Nullable String localRef, final @Nullable String remoteRef, final boolean forceUpdate, final boolean delete) { checkArgument(delete || localRef != null, "localRef can only be null if delete == true"); checkArgument(!delete || remoteRef != null, "remoteRef can't be null if delete == true"); this.localRef = localRef; this.remoteRef = remoteRef; this.forceUpdate = forceUpdate; this.delete = delete; } public String getLocalRef() { return localRef; } @Nullable public String getRemoteRef() { return remoteRef; } public boolean isForceUpdate() { return forceUpdate; } public boolean isDelete() { return delete; } } private List<TransferableRef> refsToPush = new ArrayList<>(); private Remote remote; public SendPack addRef(TransferableRef refToPush) { checkNotNull(refToPush); this.refsToPush.add(refToPush); return this; } public SendPack setRefs(List<TransferableRef> refsToPush) { checkNotNull(refsToPush); for (TransferableRef tr : refsToPush) { checkNotNull(tr); } this.refsToPush.clear(); this.refsToPush.addAll(refsToPush); return this; } public ImmutableList<TransferableRef> getRefs() { return ImmutableList.copyOf(refsToPush); } public SendPack setRemote(Remote remote) { checkNotNull(remote); this.remote = remote; return this; } public Remote getRemote() { return remote; } @Override protected TransferSummary _call() { checkState(remote != null, "no remote specified"); checkState(!refsToPush.isEmpty(), "no refs to push specified"); final IRemoteRepo remoteRepo = openRemoteRepo(remote); TransferSummary transferResult; try { transferResult = callInternal(remoteRepo); checkState(transferResult != null); } finally { try { remoteRepo.close(); } catch (IOException e) { Throwables.propagate(e); } } return transferResult; } private TransferSummary callInternal(IRemoteRepo remoteRepo) { final Remote remote = this.remote; @Nullable String localRefSpec; @Nullable String remoteRefSpec; boolean force; TransferSummary result = new TransferSummary(); for (TransferableRef ref : this.refsToPush) { localRefSpec = ref.getLocalRef(); remoteRefSpec = ref.getRemoteRef(); force = ref.isForceUpdate(); if (ref.isDelete()) { Optional<Ref> deleted = remoteRepo.deleteRef(remoteRefSpec); if (deleted.isPresent()) { ChangedRef deleteResult = new ChangedRef(deleted.get(), null, REMOVED_REF); result.add(remote.getPushURL(), deleteResult); } } else { Optional<Ref> localRef = refParse(localRefSpec); checkState(localRef.isPresent(), "RefSpec %s does not exist", localRefSpec); Optional<Ref> newRef = push(remoteRepo, remote, localRef.get(), remoteRefSpec); if (newRef.isPresent()) { ChangeTypes changeType = remoteRefSpec == null ? ADDED_REF : CHANGED_REF; ChangedRef deleteResult = new ChangedRef(localRef.get(), newRef.get(), changeType); result.add(remote.getPushURL(), deleteResult); } } } return result; } private IRemoteRepo openRemoteRepo(final Remote remote) { final IRemoteRepo remoteRepo; Optional<IRemoteRepo> resolvedRemoteRepo = getRemoteRepo(remote); checkState(resolvedRemoteRepo.isPresent(), "Failed to connect to the remote."); remoteRepo = resolvedRemoteRepo.get(); try { remoteRepo.open(); } catch (IOException e) { Throwables.propagate(e); } return remoteRepo; } private Optional<Ref> push(IRemoteRepo remoteRepo, Remote remote, Ref localRef, @Nullable String remoteRefSpec) { String localRemoteRefName; try { if (null == remoteRefSpec) { localRemoteRefName = Ref.append(Ref.REMOTES_PREFIX, remote.getName() + "/" + localRef.localName()); remoteRepo.pushNewData(localRef, getProgressListener()); } else { localRemoteRefName = Ref.append(Ref.REMOTES_PREFIX, remote.getName() + "/" + remoteRefSpec); remoteRepo.pushNewData(localRef, remoteRefSpec, getProgressListener()); } } catch (SynchronizationException e) { if (e.statusCode == StatusCode.NOTHING_TO_PUSH) { return Optional.absent(); } throw Throwables.propagate(e); } // update the local copy of the remote ref LOGGER.info("Pushing {} to {}({})", localRef, localRemoteRefName, remoteRefSpec); Optional<Ref> updateRef = updateRef(localRef.getObjectId(), localRemoteRefName); return updateRef; } private Optional<Ref> updateRef(ObjectId objectId, String refName) { return this.command(UpdateRef.class).setNewValue(objectId).setName(refName).call(); } /** * @param remote the remote to get * @return an interface for the remote repository */ @VisibleForTesting public Optional<IRemoteRepo> getRemoteRepo(Remote remote) { Hints remoteHints = new Hints(); remoteHints.set(Hints.REMOTES_READ_ONLY, Boolean.FALSE); Repository localRepository = repository(); DeduplicationService deduplicationService = context.deduplicationService(); return RemoteUtils.newRemote(GlobalContextBuilder.builder.build(remoteHints), remote, localRepository, deduplicationService); } private Optional<Ref> refParse(String refSpec) { return command(RefParse.class).setName(refSpec).call(); } }