/* 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.api.plumbing;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.GeogigTransaction;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.hooks.Hookable;
import org.locationtech.geogig.api.porcelain.CheckoutOp;
import org.locationtech.geogig.api.porcelain.MergeOp;
import org.locationtech.geogig.api.porcelain.NothingToCommitException;
import org.locationtech.geogig.api.porcelain.RebaseConflictsException;
import org.locationtech.geogig.api.porcelain.RebaseOp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
/**
* Finishes a {@link GeogigTransaction} by merging all refs that have been changed.
* <p>
* If a given ref has not been changed on the repsoitory, it will simply update the repository's ref
* to the value of the transaction ref.
* <p>
* If the repository ref was updated while the transaction occurred, the changes will be brought
* together via a merge or rebase operation and the new ref will be updated to the result.
*
* @see GeogigTransaction
*/
@Hookable(name = "transaction-end")
public class TransactionEnd extends AbstractGeoGigOp<Boolean> {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionEnd.class);
private boolean cancel = false;
private GeogigTransaction transaction = null;
private boolean rebase = false;
private Optional<String> authorName = Optional.absent();
private Optional<String> authorEmail = Optional.absent();
/**
* @param cancel if {@code true}, the transaction will be cancelled, otherwise it will be
* committed
* @return {@code this}
*/
public TransactionEnd setCancel(boolean cancel) {
this.cancel = cancel;
return this;
}
/**
* @param transaction the transaction to end
* @return {@code this}
*/
public TransactionEnd setTransaction(GeogigTransaction transaction) {
this.transaction = transaction;
return this;
}
/**
* @param rebase use rebase instead of merge when completing the transaction
* @return {@code this}
*/
public TransactionEnd setRebase(boolean rebase) {
this.rebase = rebase;
return this;
}
/**
* @param authorName the author of the transaction to use for merge commits
* @param authorEmail the email of the transaction author to use for merge commits
* @return {@code this}
*/
public TransactionEnd setAuthor(@Nullable String authorName, @Nullable String authorEmail) {
this.authorName = Optional.fromNullable(authorName);
this.authorEmail = Optional.fromNullable(authorEmail);
return this;
}
/**
* Ends the current transaction by either committing the changes or discarding them depending on
* whether cancel is true or not.
*
* @return Boolean - true if the transaction was successfully closed
*/
@Override
protected Boolean _call() {
Preconditions.checkState(!(context instanceof GeogigTransaction),
"Cannot end a transaction within a transaction!");
Preconditions.checkArgument(transaction != null, "No transaction was specified!");
try {
if (!cancel) {
updateRefs();
}
} finally {
// Erase old refs
transaction.close();
}
// Success
return true;
}
private void updateRefs() {
final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call();
final String currentBranch;
if (currHead.isPresent() && currHead.get() instanceof SymRef) {
currentBranch = ((SymRef) currHead.get()).getTarget();
} else {
currentBranch = "";
}
ImmutableSet<Ref> changedRefs = getChangedRefs();
// Lock the repository
try {
refDatabase().lock();
} catch (TimeoutException e) {
Throwables.propagate(e);
}
try {
// Update refs
for (Ref ref : changedRefs) {
if (!ref.getName().startsWith(Ref.REFS_PREFIX)) {
continue;
}
Ref updatedRef = ref;
Optional<Ref> repoRef = command(RefParse.class).setName(ref.getName()).call();
if (repoRef.isPresent() && repositoryChanged(repoRef.get())) {
if (rebase) {
// Try to rebase
transaction.command(CheckoutOp.class).setSource(ref.getName())
.setForce(true).call();
try {
transaction.command(RebaseOp.class)
.setUpstream(Suppliers.ofInstance(repoRef.get().getObjectId()))
.call();
} catch (RebaseConflictsException e) {
Throwables.propagate(e);
}
updatedRef = transaction.command(RefParse.class).setName(ref.getName())
.call().get();
} else {
// sync transactions have to use merge to prevent divergent history
transaction.command(CheckoutOp.class).setSource(ref.getName())
.setForce(true).call();
try {
transaction.command(MergeOp.class)
.setAuthor(authorName.orNull(), authorEmail.orNull())
.addCommit(Suppliers.ofInstance(repoRef.get().getObjectId()))
.call();
} catch (NothingToCommitException e) {
// The repo commit is already in our history, this is a fast
// forward.
}
updatedRef = transaction.command(RefParse.class).setName(ref.getName())
.call().get();
}
}
LOGGER.debug(String.format("commit %s %s -> %s", ref.getName(), ref.getObjectId(),
updatedRef.getObjectId()));
command(UpdateRef.class).setName(ref.getName())
.setNewValue(updatedRef.getObjectId()).call();
if (currentBranch.equals(ref.getName())) {
// Update HEAD, WORK_HEAD and STAGE_HEAD
command(UpdateSymRef.class).setName(Ref.HEAD).setNewValue(ref.getName()).call();
command(UpdateRef.class).setName(Ref.WORK_HEAD)
.setNewValue(updatedRef.getObjectId()).call();
command(UpdateRef.class).setName(Ref.STAGE_HEAD)
.setNewValue(updatedRef.getObjectId()).call();
}
}
// TODO: What happens if there are unstaged or staged changes in the repository when
// a transaction is committed?
} finally {
// Unlock the repository
refDatabase().unlock();
}
}
private ImmutableSet<Ref> getChangedRefs() {
ImmutableSet<Ref> changedRefs = transaction.getChangedRefs();
return changedRefs;
}
private boolean repositoryChanged(Ref ref) {
Optional<Ref> transactionOriginal = transaction.command(RefParse.class)
.setName(ref.getName().replace("refs/", "orig/refs/")).call();
if (transactionOriginal.isPresent()) {
return !ref.getObjectId().equals(transactionOriginal.get().getObjectId());
}
// Ref was created in transaction and on the repo
return true;
}
}