blob: 93c50efa87070335793f59961f4348c81a7a6771 [file] [log] [blame]
/*******************************************************************************
* 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 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mathias Kinzler (SAP AG) - initial implementation
* Dariusz Luksza <dariusz@luksza.org>
* Steffen Pingel (Tasktop Technologies) - fixes for bug 352253
* Thomas Wolf <thomas.wolf@paranor.ch> - Bug 499482
* Wim Jongman <wim.jongman@remainsoftware.com> - Bug 509878
*******************************************************************************/
package org.eclipse.egit.ui.internal.repository;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.internal.SafeRunnable;
import org.eclipse.egit.core.internal.Utils;
import org.eclipse.egit.core.op.CreateLocalBranchOperation;
import org.eclipse.egit.ui.IBranchNameProvider;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.ValidationUtils;
import org.eclipse.egit.ui.internal.branch.BranchOperationUI;
import org.eclipse.egit.ui.internal.components.BranchNameNormalizer;
import org.eclipse.egit.ui.internal.components.UpstreamConfigComponent;
import org.eclipse.egit.ui.internal.dialogs.AbstractBranchSelectionDialog;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* Allows to create a new local branch based on another branch or commit.
* <p>
* The source can be selected using a branch selection dialog.
* <p>
* The user can select a strategy for configuring "Pull". The default as read
* from the repository's autosetupmerge and autosetuprebase configuration is
* suggested initially.
*/
class CreateBranchPage extends WizardPage {
private static final String BRANCH_NAME_PROVIDER_ID = "org.eclipse.egit.ui.branchNameProvider"; //$NON-NLS-1$
private Job validateJob;
/**
* Get proposed target branch name for given source branch name
*
* @param sourceName
* @return target name
*/
public String getProposedTargetName(String sourceName) {
if (sourceName == null)
return null;
if (sourceName.startsWith(Constants.R_REMOTES))
return myRepository.shortenRemoteBranchName(sourceName);
if (sourceName.startsWith(Constants.R_TAGS))
return sourceName.substring(Constants.R_TAGS.length()) + "-branch"; //$NON-NLS-1$
return ""; //$NON-NLS-1$
}
private final Repository myRepository;
private final IInputValidator myValidator;
private final String myBaseRef;
private final RevCommit myBaseCommit;
private Text nameText;
/**
* Whether the contents of {@code nameText} is a suggestion or was entered by the user.
*/
private boolean nameIsSuggestion;
private Button checkout;
private BranchRebaseMode upstreamConfig;
private UpstreamConfigComponent upstreamConfigComponent;
private Label sourceIcon;
private StyledText sourceNameLabel;
private String sourceRefName = ""; //$NON-NLS-1$
private final LocalResourceManager resourceManager = new LocalResourceManager(
JFaceResources.getResources());
/**
* Constructs this page.
* <p>
* If a base branch is provided, the drop down will be selected accordingly
*
* @param repo
* the repository
* @param baseRef
* the branch or tag to base the new branch on, may be null
*/
public CreateBranchPage(Repository repo, Ref baseRef) {
super(CreateBranchPage.class.getName());
this.myRepository = repo;
if (baseRef != null) {
this.myBaseRef = baseRef.getName();
} else {
this.myBaseRef = null;
}
this.myBaseCommit = null;
this.myValidator = ValidationUtils.getRefNameInputValidator(
myRepository, Constants.R_HEADS, false);
if (baseRef != null) {
this.upstreamConfig = CreateLocalBranchOperation
.getDefaultUpstreamConfig(repo, baseRef.getName());
} else {
this.upstreamConfig = null;
}
setTitle(MessageFormat.format(UIText.CreateBranchPage_Title,
RepositoryUtil.INSTANCE.getRepositoryName(repo)));
setMessage(UIText.CreateBranchPage_ChooseBranchAndNameMessage);
}
/**
* Constructs this page.
* <p>
* If a base branch is provided, the drop down will be selected accordingly
*
* @param repo
* the repository
* @param baseCommit
* the commit to base the new branch on, may be null
*/
public CreateBranchPage(Repository repo, RevCommit baseCommit) {
super(CreateBranchPage.class.getName());
this.myRepository = repo;
this.myBaseRef = null;
this.myBaseCommit = baseCommit;
this.myValidator = ValidationUtils.getRefNameInputValidator(
myRepository, Constants.R_HEADS, false);
this.upstreamConfig = null;
setTitle(MessageFormat.format(UIText.CreateBranchPage_Title,
RepositoryUtil.INSTANCE.getRepositoryName(repo)));
setMessage(UIText.CreateBranchPage_ChooseNameMessage);
}
@Override
public void createControl(Composite parent) {
Composite main = new Composite(parent, SWT.NONE);
main.setLayout(new GridLayout(4, false));
Label sourceLabel = new Label(main, SWT.NONE);
sourceLabel.setText(UIText.CreateBranchPage_SourceLabel);
sourceLabel.setToolTipText(UIText.CreateBranchPage_SourceTooltip);
sourceIcon = new Label(main, SWT.NONE);
sourceIcon.setImage(UIIcons.getImage(resourceManager, UIIcons.BRANCH));
sourceIcon.setLayoutData(GridDataFactory.fillDefaults()
.align(SWT.END, SWT.CENTER).create());
sourceNameLabel = new StyledText(main, SWT.NONE);
sourceNameLabel.setBackground(main.getBackground());
sourceNameLabel.setEditable(false);
sourceNameLabel.setLayoutData(GridDataFactory.fillDefaults()
.align(SWT.FILL, SWT.CENTER)
.grab(true, false).create());
Button selectButton = new Button(main, SWT.NONE);
selectButton.setText(UIText.CreateBranchPage_SourceSelectButton);
selectButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
selectSource();
}
});
UIUtils.setButtonLayoutData(selectButton);
Label nameLabel = new Label(main, SWT.NONE);
nameLabel.setText(UIText.CreateBranchPage_BranchNameLabel);
nameLabel.setLayoutData(GridDataFactory.fillDefaults().span(1, 1)
.align(SWT.BEGINNING, SWT.CENTER).create());
nameLabel.setToolTipText(UIText.CreateBranchPage_BranchNameToolTip);
nameText = new Text(main, SWT.BORDER);
// give focus to the nameText if label is activated using the mnemonic
nameLabel.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e) {
nameText.setFocus();
}
});
nameText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
nameIsSuggestion = false;
}
});
// enable testing with SWTBot
nameText.setData("org.eclipse.swtbot.widget.key", "BranchName"); //$NON-NLS-1$ //$NON-NLS-2$
GridDataFactory.fillDefaults().grab(true, false).span(3, 1)
.applyTo(nameText);
upstreamConfigComponent = new UpstreamConfigComponent(main, SWT.NONE);
GridDataFactory.fillDefaults().grab(true, false).span(4, 1)
.applyTo(upstreamConfigComponent.getContainer());
upstreamConfigComponent
.addUpstreamConfigSelectionListener((newConfig) -> {
upstreamConfig = newConfig;
checkPage();
});
boolean isBare = myRepository.isBare();
checkout = new Button(main, SWT.CHECK);
checkout.setText(UIText.CreateBranchPage_CheckoutButton);
// most of the time, we probably will check this out
// unless we have a bare repository which doesn't allow
// check out at all
checkout.setSelection(!isBare);
checkout.setEnabled(!isBare);
checkout.setVisible(!isBare);
GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(
checkout);
checkout.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkPage();
}
});
createValidateJob();
Dialog.applyDialogFont(main);
setControl(main);
if (this.myBaseCommit != null)
setSourceCommit(this.myBaseCommit);
else if (myBaseRef != null)
setSourceRef(myBaseRef);
nameText.setFocus();
// add the listeners just now to avoid unneeded checkPage()
nameText.addModifyListener(e -> {
validateJob.cancel();
// Schedule without delay to ensure the job finishes before the user
// can use the Finish button of the wizard. Otherwise we would have
// to disable the Finish button, leading to flickering from job
// start to job finish.
validateJob.schedule();
});
BranchNameNormalizer normalizer = new BranchNameNormalizer(nameText);
normalizer.setVisible(false);
}
private void createValidateJob() {
validateJob = new WorkbenchJob("Validate branch name") {//$NON-NLS-1$
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
if (sourceNameLabel.isDisposed()) {
return Status.CANCEL_STATUS;
}
checkPage();
return Status.OK_STATUS;
}
};
validateJob.setSystem(true);
}
@Override
public void dispose() {
resourceManager.dispose();
if (validateJob != null) {
validateJob.cancel();
}
}
private void setSourceRef(String refName) {
String shortName = Repository.shortenRefName(refName);
sourceNameLabel.setText(shortName);
if (refName.startsWith(Constants.R_HEADS)
|| refName.startsWith(Constants.R_REMOTES))
sourceIcon.setImage(UIIcons.getImage(resourceManager,
UIIcons.BRANCH));
else if (refName.startsWith(Constants.R_TAGS))
sourceIcon.setImage(UIIcons.getImage(resourceManager, UIIcons.TAG));
else
sourceIcon.setImage(UIIcons.getImage(resourceManager,
UIIcons.CHANGESET));
sourceRefName = refName;
suggestBranchName(refName);
upstreamConfig = CreateLocalBranchOperation
.getDefaultUpstreamConfig(myRepository, refName);
updateUpstreamComponent();
checkPage();
}
private void setSourceCommit(RevCommit commit) {
sourceNameLabel.setText(Utils.getShortObjectId(commit));
sourceIcon.setImage(UIIcons
.getImage(resourceManager, UIIcons.CHANGESET));
sourceRefName = commit.name();
upstreamConfig = null;
updateUpstreamComponent();
checkPage();
}
private void selectSource() {
SourceSelectionDialog dialog = new SourceSelectionDialog(getShell(),
myRepository, sourceRefName);
int result = dialog.open();
if (result == Window.OK) {
String refName = dialog.getRefName();
setSourceRef(refName);
nameText.setFocus();
}
}
private void updateUpstreamComponent() {
upstreamConfigComponent.setUpstreamConfig(upstreamConfig);
boolean showUpstreamConfig = sourceRefName.startsWith(Constants.R_HEADS)
|| sourceRefName.startsWith(Constants.R_REMOTES);
Composite container = upstreamConfigComponent.getContainer();
GridData gd = (GridData) container.getLayoutData();
if (gd.exclude == showUpstreamConfig) {
gd.exclude = !showUpstreamConfig;
container.setVisible(showUpstreamConfig);
container.getParent().layout(true);
ensurePreferredHeight(getShell());
}
}
private void checkPage() {
try {
boolean basedOnLocalBranch = sourceRefName
.startsWith(Constants.R_HEADS);
if (basedOnLocalBranch && upstreamConfig != null) {
setMessage(UIText.CreateBranchPage_LocalBranchWarningMessage,
IMessageProvider.INFORMATION);
} else {
setMessage(UIText.CreateBranchPage_ChooseBranchAndNameMessage);
}
if (sourceRefName.length() == 0) {
setErrorMessage(UIText.CreateBranchPage_MissingSourceMessage);
return;
}
String message = this.myValidator.isValid(nameText.getText());
if (message != null) {
setErrorMessage(message);
return;
}
setErrorMessage(null);
} finally {
setPageComplete(getErrorMessage() == null
&& nameText.getText().length() > 0);
}
}
public String getBranchName() {
return nameText.getText();
}
public boolean checkoutNewBranch() {
return checkout.getSelection();
}
/**
* @param newRefName
* @param checkoutNewBranch
* @param monitor
* @throws CoreException
* @throws IOException
*/
public void createBranch(String newRefName, boolean checkoutNewBranch,
IProgressMonitor monitor)
throws CoreException, IOException {
SubMonitor progress = SubMonitor.convert(monitor,
checkoutNewBranch ? 2 : 1);
progress.setTaskName(UIText.CreateBranchPage_CreatingBranchMessage);
final CreateLocalBranchOperation cbop;
if (myBaseCommit != null
&& this.sourceRefName.equals(myBaseCommit.name()))
cbop = new CreateLocalBranchOperation(myRepository, newRefName,
myBaseCommit);
else
cbop = new CreateLocalBranchOperation(myRepository, newRefName,
myRepository.findRef(this.sourceRefName),
upstreamConfig);
cbop.execute(progress.newChild(1));
if (checkoutNewBranch && !progress.isCanceled()) {
progress.setTaskName(UIText.CreateBranchPage_CheckingOutMessage);
BranchOperationUI.checkout(myRepository, Constants.R_HEADS + newRefName)
.run(progress.newChild(1));
}
}
private void suggestBranchName(String ref) {
if (nameText.getText().length() == 0 || nameIsSuggestion) {
String branchNameSuggestion = getBranchNameSuggestionFromProvider();
if (branchNameSuggestion == null)
branchNameSuggestion = getProposedTargetName(ref);
if (branchNameSuggestion != null) {
nameText.setText(branchNameSuggestion);
nameText.selectAll();
nameIsSuggestion = true;
}
}
}
private IBranchNameProvider getBranchNameProvider() {
IExtensionRegistry registry = Platform.getExtensionRegistry();
IConfigurationElement[] config = registry
.getConfigurationElementsFor(BRANCH_NAME_PROVIDER_ID);
if (config.length > 0) {
Object provider;
try {
provider = config[0].createExecutableExtension("class"); //$NON-NLS-1$
if (provider instanceof IBranchNameProvider)
return (IBranchNameProvider) provider;
} catch (Throwable e) {
Activator.logError(
UIText.CreateBranchPage_CreateBranchNameProviderFailed,
e);
}
}
return null;
}
private String getBranchNameSuggestionFromProvider() {
final AtomicReference<String> ref = new AtomicReference<>();
final IBranchNameProvider branchNameProvider = getBranchNameProvider();
if (branchNameProvider != null) {
SafeRunnable.run(() -> ref
.set(branchNameProvider.getBranchNameSuggestion()));
}
return ref.get();
}
private static void ensurePreferredHeight(Shell shell) {
int preferredHeight = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
Point size = shell.getSize();
if (size.y < preferredHeight)
shell.setSize(size.x, preferredHeight);
}
private static class SourceSelectionDialog extends
AbstractBranchSelectionDialog {
public SourceSelectionDialog(Shell parentShell, Repository repository,
String refToMark) {
super(parentShell, repository, refToMark, SHOW_LOCAL_BRANCHES
| SHOW_REMOTE_BRANCHES | SHOW_TAGS | SHOW_REFERENCES
| SELECT_CURRENT_REF | EXPAND_LOCAL_BRANCHES_NODE
| EXPAND_REMOTE_BRANCHES_NODE);
}
@Override
protected void refNameSelected(String refName) {
setOkButtonEnabled(refName != null);
}
@Override
protected String getTitle() {
return UIText.CreateBranchPage_SourceSelectionDialogTitle;
}
@Override
protected String getMessageText() {
return UIText.CreateBranchPage_SourceSelectionDialogMessage;
}
}
}