/*******************************************************************************
* Copyright (C) 2015, Max Hohenegger <eclipse@hohenegger.eu>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.gitflow.op;
import static java.lang.String.format;
import static org.eclipse.egit.gitflow.Activator.error;
import static org.eclipse.jgit.api.MergeCommand.FastForwardMode.NO_FF;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.egit.core.internal.job.RuleUtil;
import org.eclipse.egit.core.op.BranchOperation;
import org.eclipse.egit.core.op.CreateLocalBranchOperation;
import org.eclipse.egit.core.op.DeleteBranchOperation;
import org.eclipse.egit.core.op.FetchOperation;
import org.eclipse.egit.core.op.IEGitOperation;
import org.eclipse.egit.core.op.MergeOperation;
import org.eclipse.egit.gitflow.GitFlowRepository;
import org.eclipse.egit.gitflow.internal.CoreText;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.CheckoutResult;
import org.eclipse.jgit.api.CheckoutResult.Status;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.RevWalkUtils;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RemoteConfig;
/**
* Common logic for Git Flow operations.
*/
// TODO: This class should be called AbstractGitFlowOperation for consistency
abstract public class GitFlowOperation implements IEGitOperation {
/**
* git path separator
*/
public static final String SEP = "/"; //$NON-NLS-1$
/**
* repository that is operated on.
*/
protected GitFlowRepository repository;
/**
* the status of the latest merge from this operation
*/
// TODO: Remove from this class. Not all GitFlow operations involve a merge
protected MergeResult mergeResult;
/**
* @param repository
*/
public GitFlowOperation(GitFlowRepository repository) {
this.repository = repository;
}
@Override
public ISchedulingRule getSchedulingRule() {
return RuleUtil.getRule(repository.getRepository());
}
/**
* @param branchName
* @param sourceCommit
* @return operation constructed from parameters
*/
protected CreateLocalBranchOperation createBranchFromHead(
String branchName, RevCommit sourceCommit) {
return new CreateLocalBranchOperation(repository.getRepository(),
branchName, sourceCommit);
}
/**
* git flow * start
*
* @param monitor
* @param branchName
* @param sourceCommit
* @throws CoreException
*/
protected void start(IProgressMonitor monitor, String branchName,
RevCommit sourceCommit) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 2);
CreateLocalBranchOperation branchOperation = createBranchFromHead(
branchName, sourceCommit);
branchOperation.execute(progress.newChild(1));
BranchOperation checkoutOperation = new BranchOperation(
repository.getRepository(), branchName);
checkoutOperation.execute(progress.newChild(1));
}
/**
* git flow * finish
*
* @param monitor
* @param branchName
* @param squash
* @param fastForwardSingleCommit Has no effect if {@code squash} is true.
* @param keepBranch
* @throws CoreException
* @since 4.1
*/
protected void finish(IProgressMonitor monitor, String branchName,
boolean squash, boolean keepBranch, boolean fastForwardSingleCommit)
throws CoreException {
try {
SubMonitor progress = SubMonitor.convert(monitor, 2);
mergeResult = mergeTo(progress.newChild(1), branchName,
repository.getConfig()
.getDevelop(), squash, fastForwardSingleCommit);
if (!mergeResult.getMergeStatus().isSuccessful()) {
return;
}
Ref branch = repository.findBranch(branchName);
if (branch == null) {
throw new IllegalStateException(String.format(
CoreText.GitFlowOperation_branchMissing, branchName));
}
boolean forceDelete = squash;
if (!keepBranch) {
new DeleteBranchOperation(repository.getRepository(), branch,
forceDelete).execute(progress.newChild(1));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Finish without squash, NO_FF and keep for single commit branches:
* {@link org.eclipse.egit.gitflow.op.GitFlowOperation#finish(IProgressMonitor, String, boolean, boolean, boolean)}
*
* @param monitor
* @param branchName
* @throws CoreException
*/
protected void finish(IProgressMonitor monitor, String branchName)
throws CoreException {
finish(monitor, branchName, false, false, false);
}
/**
* @param monitor
* @param branchName
* @param targetBranchName
* @param squash
* @param fastForwardSingleCommit Has no effect if {@code squash} is true.
* @return result of merging back to targetBranchName
* @throws CoreException
* @since 4.1
*/
protected @NonNull MergeResult mergeTo(IProgressMonitor monitor, String branchName,
String targetBranchName, boolean squash, boolean fastForwardSingleCommit) throws CoreException {
try {
if (!repository.hasBranch(targetBranchName)) {
throw new RuntimeException(String.format(
CoreText.GitFlowOperation_branchNotFound,
targetBranchName));
}
SubMonitor progress = SubMonitor.convert(monitor, 2);
boolean dontCloseProjects = false;
BranchOperation branchOperation = new BranchOperation(
repository.getRepository(), targetBranchName,
dontCloseProjects);
branchOperation.execute(progress.newChild(1));
Status status = branchOperation.getResult().getStatus();
if (!CheckoutResult.Status.OK.equals(status)) {
throw new CoreException(error(format(
CoreText.GitFlowOperation_unableToCheckout, branchName,
status.toString())));
}
MergeOperation mergeOperation = new MergeOperation(
repository.getRepository(), branchName);
mergeOperation.setSquash(squash);
if (squash) {
mergeOperation.setCommit(true);
}
if (!squash && (!fastForwardSingleCommit || hasMultipleCommits(branchName))) {
mergeOperation.setFastForwardMode(NO_FF);
}
mergeOperation.execute(progress.newChild(1));
MergeResult result = mergeOperation.getResult();
if (result == null) {
throw new CoreException(error(format(
CoreText.GitFlowOperation_unableToMerge, branchName,
targetBranchName)));
}
return result;
} catch (GitAPIException | IOException e) {
throw new RuntimeException(e);
}
}
private boolean hasMultipleCommits(String branchName) throws IOException {
return getAheadOfDevelopCount(branchName) > 1;
}
private int getAheadOfDevelopCount(String branchName) throws IOException {
String parentBranch = repository.getConfig().getDevelop();
Ref develop = repository.findBranch(parentBranch);
Ref branch = repository.findBranch(branchName);
RevWalk walk = new RevWalk(repository.getRepository());
RevCommit branchCommit = walk.parseCommit(branch.getObjectId());
RevCommit developCommit = walk.parseCommit(develop.getObjectId());
RevCommit mergeBase = findCommonBase(walk, branchCommit, developCommit);
walk.reset();
walk.setRevFilter(RevFilter.ALL);
int aheadCount = RevWalkUtils.count(walk, branchCommit, mergeBase);
return aheadCount;
}
private RevCommit findCommonBase(RevWalk walk, RevCommit branchCommit,
RevCommit developCommit) throws IOException {
walk.setRevFilter(RevFilter.MERGE_BASE);
walk.markStart(branchCommit);
walk.markStart(developCommit);
return walk.next();
}
/**
* Merge without squash and NO_FF for single commit branches:
* {@link org.eclipse.egit.gitflow.op.GitFlowOperation#mergeTo(IProgressMonitor, String, String, boolean, boolean)}
*
* @param monitor
* @param branchName
* @param targetBranchName
* @return result of merging back to targetBranchName
* @throws CoreException
*/
protected MergeResult mergeTo(IProgressMonitor monitor, String branchName,
String targetBranchName) throws CoreException {
return mergeTo(monitor, branchName, targetBranchName, false, false);
}
/**
* Fetch using the default remote configuration
*
* @param monitor
* @param timeout
* timeout in seconds
* @return result of fetching from remote
* @throws URISyntaxException
* @throws InvocationTargetException
*
* @since 4.2
*/
protected FetchResult fetch(IProgressMonitor monitor, int timeout)
throws URISyntaxException, InvocationTargetException {
RemoteConfig config = repository.getConfig().getDefaultRemoteConfig();
FetchOperation fetchOperation = new FetchOperation(
repository.getRepository(), config, timeout, false);
fetchOperation.run(monitor);
return fetchOperation.getOperationResult();
}
/**
* @param monitor
* @return resulting of fetching from remote
* @throws URISyntaxException
* @throws InvocationTargetException
* @deprecated Use {@link GitFlowOperation#fetch(IProgressMonitor, int)}
* instead.
*/
@Deprecated
protected FetchResult fetch(IProgressMonitor monitor)
throws URISyntaxException, InvocationTargetException {
return fetch(monitor, 0);
}
/**
* @return The result of the merge this operation performs. May be null, if
* no merge was performed.
*/
public MergeResult getMergeResult() {
return mergeResult;
}
}