/* Copyright (c) 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.web.api.commands; import static com.google.common.base.Preconditions.checkState; import javax.annotation.Nullable; import org.locationtech.geogig.api.CommitBuilder; import org.locationtech.geogig.api.Context; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.RevTreeBuilder; import org.locationtech.geogig.api.plumbing.FindCommonAncestor; import org.locationtech.geogig.api.plumbing.FindTreeChild; import org.locationtech.geogig.api.plumbing.RefParse; import org.locationtech.geogig.api.plumbing.ResolveTreeish; import org.locationtech.geogig.api.plumbing.RevObjectParse; import org.locationtech.geogig.api.plumbing.WriteBack; import org.locationtech.geogig.api.plumbing.merge.MergeScenarioReport; import org.locationtech.geogig.api.plumbing.merge.ReportMergeScenarioOp; import org.locationtech.geogig.api.porcelain.AddOp; import org.locationtech.geogig.api.porcelain.MergeOp; import org.locationtech.geogig.api.porcelain.MergeOp.MergeReport; import org.locationtech.geogig.web.api.AbstractWebAPICommand; import org.locationtech.geogig.web.api.CommandContext; import org.locationtech.geogig.web.api.CommandResponse; import org.locationtech.geogig.web.api.CommandSpecException; import org.locationtech.geogig.web.api.ResponseWriter; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Suppliers; import com.google.common.collect.Lists; /** * The interface for the Add operation in GeoGig. * * Web interface for {@link AddOp} */ public class RevertFeatureWebOp extends AbstractWebAPICommand { private String featurePath; private ObjectId oldCommitId; private ObjectId newCommitId; private Optional<String> authorName = Optional.absent(); private Optional<String> authorEmail = Optional.absent(); private Optional<String> commitMessage = Optional.absent(); private Optional<String> mergeMessage = Optional.absent(); /** * Mutator for the featurePath variable * * @param featurePath - the path to the feature you want to revert */ public void setPath(String featurePath) { this.featurePath = featurePath; } /** * Mutator for the oldCommitId variable * * @param oldCommitId - the commit that contains the version of the feature to revert to */ public void setOldCommitId(String oldCommitId) { this.oldCommitId = ObjectId.valueOf(oldCommitId); } /** * Mutator for the newCommitId variable * * @param newCommitId - the commit that contains the version of the feature that we want to undo */ public void setNewCommitId(String newCommitId) { this.newCommitId = ObjectId.valueOf(newCommitId); } /** * @param authorName the author of the merge commit */ public void setAuthorName(@Nullable String authorName) { this.authorName = Optional.fromNullable(authorName); } /** * @param authorEmail the email of the author of the merge commit */ public void setAuthorEmail(@Nullable String authorEmail) { this.authorEmail = Optional.fromNullable(authorEmail); } /** * @param commitMessage the commit message for the revert */ public void setCommitMessage(@Nullable String commitMessage) { this.commitMessage = Optional.fromNullable(commitMessage); } /** * @param mergeMessage the message for the merge of the revert commit */ public void setMergeMessage(@Nullable String mergeMessage) { this.mergeMessage = Optional.fromNullable(mergeMessage); } /** * Runs the command and builds the appropriate response * * @param context - the context to use for this command * * @throws CommandSpecException */ @Override public void run(CommandContext context) { if (this.getTransactionId() == null) { throw new CommandSpecException( "No transaction was specified, revert feature requires a transaction to preserve the stability of the repository."); } final Context geogig = this.getCommandLocator(context); Optional<RevTree> newTree = Optional.absent(); Optional<RevTree> oldTree = Optional.absent(); // get tree from new commit Optional<ObjectId> treeId = geogig.command(ResolveTreeish.class).setTreeish(newCommitId) .call(); Preconditions.checkState(treeId.isPresent(), "New commit id did not resolve to a valid tree."); newTree = geogig.command(RevObjectParse.class).setRefSpec(treeId.get().toString()) .call(RevTree.class); Preconditions.checkState(newTree.isPresent(), "Unable to read the new commit tree."); // get tree from old commit treeId = geogig.command(ResolveTreeish.class).setTreeish(oldCommitId).call(); Preconditions.checkState(treeId.isPresent(), "Old commit id did not resolve to a valid tree."); oldTree = geogig.command(RevObjectParse.class).setRefSpec(treeId.get().toString()) .call(RevTree.class); Preconditions.checkState(newTree.isPresent(), "Unable to read the old commit tree."); // get feature from old tree Optional<NodeRef> node = geogig.command(FindTreeChild.class).setParent(oldTree.get()) .setIndex(true).setChildPath(featurePath).call(); boolean delete = false; if (!node.isPresent()) { delete = true; node = geogig.command(FindTreeChild.class).setParent(newTree.get()).setIndex(true) .setChildPath(featurePath).call(); Preconditions.checkState(node.isPresent(), "The feature was not found in either commit tree."); } // get the new parent tree ObjectId metadataId = ObjectId.NULL; Optional<NodeRef> parentNode = geogig.command(FindTreeChild.class).setParent(newTree.get()) .setChildPath(node.get().getParentPath()).setIndex(true).call(); RevTreeBuilder treeBuilder = null; if (parentNode.isPresent()) { metadataId = parentNode.get().getMetadataId(); Optional<RevTree> parsed = geogig.command(RevObjectParse.class) .setObjectId(parentNode.get().getNode().getObjectId()).call(RevTree.class); checkState(parsed.isPresent(), "Parent tree couldn't be found in the repository."); treeBuilder = new RevTreeBuilder(geogig.stagingDatabase(), parsed.get()); treeBuilder.remove(node.get().getNode().getName()); } else { treeBuilder = new RevTreeBuilder(geogig.stagingDatabase()); } // put the old feature into the new tree if (!delete) { treeBuilder.put(node.get().getNode()); } ObjectId newTreeId = geogig.command(WriteBack.class) .setAncestor(newTree.get().builder(geogig.stagingDatabase())) .setChildPath(node.get().getParentPath()).setToIndex(true) .setTree(treeBuilder.build()).setMetadataId(metadataId).call(); // build new commit with parent of new commit and the newly built tree CommitBuilder builder = new CommitBuilder(); builder.setParentIds(Lists.newArrayList(newCommitId)); builder.setTreeId(newTreeId); builder.setAuthor(authorName.orNull()); builder.setAuthorEmail(authorEmail.orNull()); builder.setMessage(commitMessage.or("Reverted changes made to " + featurePath + " at " + newCommitId.toString())); RevCommit mapped = builder.build(); context.getGeoGIG().getRepository().objectDatabase().put(mapped); // merge commit into current branch final Optional<Ref> currHead = geogig.command(RefParse.class).setName(Ref.HEAD).call(); if (!currHead.isPresent()) { throw new CommandSpecException("Repository has no HEAD, can't merge."); } MergeOp merge = geogig.command(MergeOp.class); merge.setAuthor(authorName.orNull(), authorEmail.orNull()); merge.addCommit(Suppliers.ofInstance(mapped.getId())); merge.setMessage(mergeMessage.or("Merged revert of " + featurePath)); try { final MergeReport report = merge.call(); context.setResponseContent(new CommandResponse() { @Override public void write(ResponseWriter out) throws Exception { out.start(); out.writeMergeResponse(Optional.fromNullable(report.getMergeCommit()), report .getReport().get(), geogig, report.getOurs(), report.getPairs().get(0) .getTheirs(), report.getPairs().get(0).getAncestor()); out.finish(); } }); } catch (Exception e) { final RevCommit ours = context.getGeoGIG().getRepository() .getCommit(currHead.get().getObjectId()); final RevCommit theirs = context.getGeoGIG().getRepository().getCommit(mapped.getId()); final Optional<ObjectId> ancestor = geogig.command(FindCommonAncestor.class) .setLeft(ours).setRight(theirs).call(); context.setResponseContent(new CommandResponse() { final MergeScenarioReport report = geogig.command(ReportMergeScenarioOp.class) .setMergeIntoCommit(ours).setToMergeCommit(theirs).call(); @Override public void write(ResponseWriter out) throws Exception { out.start(); Optional<RevCommit> mergeCommit = Optional.absent(); out.writeMergeResponse(mergeCommit, report, geogig, ours.getId(), theirs.getId(), ancestor.get()); out.finish(); } }); } } }