/*******************************************************************************
* Copyright (c) 2010, 2016 SAP AG and others.
* 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
*
* Contributors:
* Mathias Kinzler (SAP AG) - initial implementation
* Thomas Wolf <thomas.wolf@paranor.ch> - Refactor
*******************************************************************************/
package org.eclipse.egit.ui.internal.branch;
import java.io.File;
import java.util.List;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.op.BranchOperation;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator;
import org.eclipse.egit.ui.internal.dialogs.NonDeletedFilesDialog;
import org.eclipse.egit.ui.internal.repository.CreateBranchWizard;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.CheckoutResult;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.PlatformUI;
/**
* The UI wrapper for {@link BranchOperation}
*/
public class BranchOperationUI {
private final Repository repository;
private String target;
/**
* In the case of checkout conflicts, a dialog is shown to let the user
* stash, reset or commit. After that, checkout is tried again. The second
* time we do checkout, we don't want to ask any questions we already asked
* the first time, so this will be false then.
*/
private final boolean showQuestionsBeforeCheckout;
/**
* Create an operation for checking out a branch
*
* @param repository
* @param target
* a valid {@link Ref} name or commit id
* @return the {@link BranchOperationUI}
*/
public static BranchOperationUI checkout(Repository repository,
String target) {
return new BranchOperationUI(repository, target, true);
}
/**
* @param refName
* the full ref name which will be checked out
* @return true if checkout will need additional input from the user before
* continuing
*/
public static boolean checkoutWillShowQuestionDialog(String refName) {
return shouldShowCheckoutRemoteTrackingDialog(refName);
}
/**
* @param repository
* @param target
* @param showQuestionsBeforeCheckout
*/
private BranchOperationUI(Repository repository, String target,
boolean showQuestionsBeforeCheckout) {
this.repository = repository;
this.target = target;
this.showQuestionsBeforeCheckout = showQuestionsBeforeCheckout;
}
private String confirmTarget(IProgressMonitor monitor) {
if (target != null) {
if (!repository.getRepositoryState().canCheckout()) {
PlatformUI.getWorkbench().getDisplay()
.asyncExec(new Runnable() {
@Override
public void run() {
MessageDialog.openError(getShell(),
UIText.BranchAction_cannotCheckout,
NLS.bind(
UIText.BranchAction_repositoryState,
repository.getRepositoryState()
.getDescription()));
}
});
return null;
}
if (LaunchFinder.shouldCancelBecauseOfRunningLaunches(repository,
monitor)) {
return null;
}
askForTargetIfNecessary();
}
return target;
}
private void doCheckout(BranchOperation bop, boolean restore,
IProgressMonitor monitor)
throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, restore ? 10 : 1);
if (!restore) {
bop.execute(progress.newChild(1));
} else {
final BranchProjectTracker tracker = new BranchProjectTracker(
repository);
IMemento snapshot = tracker.snapshot();
bop.execute(progress.newChild(7));
tracker.save(snapshot);
IWorkspaceRunnable action = new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor innerMonitor)
throws CoreException {
tracker.restore(innerMonitor);
}
};
ResourcesPlugin.getWorkspace().run(action,
ResourcesPlugin.getWorkspace().getRoot(),
IWorkspace.AVOID_UPDATE, progress.newChild(3));
}
}
/**
* Starts the operation asynchronously
*/
public void start() {
target = confirmTarget(new NullProgressMonitor());
if (target == null) {
return;
}
String repoName = Activator.getDefault().getRepositoryUtil()
.getRepositoryName(repository);
String jobname = NLS.bind(UIText.BranchAction_checkingOut, repoName,
target);
boolean restore = Activator.getDefault().getPreferenceStore()
.getBoolean(UIPreferences.CHECKOUT_PROJECT_RESTORE);
final CheckoutJob job = new CheckoutJob(jobname, restore);
job.setUser(true);
job.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent cevent) {
show(job.getCheckoutResult());
}
});
job.schedule();
}
private class CheckoutJob extends Job {
private BranchOperation bop;
private final boolean restore;
public CheckoutJob(String jobName, boolean restore) {
super(jobName);
this.restore = restore;
}
@Override
public IStatus run(IProgressMonitor monitor) {
bop = new BranchOperation(repository, target, !restore);
try {
doCheckout(bop, restore, monitor);
} catch (CoreException e) {
switch (bop.getResult().getStatus()) {
case CONFLICTS:
case NONDELETED:
break;
default:
return Activator.createErrorStatus(
UIText.BranchAction_branchFailed, e);
}
} finally {
GitLightweightDecorator.refresh();
monitor.done();
}
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
if (JobFamilies.CHECKOUT.equals(family))
return true;
return super.belongsTo(family);
}
@NonNull
public CheckoutResult getCheckoutResult() {
return bop.getResult();
}
}
/**
* Runs the operation synchronously.
*
* @param monitor
* @throws CoreException
*
*/
public void run(IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 100);
target = confirmTarget(progress.newChild(20));
if (target == null) {
return;
}
final boolean restore = Activator.getDefault().getPreferenceStore()
.getBoolean(UIPreferences.CHECKOUT_PROJECT_RESTORE);
BranchOperation bop = new BranchOperation(repository, target, !restore);
doCheckout(bop, restore, progress.newChild(80));
show(bop.getResult());
}
private void askForTargetIfNecessary() {
if (target != null && showQuestionsBeforeCheckout) {
if (shouldShowCheckoutRemoteTrackingDialog(target))
target = getTargetWithCheckoutRemoteTrackingDialog();
}
}
private static boolean shouldShowCheckoutRemoteTrackingDialog(String refName) {
boolean isRemoteTrackingBranch = refName != null
&& refName.startsWith(Constants.R_REMOTES);
if (isRemoteTrackingBranch) {
boolean showDetachedHeadWarning = Activator.getDefault()
.getPreferenceStore()
.getBoolean(UIPreferences.SHOW_DETACHED_HEAD_WARNING);
// If the user has not yet chosen to ignore the warning about
// getting into a "detached HEAD" state, then we show them a dialog
// whether a remote-tracking branch should be checked out with a
// detached HEAD or checking it out as a new local branch.
return showDetachedHeadWarning;
} else {
return false;
}
}
private String getTargetWithCheckoutRemoteTrackingDialog() {
final String[] dialogResult = new String[1];
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
dialogResult[0] = getTargetWithCheckoutRemoteTrackingDialogInUI();
}
});
return dialogResult[0];
}
private String getTargetWithCheckoutRemoteTrackingDialogInUI() {
String[] buttons = new String[] {
UIText.BranchOperationUI_CheckoutRemoteTrackingAsLocal,
UIText.BranchOperationUI_CheckoutRemoteTrackingCommit,
IDialogConstants.CANCEL_LABEL };
MessageDialog questionDialog = new MessageDialog(
getShell(),
UIText.BranchOperationUI_CheckoutRemoteTrackingTitle,
null,
UIText.BranchOperationUI_CheckoutRemoteTrackingQuestion,
MessageDialog.QUESTION, buttons, 0);
int result = questionDialog.open();
if (result == 0) {
// Check out as new local branch
CreateBranchWizard wizard = new CreateBranchWizard(repository,
target);
WizardDialog createBranchDialog = new WizardDialog(getShell(),
wizard);
createBranchDialog.open();
return null;
} else if (result == 1) {
// Check out commit
return target;
} else {
// Cancel
return null;
}
}
private Shell getShell() {
return PlatformUI.getWorkbench().getDisplay().getActiveShell();
}
/**
* @param result
* the result to show
*/
private void show(final @NonNull CheckoutResult result) {
if (result.getStatus() == CheckoutResult.Status.CONFLICTS) {
PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
Shell shell = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getShell();
CleanupUncomittedChangesDialog cleanupUncomittedChangesDialog = new CleanupUncomittedChangesDialog(
shell,
UIText.BranchResultDialog_CheckoutConflictsTitle,
NLS.bind(
UIText.BranchResultDialog_CheckoutConflictsMessage,
Repository.shortenRefName(target)),
repository, result.getConflictList());
cleanupUncomittedChangesDialog.open();
if (cleanupUncomittedChangesDialog.shouldContinue()) {
BranchOperationUI op = new BranchOperationUI(repository,
target, false);
op.start();
}
}
});
} else if (result.getStatus() == CheckoutResult.Status.NONDELETED) {
// double-check if the files are still there
boolean show = false;
List<String> pathList = result.getUndeletedList();
for (String path : pathList)
if (new File(repository.getWorkTree(), path).exists()) {
show = true;
break;
}
if (!show)
return;
PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
Shell shell = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getShell();
new NonDeletedFilesDialog(shell, repository, result
.getUndeletedList()).open();
}
});
} else if (result.getStatus() == CheckoutResult.Status.OK) {
if (RepositoryUtil.isDetachedHead(repository))
showDetachedHeadWarning();
}
}
private void showDetachedHeadWarning() {
PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
IPreferenceStore store = Activator.getDefault()
.getPreferenceStore();
if (store.getBoolean(UIPreferences.SHOW_DETACHED_HEAD_WARNING)) {
String toggleMessage = UIText.BranchResultDialog_DetachedHeadWarningDontShowAgain;
MessageDialogWithToggle.openInformation(PlatformUI
.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
UIText.BranchOperationUI_DetachedHeadTitle,
UIText.BranchOperationUI_DetachedHeadMessage,
toggleMessage, false, store,
UIPreferences.SHOW_DETACHED_HEAD_WARNING);
}
}
});
}
}