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;
}
}
}