/* 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: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.api.porcelain; 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 java.util.Collection; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import org.locationtech.geogig.api.AbstractGeoGigOp; 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.RevObject.TYPE; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.RevTreeBuilder; import org.locationtech.geogig.api.SymRef; import org.locationtech.geogig.api.hooks.Hookable; 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.RevParse; import org.locationtech.geogig.api.plumbing.UpdateRef; import org.locationtech.geogig.api.plumbing.UpdateSymRef; import org.locationtech.geogig.api.plumbing.WriteBack; import org.locationtech.geogig.api.plumbing.merge.Conflict; import org.locationtech.geogig.api.porcelain.CheckoutException.StatusCode; import org.locationtech.geogig.api.porcelain.ConfigOp.ConfigAction; import org.locationtech.geogig.api.porcelain.ConfigOp.ConfigScope; import org.locationtech.geogig.di.CanRunDuringConflict; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.collect.Sets; /** * Updates objects in the working tree to match the version in the index or the specified tree. If * no {@link #addPath paths} are given, will also update {@link Ref#HEAD HEAD} to set the specified * branch as the current branch, or to the specified commit if the given {@link #setSource origin} * is a commit id instead of a branch name, in which case HEAD will be a plain ref instead of a * symbolic ref, hence making it a "dettached head". */ @CanRunDuringConflict @Hookable(name = "checkout") public class CheckoutOp extends AbstractGeoGigOp<CheckoutResult> { private String branchOrCommit; private Set<String> paths; private boolean force = false; private boolean ours; private boolean theirs; public CheckoutOp() { paths = Sets.newTreeSet(); } public CheckoutOp setSource(@Nullable final String branchOrCommit) { this.branchOrCommit = branchOrCommit; return this; } public CheckoutOp setForce(final boolean force) { this.force = force; return this; } public CheckoutOp addPath(final CharSequence path) { checkNotNull(path); paths.add(path.toString()); return this; } public CheckoutOp setOurs(final boolean ours) { this.ours = ours; return this; } public CheckoutOp setTheirs(final boolean theirs) { this.theirs = theirs; return this; } public CheckoutOp addPaths(final Collection<? extends CharSequence> paths) { checkNotNull(paths); for (CharSequence path : paths) { addPath(path); } return this; } /** * @return the id of the new work tree */ @Override protected CheckoutResult _call() { checkState(branchOrCommit != null || !paths.isEmpty(), "No branch, tree, or path were specified"); checkArgument(!(ours && theirs), "Cannot use both --ours and --theirs."); checkArgument((ours == theirs) || branchOrCommit == null, "--ours/--theirs is incompatible with switching branches."); CheckoutResult result = new CheckoutResult(); List<Conflict> conflicts = stagingDatabase().getConflicts(null, null); if (!paths.isEmpty()) { result.setResult(CheckoutResult.Results.UPDATE_OBJECTS); Optional<RevTree> tree = Optional.absent(); List<String> unmerged = lookForUnmerged(conflicts, paths); if (!unmerged.isEmpty()) { if (!(force || ours || theirs)) { StringBuilder msg = new StringBuilder(); for (String path : unmerged) { msg.append("error: path " + path + " is unmerged.\n"); } throw new CheckoutException(msg.toString(), StatusCode.UNMERGED_PATHS); } } if (branchOrCommit != null) { Optional<ObjectId> id = command(ResolveTreeish.class).setTreeish(branchOrCommit) .call(); checkState(id.isPresent(), "'" + branchOrCommit + "' not found in repository."); tree = command(RevObjectParse.class).setObjectId(id.get()).call(RevTree.class); } else { tree = Optional.of(index().getTree()); } Optional<RevTree> mainTree = tree; for (String st : paths) { if (unmerged.contains(st)) { if (ours || theirs) { String refspec = ours ? Ref.ORIG_HEAD : Ref.MERGE_HEAD; Optional<ObjectId> treeId = command(ResolveTreeish.class).setTreeish( refspec).call(); if (treeId.isPresent()) { tree = command(RevObjectParse.class).setObjectId(treeId.get()).call( RevTree.class); } } else {// --force continue; } } else { tree = mainTree; } Optional<NodeRef> node = command(FindTreeChild.class).setParent(tree.get()) .setIndex(true).setChildPath(st).call(); if ((ours || theirs) && !node.isPresent()) { // remove the node. command(RemoveOp.class).addPathToRemove(st).call(); } else { checkState(node.isPresent(), "pathspec '" + st + "' didn't match a feature in the tree"); if (node.get().getType() == TYPE.TREE) { RevTreeBuilder treeBuilder = new RevTreeBuilder(stagingDatabase(), workingTree().getTree()); treeBuilder.remove(st); treeBuilder.put(node.get().getNode()); RevTree newRoot = treeBuilder.build(); stagingDatabase().put(newRoot); workingTree().updateWorkHead(newRoot.getId()); } else { ObjectId metadataId = ObjectId.NULL; Optional<NodeRef> parentNode = command(FindTreeChild.class) .setParent(workingTree().getTree()) .setChildPath(node.get().getParentPath()).setIndex(true).call(); RevTreeBuilder treeBuilder = null; if (parentNode.isPresent()) { metadataId = parentNode.get().getMetadataId(); Optional<RevTree> parsed = 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(stagingDatabase(), parsed.get()); treeBuilder.remove(node.get().getNode().getName()); } else { treeBuilder = new RevTreeBuilder(stagingDatabase()); } treeBuilder.put(node.get().getNode()); ObjectId newTreeId = command(WriteBack.class) .setAncestor( workingTree().getTree().builder(stagingDatabase())) .setChildPath(node.get().getParentPath()).setToIndex(true) .setTree(treeBuilder.build()).setMetadataId(metadataId).call(); workingTree().updateWorkHead(newTreeId); } } } } else { if (!conflicts.isEmpty()) { if (!(force)) { StringBuilder msg = new StringBuilder(); for (Conflict conflict : conflicts) { msg.append("error: " + conflict.getPath() + " needs merge.\n"); } msg.append("You need to resolve your index first.\n"); throw new CheckoutException(msg.toString(), StatusCode.UNMERGED_PATHS); } } Optional<Ref> targetRef = Optional.absent(); Optional<ObjectId> targetCommitId = Optional.absent(); Optional<ObjectId> targetTreeId = Optional.absent(); targetRef = command(RefParse.class).setName(branchOrCommit).call(); if (targetRef.isPresent()) { ObjectId commitId = targetRef.get().getObjectId(); if (targetRef.get().getName().startsWith(Ref.REMOTES_PREFIX)) { String remoteName = targetRef.get().getName(); remoteName = remoteName.substring(Ref.REMOTES_PREFIX.length(), targetRef.get() .getName().lastIndexOf("/")); if (branchOrCommit.contains(remoteName + '/')) { RevCommit commit = command(RevObjectParse.class).setObjectId(commitId) .call(RevCommit.class).get(); targetTreeId = Optional.of(commit.getTreeId()); targetCommitId = Optional.of(commit.getId()); targetRef = Optional.absent(); } else { Ref branch = command(BranchCreateOp.class) .setName(targetRef.get().localName()) .setSource(commitId.toString()).call(); command(ConfigOp.class).setAction(ConfigAction.CONFIG_SET) .setScope(ConfigScope.LOCAL) .setName("branches." + branch.localName() + ".remote") .setValue(remoteName).call(); command(ConfigOp.class).setAction(ConfigAction.CONFIG_SET) .setScope(ConfigScope.LOCAL) .setName("branches." + branch.localName() + ".merge") .setValue(targetRef.get().getName()).call(); targetRef = Optional.of(branch); result.setResult(CheckoutResult.Results.CHECKOUT_REMOTE_BRANCH); result.setRemoteName(remoteName); } } if (commitId.isNull()) { targetTreeId = Optional.of(ObjectId.NULL); targetCommitId = Optional.of(ObjectId.NULL); } else { Optional<RevCommit> parsed = command(RevObjectParse.class) .setObjectId(commitId).call(RevCommit.class); checkState(parsed.isPresent()); checkState(parsed.get() instanceof RevCommit); RevCommit commit = parsed.get(); targetCommitId = Optional.of(commit.getId()); targetTreeId = Optional.of(commit.getTreeId()); } } else { final Optional<ObjectId> addressed = command(RevParse.class).setRefSpec( branchOrCommit).call(); checkArgument(addressed.isPresent(), "source '" + branchOrCommit + "' not found in repository"); RevCommit commit = command(RevObjectParse.class).setObjectId(addressed.get()) .call(RevCommit.class).get(); targetTreeId = Optional.of(commit.getTreeId()); targetCommitId = Optional.of(commit.getId()); } if (targetTreeId.isPresent()) { if (!force) { if (!index().isClean() || !workingTree().isClean()) { throw new CheckoutException(StatusCode.LOCAL_CHANGES_NOT_COMMITTED); } } // update work tree ObjectId treeId = targetTreeId.get(); workingTree().updateWorkHead(treeId); index().updateStageHead(treeId); result.setNewTree(treeId); if (targetRef.isPresent()) { // update HEAD Ref target = targetRef.get(); String refName; if (target instanceof SymRef) {// beware of cyclic refs, peel symrefs refName = ((SymRef) target).getTarget(); } else { refName = target.getName(); } command(UpdateSymRef.class).setName(Ref.HEAD).setNewValue(refName).call(); result.setNewRef(targetRef.get()); result.setOid(targetCommitId.get()); result.setResult(CheckoutResult.Results.CHECKOUT_LOCAL_BRANCH); } else { // set HEAD to a dettached state ObjectId commitId = targetCommitId.get(); command(UpdateRef.class).setName(Ref.HEAD).setNewValue(commitId).call(); result.setOid(commitId); result.setResult(CheckoutResult.Results.DETACHED_HEAD); } Optional<Ref> ref = command(RefParse.class).setName(Ref.MERGE_HEAD).call(); if (ref.isPresent()) { command(UpdateRef.class).setName(Ref.MERGE_HEAD).setDelete(true).call(); } return result; } } result.setNewTree(workingTree().getTree().getId()); return result; } private List<String> lookForUnmerged(List<Conflict> conflicts, Set<String> paths) { List<String> unmerged = Lists.newArrayList(); for (String path : paths) { for (Conflict conflict : conflicts) { if (conflict.getPath().equals(path)) { unmerged.add(path); break; } } } return unmerged; } }