package com.atlassian.jgitflow.core.command;
import java.io.File;
import java.io.IOException;
import java.util.List;
import com.atlassian.jgitflow.core.GitFlowConfiguration;
import com.atlassian.jgitflow.core.JGitFlowConstants;
import com.atlassian.jgitflow.core.exception.*;
import com.atlassian.jgitflow.core.extension.FeatureFinishExtension;
import com.atlassian.jgitflow.core.extension.impl.EmptyFeatureFinishExtension;
import com.atlassian.jgitflow.core.extension.impl.MergeProcessExtensionWrapper;
import com.atlassian.jgitflow.core.util.FileHelper;
import com.atlassian.jgitflow.core.util.GitHelper;
import com.atlassian.jgitflow.core.util.IterableHelper;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.StringUtils;
import static com.atlassian.jgitflow.core.util.Preconditions.checkState;
/**
* Finish a feature.
* <p>
* This will merge the feature into develop and set the local branch to develop.
* </p>
* <p></p>
* Examples ({@code flow} is a {@link com.atlassian.jgitflow.core.JGitFlow} instance):
* <p></p>
* Finish a feature:
* <p></p>
* <pre>
* flow.featureFinish("feature").call();
* </pre>
* <p></p>
* Don't delete the local feature branch
* <p></p>
* <pre>
* flow.featureFinish("feature").setKeepBranch(true).call();
* </pre>
* <p></p>
* Squash all commits on the feature branch into one before merging
* <p></p>
* <pre>
* flow.featureFinish("feature").setSquash(true).call();
* </pre>
*/
public class FeatureFinishCommand extends AbstractBranchMergingCommand<FeatureFinishCommand, MergeResult>
{
private static final String SHORT_NAME = "feature-finish";
private boolean rebase;
private boolean squash;
private boolean noMerge;
private boolean suppressFastForward;
private FeatureFinishExtension extension;
/**
* Create a new feature finish command instance.
* <p></p>
* An instance of this class is usually obtained by calling {@link com.atlassian.jgitflow.core.JGitFlow#featureFinish(String)}
*
* @param git The git instance to use
* @param gfConfig The GitFlowConfiguration to use
*/
public FeatureFinishCommand(String branchName, Git git, GitFlowConfiguration gfConfig)
{
super(branchName, git, gfConfig);
checkState(!StringUtils.isEmptyOrNull(branchName));
this.rebase = false;
this.squash = false;
this.noMerge = false;
this.extension = new EmptyFeatureFinishExtension();
}
/**
* @return nothing
* @throws com.atlassian.jgitflow.core.exception.NotInitializedException
* @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
* @throws com.atlassian.jgitflow.core.exception.LocalBranchMissingException
* @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
* @throws com.atlassian.jgitflow.core.exception.DirtyWorkingTreeException
* @throws com.atlassian.jgitflow.core.exception.MergeConflictsNotResolvedException
* @throws com.atlassian.jgitflow.core.exception.BranchOutOfDateException
*/
@Override
public MergeResult call() throws NotInitializedException, JGitFlowGitAPIException, LocalBranchMissingException, JGitFlowIOException, DirtyWorkingTreeException, MergeConflictsNotResolvedException, BranchOutOfDateException, JGitFlowExtensionException, GitAPIException
{
MergeResult mergeResult = createEmptyMergeResult();
String prefixedBranchName = runBeforeAndGetPrefixedBranchName(extension.before(), JGitFlowConstants.PREFIXES.FEATURE);
enforcer().requireGitFlowInitialized();
enforcer().requireLocalBranchExists(prefixedBranchName);
//check to see if we're restoring from a merge conflict
File flowDir = new File(git.getRepository().getDirectory(), JGitFlowConstants.GITFLOW_DIR);
File mergeBase = new File(flowDir, JGitFlowConstants.MERGE_BASE);
if (!noMerge && mergeBase.exists())
{
reporter.debugText(getCommandName(), "restoring from merge conflict. base: " + mergeBase.getAbsolutePath());
if (GitHelper.workingTreeIsClean(git, isAllowUntracked()).isClean())
{
//check to see if the merge was done
String finishBase = FileHelper.readFirstLine(mergeBase);
if (GitHelper.isMergedInto(git, prefixedBranchName, finishBase))
{
mergeBase.delete();
cleanupBranchesIfNeeded(gfConfig.getDevelop(), prefixedBranchName);
reporter.endCommand();
return null;
}
else
{
mergeBase.delete();
}
}
else
{
reporter.errorText(getCommandName(), "Merge conflicts are not resolved");
reporter.endCommand();
throw new MergeConflictsNotResolvedException("Merge conflicts are not resolved");
}
}
//not restoring a merge, continue
enforcer().requireCleanWorkingTree(isAllowUntracked());
try
{
doFetchIfNeeded(extension);
ensureLocalBranchesNotBehindRemotes(prefixedBranchName, prefixedBranchName, gfConfig.getDevelop());
//checkout the branch to merge just so we can run any extensions that need to be on this branch
checkoutTopicBranch(prefixedBranchName, extension);
if (rebase)
{
runExtensionCommands(extension.beforeRebase());
FeatureRebaseCommand rebaseCommand = new FeatureRebaseCommand(getBranchName(), git, gfConfig);
rebaseCommand.setAllowUntracked(isAllowUntracked()).call();
runExtensionCommands(extension.afterRebase());
}
if (!noMerge)
{
RevCommit developCommit = GitHelper.getLatestCommit(git, gfConfig.getDevelop());
RevCommit featureCommit = GitHelper.getLatestCommit(git, prefixedBranchName);
List<RevCommit> commitList = IterableHelper.asList(git.log().setMaxCount(2).addRange(developCommit, featureCommit).call());
MergeProcessExtensionWrapper developExtension = new MergeProcessExtensionWrapper(extension.beforeDevelopCheckout(), extension.afterDevelopCheckout(), extension.beforeDevelopMerge(), extension.afterDevelopMerge());
if (commitList.size() < 2)
{
MergeCommand.FastForwardMode ffMode = suppressFastForward ? MergeCommand.FastForwardMode.NO_FF : MergeCommand.FastForwardMode.FF;
mergeResult = doMerge(prefixedBranchName, gfConfig.getDevelop(), developExtension, false, ffMode);
}
else
{
mergeResult = doMerge(prefixedBranchName, gfConfig.getDevelop(), developExtension, squash);
}
if (null == mergeResult || mergeResult.getMergeStatus().equals(MergeResult.MergeStatus.FAILED) || mergeResult.getMergeStatus().equals(MergeResult.MergeStatus.CONFLICTING))
{
FileHelper.createParentDirs(mergeBase);
FileUtils.createNewFile(mergeBase);
FileHelper.writeStringToFile(gfConfig.getDevelop(), mergeBase);
reporter.endCommand();
reporter.flush();
throw new MergeConflictsNotResolvedException("merge conflicts exist, please resolve!");
}
}
doPushIfNeeded(extension, false, gfConfig.getDevelop(), prefixedBranchName);
cleanupBranchesIfNeeded(gfConfig.getDevelop(), prefixedBranchName);
reporter.infoText(getCommandName(), "checking out '" + gfConfig.getDevelop() + "'");
git.checkout().setName(gfConfig.getDevelop()).call();
reporter.endCommand();
runExtensionCommands(extension.after());
return mergeResult;
}
catch (GitAPIException e)
{
reporter.endCommand();
throw new JGitFlowGitAPIException(e);
}
catch (IOException e)
{
reporter.endCommand();
throw new JGitFlowIOException(e);
}
finally
{
reporter.endCommand();
reporter.flush();
}
}
/**
* Set whether to perform a git rebase on the feature before doing the merge
*
* @param rebase {@code true} to do a rebase, {@code false}(default) otherwise
* @return {@code this}
*/
public FeatureFinishCommand setRebase(boolean rebase)
{
this.rebase = rebase;
return this;
}
/**
* Set whether to squash all commits into a single commit before the merge
*
* @param squash {@code true} to squash, {@code false}(default) otherwise
* @return {@code this}
*/
public FeatureFinishCommand setSquash(boolean squash)
{
this.squash = squash;
return this;
}
public FeatureFinishCommand setNoMerge(boolean noMerge)
{
this.noMerge = noMerge;
return this;
}
public FeatureFinishCommand setSuppressFastForward(boolean suppressFastForward)
{
this.suppressFastForward = suppressFastForward;
return this;
}
public FeatureFinishCommand setExtension(FeatureFinishExtension extension)
{
this.extension = extension;
return this;
}
@Override
protected String getCommandName()
{
return SHORT_NAME;
}
}