package com.beijunyi.parallelgit.filesystem.commands; import java.io.IOException; import java.util.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.beijunyi.parallelgit.filesystem.GfsStatusProvider; import com.beijunyi.parallelgit.filesystem.GitFileSystem; import com.beijunyi.parallelgit.filesystem.exceptions.GfsCheckoutConflictException; import com.beijunyi.parallelgit.filesystem.exceptions.NoBranchException; import com.beijunyi.parallelgit.filesystem.merge.MergeConflict; import com.beijunyi.parallelgit.filesystem.merge.MergeNote; import com.beijunyi.parallelgit.utils.BranchUtils; import com.beijunyi.parallelgit.utils.CommitUtils; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.merge.*; import org.eclipse.jgit.revwalk.RevCommit; import static com.beijunyi.parallelgit.filesystem.commands.GfsMerge.Status.*; import static com.beijunyi.parallelgit.filesystem.io.GfsDefaultCheckout.checkout; import static com.beijunyi.parallelgit.filesystem.io.GfsTreeIterator.iterateRoot; import static com.beijunyi.parallelgit.filesystem.merge.GfsMergeCheckout.handleConflicts; import static com.beijunyi.parallelgit.filesystem.merge.MergeConflict.readConflicts; import static com.beijunyi.parallelgit.filesystem.merge.MergeNote.mergeSquash; import static com.beijunyi.parallelgit.utils.CommitUtils.*; import static com.beijunyi.parallelgit.utils.RefUtils.getBranchRef; import static java.util.Collections.*; import static org.eclipse.jgit.dircache.DirCache.newInCore; import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE; public class GfsMerge extends GfsCommand<GfsMerge.Result> { private MergeStrategy strategy = RECURSIVE; private MergeFormatter formatter = new MergeFormatter(); private DirCache cache = newInCore(); private String branch; private Ref branchRef; private RevCommit headCommit; private String source; private Ref sourceRef; private RevCommit sourceHeadCommit; private boolean fastForwardOnly = false; private boolean squash = false; private boolean commit = true; private PersonIdent committer; private String message; public GfsMerge(GitFileSystem gfs) { super(gfs); } @Nonnull @Override protected Result doExecute(GfsStatusProvider.Update update) throws IOException { prepareBranchHead(); prepareSource(); prepareMessage(); Result result = null; if(isUpToDate()) result = Result.upToDate(headCommit); else if(canBeFastForwarded()) result = fastForward(update); else if(fastForwardOnly) result = Result.aborted(); if(result != null) { return result; } return threeWayMerge(update); } @Nonnull public GfsMerge source(@Nullable String branch) { this.source = branch; return this; } @Nonnull public GfsMerge fastForwardOnly(boolean fastForwardOnly) { this.fastForwardOnly = fastForwardOnly; return this; } @Nonnull public GfsMerge squash(boolean squash) { this.squash = squash; return this; } @Nonnull public GfsMerge commit(boolean commit) { this.commit = commit; return this; } @Nonnull public GfsMerge committer(@Nullable PersonIdent committer) { this.committer = committer; return this; } @Nonnull public GfsMerge message(@Nullable String message) { this.message = message; return this; } @Nonnull public GfsMerge strategy(@Nullable MergeStrategy strategy) { this.strategy = strategy; return this; } private void prepareBranchHead() throws IOException { if(!status.isAttached()) throw new NoBranchException(); branch = status.branch(); branchRef = getBranchRef(branch, repo); if(status.isInitialized()) headCommit = status.commit(); } private void prepareSource() throws IOException { sourceRef = getBranchRef(source, repo); sourceHeadCommit = CommitUtils.getCommit(sourceRef, repo); } private void prepareMessage() throws IOException { if(message == null) { if(squash) { List<RevCommit> squashedCommits = listUnmergedCommits(sourceHeadCommit, headCommit, repo); message = new SquashMessageFormatter().format(squashedCommits, branchRef); } else { message = new MergeMessageFormatter().format(singletonList(sourceRef), branchRef); } } } private boolean isUpToDate() throws IOException { return headCommit != null && isMergedInto(sourceHeadCommit, headCommit, repo); } private boolean canBeFastForwarded() throws IOException { return headCommit == null || isMergedInto(headCommit, sourceHeadCommit, repo); } @Nonnull private Result fastForward(GfsStatusProvider.Update update) throws IOException { boolean success = tryCheckout(sourceHeadCommit.getTree()); Result result; if(!success) { result = Result.checkoutConflict(); } else if(squash) { update.mergeNote(mergeSquash(message)); result = Result.fastForwardSquashed(); } else { BranchUtils.merge(branch, sourceHeadCommit, sourceRef, FAST_FORWARD.toString(), repo); result = Result.fastForward(sourceHeadCommit); } return result; } @Nonnull private Result threeWayMerge(GfsStatusProvider.Update update) throws IOException { Merger merger = prepareMerger(); boolean success = merger.merge(headCommit, sourceHeadCommit); if(success) { return updateFileSystemStatus(update, merger); } else { Map<String, MergeConflict> conflicts; if(merger instanceof ResolveMerger) { ResolveMerger resolver = (ResolveMerger) merger; conflicts = readConflicts(resolver); writeConflicts(conflicts); writeConflictMergeNote(update, resolver.getUnmergedPaths()); } else { conflicts = emptyMap(); } return Result.conflicting(conflicts); } } private void writeConflictMergeNote(GfsStatusProvider.Update update, List<String> unmergedPaths) { message = new MergeMessageFormatter().formatWithConflicts(message, unmergedPaths); if(squash) { update.mergeNote(MergeNote.mergeSquashConflicting(message)); } else { update.mergeNote(MergeNote.mergeConflicting(sourceHeadCommit, message)); } } @Nonnull private Result updateFileSystemStatus(GfsStatusProvider.Update update, Merger merger) throws IOException { AnyObjectId treeId = merger.getResultTreeId(); checkout(gfs, treeId); RevCommit newCommit = null; if(commit && !squash) { prepareCommitter(); newCommit = createCommit(message, treeId, committer, committer, Arrays.asList(headCommit, sourceHeadCommit), repo); BranchUtils.merge(branch, newCommit, sourceRef, "Merge made by " + strategy.getName() + ".", repo); update.commit(newCommit); } if(!commit) { update.mergeNote(MergeNote.mergeNoCommit(sourceHeadCommit, message)); return Result.mergedNotCommitted(); } if(squash) { update.mergeNote(MergeNote.mergeSquash(message)); return Result.mergedSquashed(); } return Result.merged(newCommit); } private void writeConflicts(Map<String, MergeConflict> conflicts) throws IOException { handleConflicts(gfs, conflicts) .withFormatter(formatter) .checkout(cache); } private boolean tryCheckout(AnyObjectId tree) throws IOException { try { checkout(gfs, tree); } catch(GfsCheckoutConflictException e) { return false; } return true; } @Nonnull private Merger prepareMerger() throws IOException { Merger merger = strategy.newMerger(repo, true); if(merger instanceof ResolveMerger) { ResolveMerger resolver = ((ResolveMerger)merger); resolver.setDirCache(cache); resolver.setCommitNames(new String[] {"BASE", branchRef.getName(), sourceRef.getName()}); resolver.setWorkingTreeIterator(iterateRoot(gfs)); } return merger; } private void prepareCommitter() { if(committer == null) committer = new PersonIdent(repo); } public enum Status { CHECKOUT_CONFLICT, ABORTED, ALREADY_UP_TO_DATE, FAST_FORWARD, FAST_FORWARD_SQUASHED, MERGED, MERGED_SQUASHED, MERGED_NOT_COMMITTED, CONFLICTING } public static class Result implements GfsCommandResult { private final Status status; private final Map<String, MergeConflict> conflicts; private final RevCommit commit; private Result(Status status, Map<String, MergeConflict> conflicts, @Nullable RevCommit commit) { this.status = status; this.conflicts = conflicts; this.commit = commit; } private Result(Status status, @Nullable RevCommit commit) { this(status, Collections.<String, MergeConflict>emptyMap(), commit); } @Nonnull public static Result checkoutConflict() { return new Result(CHECKOUT_CONFLICT, null); } @Nonnull public static Result aborted() { return new Result(ABORTED, null); } @Nonnull public static Result upToDate(RevCommit commit) { return new Result(ALREADY_UP_TO_DATE, commit); } @Nonnull public static Result fastForward(RevCommit commit) { return new Result(FAST_FORWARD, commit); } @Nonnull public static Result fastForwardSquashed() { return new Result(FAST_FORWARD_SQUASHED, null); } @Nonnull public static Result merged(RevCommit commit) { return new Result(MERGED, commit); } @Nonnull public static Result mergedSquashed() { return new Result(MERGED_SQUASHED, null); } @Nonnull public static Result mergedNotCommitted() { return new Result(MERGED_NOT_COMMITTED, null); } @Nonnull public static Result conflicting(Map<String, MergeConflict> conflicts) { return new Result(CONFLICTING, conflicts, null); } @Override public boolean isSuccessful() { switch(status) { case ALREADY_UP_TO_DATE: case FAST_FORWARD: case FAST_FORWARD_SQUASHED: case MERGED: case MERGED_SQUASHED: case MERGED_NOT_COMMITTED: return true; default: return false; } } @Nonnull public Status getStatus() { return status; } @Nonnull public Map<String, MergeConflict> getConflicts() { return conflicts; } @Nullable public RevCommit getCommit() { return commit; } } }