/* 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.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.FeatureInfo; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.ProgressListener; import org.locationtech.geogig.api.Ref; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.SubProgressListener; import org.locationtech.geogig.api.SymRef; import org.locationtech.geogig.api.plumbing.DiffTree; import org.locationtech.geogig.api.plumbing.FindCommonAncestor; import org.locationtech.geogig.api.plumbing.RefParse; import org.locationtech.geogig.api.plumbing.ResolveBranchId; import org.locationtech.geogig.api.plumbing.UpdateRef; import org.locationtech.geogig.api.plumbing.UpdateSymRef; import org.locationtech.geogig.api.plumbing.diff.DiffEntry; import org.locationtech.geogig.api.plumbing.merge.CheckMergeScenarioOp; import org.locationtech.geogig.api.plumbing.merge.Conflict; import org.locationtech.geogig.api.plumbing.merge.ConflictsWriteOp; import org.locationtech.geogig.api.plumbing.merge.MergeScenarioReport; import org.locationtech.geogig.api.plumbing.merge.ReportMergeScenarioOp; import org.locationtech.geogig.api.plumbing.merge.SaveMergeCommitMessageOp; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.collect.Lists; /** * * Merge two or more histories together. * */ public class MergeOp extends AbstractGeoGigOp<MergeOp.MergeReport> { private List<ObjectId> commits = new ArrayList<ObjectId>();; private String message = null; private boolean ours; private boolean theirs; private boolean noCommit; private Optional<String> authorName = Optional.absent(); private Optional<String> authorEmail = Optional.absent(); /** * @param message the message for the merge commit * @return {@code this} */ public MergeOp setMessage(final String message) { this.message = message; return this; } /** * Adds a commit whose history should be merged. * * @param onto a supplier for the commit id * @return {@code this} */ public MergeOp addCommit(final Supplier<ObjectId> commit) { Preconditions.checkNotNull(commit); this.commits.add(commit.get()); return this; } /** * * @param ours true if the "ours" strategy should be used * @return {@code this} */ public MergeOp setOurs(boolean ours) { this.ours = ours; return this; } /** * * @param ours true if the "theirs" strategy should be used * @return {@code this} */ public MergeOp setTheirs(boolean theirs) { this.theirs = theirs; return this; } /** * * @param ours true if no commit should be made after the merge, leaving just the index with the * merge result * @return {@code this} */ public MergeOp setNoCommit(boolean noCommit) { this.noCommit = noCommit; return this; } /** * * @param author the author of the commit * @param email email of author * @return {@code this} */ public MergeOp setAuthor(@Nullable String authorName, @Nullable String authorEmail) { this.authorName = Optional.fromNullable(authorName); this.authorEmail = Optional.fromNullable(authorEmail); return this; } /** * Executes the merge operation. * * @return always {@code true} */ @Override protected MergeReport _call() throws RuntimeException { Preconditions.checkArgument(commits.size() > 0, "No commits specified for merge."); Preconditions.checkArgument(!(ours && theirs), "Cannot use both --ours and --theirs."); final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call(); Preconditions.checkState(currHead.isPresent(), "Repository has no HEAD, can't rebase."); Ref headRef = currHead.get(); ObjectId oursId = headRef.getObjectId(); // Preconditions.checkState(currHead.get() instanceof SymRef, // "Can't rebase from detached HEAD"); // SymRef headRef = (SymRef) currHead.get(); // final String currentBranch = headRef.getTarget(); getProgressListener().started(); boolean fastForward = true; boolean changed = false; Optional<MergeScenarioReport> mergeScenario = Optional.absent(); List<CommitAncestorPair> pairs = Lists.newArrayList(); boolean hasConflictsOrAutomerge; List<RevCommit> revCommits = Lists.newArrayList(); if (!ObjectId.NULL.equals(headRef.getObjectId())) { revCommits.add(repository().getCommit(headRef.getObjectId())); } for (ObjectId commitId : commits) { revCommits.add(repository().getCommit(commitId)); } hasConflictsOrAutomerge = command(CheckMergeScenarioOp.class).setCommits(revCommits).call() .booleanValue(); if (hasConflictsOrAutomerge && !theirs) { Preconditions.checkState(commits.size() < 2, "Conflicted merge.\nCannot merge more than two commits when conflicts exist" + " or features have been modified in several histories"); RevCommit headCommit = repository().getCommit(headRef.getObjectId()); ObjectId commitId = commits.get(0); Preconditions.checkArgument(!ObjectId.NULL.equals(commitId), "Cannot merge a NULL commit."); Preconditions.checkArgument(repository().commitExists(commitId), "Not a valid commit: " + commitId.toString()); final RevCommit targetCommit = repository().getCommit(commitId); Optional<ObjectId> ancestorCommit = command(FindCommonAncestor.class) .setLeft(headCommit).setRight(targetCommit).call(); pairs.add(new CommitAncestorPair(commitId, ancestorCommit.get())); mergeScenario = Optional.of(command(ReportMergeScenarioOp.class) .setMergeIntoCommit(headCommit).setToMergeCommit(targetCommit).call()); List<FeatureInfo> merged = mergeScenario.get().getMerged(); for (FeatureInfo feature : merged) { this.workingTree().insert(NodeRef.parentPath(feature.getPath()), feature.getFeature()); Iterator<DiffEntry> unstaged = workingTree().getUnstaged(null); index().stage(getProgressListener(), unstaged, 0); changed = true; fastForward = false; } List<DiffEntry> unconflicting = mergeScenario.get().getUnconflicted(); if (!unconflicting.isEmpty()) { index().stage(getProgressListener(), unconflicting.iterator(), 0); changed = true; fastForward = false; } workingTree().updateWorkHead(index().getTree().getId()); List<Conflict> conflicts = mergeScenario.get().getConflicts(); if (!ours && !conflicts.isEmpty()) { // In case we use the "ours" strategy, we do nothing. We ignore conflicting // changes and leave the current elements command(UpdateRef.class).setName(Ref.MERGE_HEAD).setNewValue(commitId).call(); command(UpdateRef.class).setName(Ref.ORIG_HEAD).setNewValue(headCommit.getId()) .call(); command(ConflictsWriteOp.class).setConflicts(conflicts).call(); StringBuilder msg = new StringBuilder(); Optional<Ref> ref = command(ResolveBranchId.class).setObjectId(commitId).call(); if (ref.isPresent()) { msg.append("Merge branch " + ref.get().getName()); } else { msg.append("Merge commit '" + commitId.toString() + "'. "); } msg.append("\n\nConflicts:\n"); for (Conflict conflict : mergeScenario.get().getConflicts()) { msg.append("\t" + conflict.getPath() + "\n"); } command(SaveMergeCommitMessageOp.class).setMessage(msg.toString()).call(); StringBuilder sb = new StringBuilder(); for (Conflict conflict : conflicts) { sb.append("CONFLICT: Merge conflict in " + conflict.getPath() + "\n"); } sb.append("Automatic merge failed. Fix conflicts and then commit the result.\n"); throw new MergeConflictsException(sb.toString(), headCommit.getId(), commitId); } } else { Preconditions.checkState(!hasConflictsOrAutomerge || commits.size() < 2, "Conflicted merge.\nCannot merge more than two commits when conflicts exist" + " or features have been modified in several histories"); for (ObjectId commitId : commits) { ProgressListener subProgress = subProgress(100.f / commits.size()); Preconditions.checkArgument(!ObjectId.NULL.equals(commitId), "Cannot merge a NULL commit."); Preconditions.checkArgument(repository().commitExists(commitId), "Not a valid commit: " + commitId.toString()); subProgress.started(); if (ObjectId.NULL.equals(headRef.getObjectId())) { // Fast-forward if (headRef instanceof SymRef) { final String currentBranch = ((SymRef) headRef).getTarget(); command(UpdateRef.class).setName(currentBranch).setNewValue(commitId) .call(); headRef = (SymRef) command(UpdateSymRef.class).setName(Ref.HEAD) .setNewValue(currentBranch).call().get(); } else { headRef = command(UpdateRef.class).setName(headRef.getName()) .setNewValue(commitId).call().get(); } workingTree().updateWorkHead(commitId); index().updateStageHead(commitId); subProgress.complete(); changed = true; continue; } RevCommit headCommit = repository().getCommit(headRef.getObjectId()); final RevCommit targetCommit = repository().getCommit(commitId); Optional<ObjectId> ancestorCommit = command(FindCommonAncestor.class) .setLeft(headCommit).setRight(targetCommit).call(); pairs.add(new CommitAncestorPair(commitId, ancestorCommit.get())); subProgress.setProgress(10.f); Preconditions.checkState(ancestorCommit.isPresent(), "No ancestor commit could be found."); if (commits.size() == 1) { mergeScenario = Optional.of(command(ReportMergeScenarioOp.class) .setMergeIntoCommit(headCommit).setToMergeCommit(targetCommit).call()); if (ancestorCommit.get().equals(headCommit.getId())) { // Fast-forward if (headRef instanceof SymRef) { final String currentBranch = ((SymRef) headRef).getTarget(); command(UpdateRef.class).setName(currentBranch).setNewValue(commitId) .call(); headRef = (SymRef) command(UpdateSymRef.class).setName(Ref.HEAD) .setNewValue(currentBranch).call().get(); } else { headRef = command(UpdateRef.class).setName(headRef.getName()) .setNewValue(commitId).call().get(); } workingTree().updateWorkHead(commitId); index().updateStageHead(commitId); subProgress.complete(); changed = true; continue; } else if (ancestorCommit.get().equals(commitId)) { continue; } } // get changes Iterator<DiffEntry> diff = command(DiffTree.class).setOldTree(ancestorCommit.get()) .setNewTree(targetCommit.getId()).setReportTrees(true).call(); // stage changes index().stage(new SubProgressListener(subProgress, 100.f), diff, 0); changed = true; fastForward = false; workingTree().updateWorkHead(index().getTree().getId()); subProgress.complete(); } } if (!changed) { throw new NothingToCommitException("The branch has already been merged."); } RevCommit mergeCommit = commit(fastForward); MergeReport result = new MergeReport(mergeCommit, mergeScenario, oursId, pairs); return result; } private RevCommit commit(boolean fastForward) { RevCommit mergeCommit; if (fastForward) { mergeCommit = repository().getCommit(commits.get(0)); } else { String commitMessage = message; if (commitMessage == null) { commitMessage = ""; for (ObjectId commit : commits) { Optional<Ref> ref = command(ResolveBranchId.class).setObjectId(commit).call(); if (ref.isPresent()) { commitMessage += "Merge branch " + ref.get().getName(); } else { commitMessage += "Merge commit '" + commit.toString() + "'. "; } } } if (noCommit) { final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call(); SymRef headRef = (SymRef) currHead.get(); RevCommit headCommit = repository().getCommit(headRef.getObjectId()); command(UpdateRef.class).setName(Ref.MERGE_HEAD).setNewValue(commits.get(0)).call(); // TODO:how to store multiple ids when octopus merge command(UpdateRef.class).setName(Ref.ORIG_HEAD).setNewValue(headCommit.getId()) .call(); mergeCommit = headCommit; command(SaveMergeCommitMessageOp.class).setMessage(commitMessage).call(); } else { mergeCommit = command(CommitOp.class).setAllowEmpty(true).setMessage(commitMessage) .addParents(commits).setAuthor(authorName.orNull(), authorEmail.orNull()) .call(); } } getProgressListener().complete(); return mergeCommit; } public class CommitAncestorPair { private ObjectId theirs; private ObjectId ancestor; public ObjectId getTheirs() { return theirs; } public ObjectId getAncestor() { return ancestor; } public CommitAncestorPair(ObjectId theirs, ObjectId ancestor) { this.theirs = theirs; this.ancestor = ancestor; } } public class MergeReport { private RevCommit mergeCommit; private Optional<MergeScenarioReport> report; private ObjectId ours; private List<CommitAncestorPair> pairs; public RevCommit getMergeCommit() { return mergeCommit; } public ObjectId getOurs() { return ours; } public List<CommitAncestorPair> getPairs() { return pairs; } public Optional<MergeScenarioReport> getReport() { return report; } public MergeReport(RevCommit mergeCommit, Optional<MergeScenarioReport> report, ObjectId ours, List<CommitAncestorPair> pairs) { this.mergeCommit = mergeCommit; this.report = report; this.ours = ours; this.pairs = pairs; } } }