Package org.locationtech.geogig.api.porcelain

Source Code of org.locationtech.geogig.api.porcelain.MergeOp$MergeReport

/* 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;
        }
    }
}
TOP

Related Classes of org.locationtech.geogig.api.porcelain.MergeOp$MergeReport

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.