/*******************************************************************************
* Copyright (c) 2013, 2016 Robin Stocker <robin@nibor.org> 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
*******************************************************************************/
package org.eclipse.egit.ui.internal.push;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.egit.core.internal.Utils;
import org.eclipse.egit.core.op.CreateLocalBranchOperation;
import org.eclipse.egit.ui.Activator;
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.components.BranchNameNormalizer;
import org.eclipse.egit.ui.internal.components.RefContentAssistProvider;
import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo;
import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.IRemoteSelectionListener;
import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.SelectionType;
import org.eclipse.egit.ui.internal.components.UpstreamConfigComponent;
import org.eclipse.egit.ui.internal.components.UpstreamConfigComponent.UpstreamConfigSelectionListener;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.RowLayoutFactory;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.lib.BranchConfig;
import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.swt.SWT;
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.graphics.Image;
import org.eclipse.swt.graphics.Resource;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
/**
* Page that is part of the "Push Branch..." wizard, where the user selects the
* remote, the branch name and the upstream config.
*/
public class PushBranchPage extends WizardPage {
private static final int MAX_SHORTCOMMIT_MESSAGE_LENGTH = 65;
private final Repository repository;
private final ObjectId commitToPush;
private final Ref ref;
private boolean showNewRemoteButton = true;
private RemoteConfig remoteConfig;
private List<RemoteConfig> remoteConfigs;
private RemoteSelectionCombo remoteSelectionCombo;
private Text remoteBranchNameText;
private RefContentAssistProvider assist;
private BranchRebaseMode upstreamConfig;
private UpstreamConfigComponent upstreamConfigComponent;
private boolean forceUpdateSelected = false;
/** Only set if user selected "New Remote" */
private AddRemotePage addRemotePage;
private Set<Resource> disposables = new HashSet<>();
/**
* Create the page.
*
* @param repository
* @param commitToPush
* @param ref
* An optional ref to give hints
*/
public PushBranchPage(Repository repository, ObjectId commitToPush, Ref ref) {
super(UIText.PushBranchPage_PageName);
setTitle(UIText.PushBranchPage_PageTitle);
setMessage(UIText.PushBranchPage_PageMessage);
this.repository = repository;
this.commitToPush = commitToPush;
this.ref = ref;
}
/**
* @param showNewRemoteButton
*/
public void setShowNewRemoteButton(boolean showNewRemoteButton) {
this.showNewRemoteButton = showNewRemoteButton;
}
/**
* @return the page used to add a new remote, or null if an existing remote
* was chosen
*/
AddRemotePage getAddRemotePage() {
return addRemotePage;
}
RemoteConfig getRemoteConfig() {
return remoteConfig;
}
/**
* @return the chosen short name of the branch on the remote
*/
String getFullRemoteReference() {
if (!remoteBranchNameText.getText().startsWith(Constants.R_REFS))
return Constants.R_HEADS + remoteBranchNameText.getText();
else
return remoteBranchNameText.getText();
}
BranchRebaseMode getUpstreamConfig() {
return upstreamConfig;
}
boolean isForceUpdateSelected() {
return forceUpdateSelected;
}
@Override
public void createControl(Composite parent) {
try {
this.remoteConfigs = RemoteConfig.getAllRemoteConfigs(repository.getConfig());
Collections.sort(remoteConfigs, new Comparator<RemoteConfig>() {
@Override
public int compare(RemoteConfig first, RemoteConfig second) {
return String.CASE_INSENSITIVE_ORDER.compare(
first.getName(), second.getName());
}
});
} catch (URISyntaxException e) {
this.remoteConfigs = new ArrayList<>();
handleError(e);
}
Composite main = new Composite(parent, SWT.NONE);
main.setLayout(GridLayoutFactory.swtDefaults().create());
Composite inputPanel = new Composite(main, SWT.NONE);
GridDataFactory.fillDefaults().grab(true, false).applyTo(inputPanel);
GridLayoutFactory.fillDefaults().numColumns(1).applyTo(inputPanel);
Label sourceLabel = new Label(inputPanel, SWT.NONE);
sourceLabel.setText(UIText.PushBranchPage_Source);
Composite sourceComposite = new Composite(inputPanel, SWT.NONE);
sourceComposite.setLayoutData(GridDataFactory.fillDefaults()
.indent(UIUtils.getControlIndent(), 0).create());
RowLayout rowLayout = RowLayoutFactory.fillDefaults().create();
rowLayout.center = true;
sourceComposite.setLayout(rowLayout);
if (this.ref != null) {
Image branchIcon = UIIcons.BRANCH.createImage();
this.disposables.add(branchIcon);
Label branchIconLabel = new Label(sourceComposite, SWT.NONE);
branchIconLabel
.setLayoutData(new RowData(branchIcon.getBounds().width,
branchIcon.getBounds().height));
branchIconLabel.setImage(branchIcon);
Label localBranchLabel = new Label(sourceComposite, SWT.NONE);
localBranchLabel.setText(Repository.shortenRefName(this.ref
.getName()));
Label spacer = new Label(sourceComposite, SWT.NONE);
spacer.setLayoutData(new RowData(3, SWT.DEFAULT));
}
Image commitIcon = UIIcons.CHANGESET.createImage();
this.disposables.add(commitIcon);
Label commitIconLabel = new Label(sourceComposite, SWT.NONE);
commitIconLabel.setImage(commitIcon);
commitIconLabel.setLayoutData(new RowData(commitIcon.getBounds().width,
commitIcon.getBounds().height));
Label commit = new Label(sourceComposite, SWT.NONE);
StringBuilder commitBuilder = new StringBuilder(this.commitToPush
.abbreviate(7).name());
StringBuilder commitTooltipBuilder = new StringBuilder(
this.commitToPush.getName());
try (RevWalk revWalk = new RevWalk(repository)) {
RevCommit revCommit = revWalk.parseCommit(this.commitToPush);
commitBuilder.append(" "); //$NON-NLS-1$
commitBuilder.append(Utils.shortenText(revCommit.getShortMessage(),
MAX_SHORTCOMMIT_MESSAGE_LENGTH));
commitTooltipBuilder.append("\n\n"); //$NON-NLS-1$
commitTooltipBuilder.append(revCommit.getFullMessage());
} catch (IOException ex) {
commitBuilder
.append(UIText.PushBranchPage_CannotAccessCommitDescription);
commitTooltipBuilder.append(ex.getMessage());
Activator.handleError(ex.getLocalizedMessage(), ex, false);
}
commit.setText(commitBuilder.toString());
commit.setToolTipText(commitTooltipBuilder.toString());
Label destinationLabel = new Label(inputPanel, SWT.NONE);
destinationLabel.setText(UIText.PushBranchPage_Destination);
GridDataFactory.fillDefaults().applyTo(destinationLabel);
Composite remoteGroup = new Composite(inputPanel, SWT.NONE);
remoteGroup.setLayoutData(GridDataFactory.fillDefaults()
.indent(UIUtils.getControlIndent(), 0).create());
remoteGroup.setLayout(GridLayoutFactory.fillDefaults().numColumns(3)
.create());
Label remoteLabel = new Label(remoteGroup, SWT.NONE);
remoteLabel.setText(UIText.PushBranchPage_RemoteLabel);
// Use full width in case "New Remote..." button is not shown
int remoteSelectionSpan = showNewRemoteButton ? 1 : 2;
remoteSelectionCombo = new RemoteSelectionCombo(remoteGroup, SWT.NONE,
SelectionType.PUSH);
GridDataFactory.fillDefaults().grab(true, false).span(remoteSelectionSpan, 1)
.applyTo(remoteSelectionCombo);
setRemoteConfigs();
remoteSelectionCombo
.addRemoteSelectionListener(new IRemoteSelectionListener() {
@Override
public void remoteSelected(RemoteConfig rc) {
remoteConfig = rc;
setRefAssist(rc);
checkPage();
}
});
if (showNewRemoteButton) {
Button newRemoteButton = new Button(remoteGroup, SWT.PUSH);
newRemoteButton.setText(UIText.PushBranchPage_NewRemoteButton);
GridDataFactory.fillDefaults().applyTo(newRemoteButton);
newRemoteButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
showNewRemoteDialog();
}
});
}
Label branchNameLabel = new Label(remoteGroup, SWT.NONE);
branchNameLabel.setText(UIText.PushBranchPage_RemoteBranchNameLabel);
remoteBranchNameText = new Text(remoteGroup, SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
.applyTo(remoteBranchNameText);
remoteBranchNameText.setText(getSuggestedBranchName());
UIUtils.addRefContentProposalToText(remoteBranchNameText,
this.repository, () -> {
if (PushBranchPage.this.assist != null) {
return PushBranchPage.this.assist
.getRefsForContentAssist(false, true);
}
return Collections.emptyList();
});
if (this.ref != null) {
upstreamConfigComponent = new UpstreamConfigComponent(inputPanel,
SWT.NONE);
upstreamConfigComponent.getContainer().setLayoutData(
GridDataFactory.fillDefaults().grab(true, false).span(3, 1)
.indent(SWT.DEFAULT, 20).create());
upstreamConfigComponent
.addUpstreamConfigSelectionListener(new UpstreamConfigSelectionListener() {
@Override
public void upstreamConfigSelected(
BranchRebaseMode newUpstreamConfig) {
upstreamConfig = newUpstreamConfig;
checkPage();
}
});
setDefaultUpstreamConfig();
}
final Button forceUpdateButton = new Button(inputPanel, SWT.CHECK);
forceUpdateButton.setText(UIText.PushBranchPage_ForceUpdateButton);
forceUpdateButton.setSelection(false);
forceUpdateButton.setLayoutData(GridDataFactory.fillDefaults()
.grab(true, false).span(3, 1).create());
forceUpdateButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
forceUpdateSelected = forceUpdateButton.getSelection();
}
});
Link advancedDialogLink = new Link(main, SWT.NONE);
advancedDialogLink.setText(UIText.PushBranchPage_advancedWizardLink);
advancedDialogLink
.setToolTipText(UIText.PushBranchPage_advancedWizardLinkTooltip);
advancedDialogLink.setLayoutData(new GridData(SWT.END, SWT.END, false,
true));
advancedDialogLink.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
Shell parentShell = getShell().getParent().getShell();
PushWizard advancedWizard = null;
try {
advancedWizard = new PushWizard(repository);
getShell().close();
new WizardDialog(parentShell, advancedWizard).open();
} catch (URISyntaxException ex) {
Activator.logError(ex.getMessage(), ex);
}
}
});
setControl(main);
checkPage();
// Add listener now to avoid setText above to already trigger it.
remoteBranchNameText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
// Do not use a tooltip since there is already a content proposal
// adapter on this field
BranchNameNormalizer normalizer = new BranchNameNormalizer(
remoteBranchNameText, null);
normalizer.setVisible(false);
}
private void setRemoteConfigs() {
remoteSelectionCombo.setItems(remoteConfigs);
if (this.ref != null) {
String branchName = Repository.shortenRefName(this.ref.getName());
BranchConfig branchConfig = new BranchConfig(
repository.getConfig(), branchName);
String remoteName = branchConfig.getRemote();
if (remoteName != null) {
for (RemoteConfig rc : remoteConfigs) {
if (remoteName.equals(rc.getName()))
remoteSelectionCombo.setSelectedRemote(rc);
}
}
}
remoteConfig = remoteSelectionCombo.getSelectedRemote();
setRefAssist(remoteConfig);
}
private void setDefaultUpstreamConfig() {
if (this.ref != null) {
String branchName = Repository.shortenRefName(ref.getName());
BranchConfig branchConfig = new BranchConfig(
repository.getConfig(), branchName);
boolean alreadyConfigured = branchConfig.getMerge() != null;
BranchRebaseMode config;
if (alreadyConfigured) {
config = PullCommand.getRebaseMode(branchName,
repository.getConfig());
} else {
config = CreateLocalBranchOperation.getDefaultUpstreamConfig(
repository, Constants.R_REMOTES
+ Constants.DEFAULT_REMOTE_NAME + "/" + branchName); //$NON-NLS-1$
}
this.upstreamConfig = config;
this.upstreamConfigComponent.setUpstreamConfig(this.upstreamConfig);
}
}
private void showNewRemoteDialog() {
AddRemoteWizard wizard = new AddRemoteWizard(repository);
WizardDialog dialog = new WizardDialog(getShell(), wizard);
int result = dialog.open();
if (result == Window.OK) {
URIish uri = wizard.getUri();
String remoteName = wizard.getRemoteName();
addRemotePage = wizard.getAddRemotePage();
setSelectedRemote(remoteName, uri);
}
}
private void checkPage() {
try {
if (remoteConfig == null) {
setErrorMessage(UIText.PushBranchPage_ChooseRemoteError);
return;
}
String branchName = remoteBranchNameText.getText();
if (branchName.length() == 0) {
setErrorMessage(MessageFormat.format(
UIText.PushBranchPage_ChooseBranchNameError,
remoteConfig.getName()));
return;
}
if (!Repository.isValidRefName(Constants.R_HEADS + branchName)) {
setErrorMessage(UIText.PushBranchPage_InvalidBranchNameError);
return;
}
if (getUpstreamConfig() != null
&& hasDifferentUpstreamConfiguration()) {
setMessage(
UIText.PushBranchPage_UpstreamConfigOverwriteWarning,
IMessageProvider.WARNING);
} else {
setMessage(UIText.PushBranchPage_PageMessage);
}
setErrorMessage(null);
} finally {
setPageComplete(getErrorMessage() == null);
}
}
void setSelectedRemote(String remoteName, URIish urIish) {
try {
RemoteConfig config = new RemoteConfig(repository.getConfig(),
remoteName);
config.addURI(urIish);
remoteSelectionCombo.setItems(Arrays.asList(config));
this.remoteConfig = config;
remoteSelectionCombo.setEnabled(false);
setRefAssist(this.remoteConfig);
checkPage();
} catch (URISyntaxException e) {
handleError(e);
}
}
private String getSuggestedBranchName() {
if (ref != null && !ref.getName().startsWith(Constants.R_REMOTES)) {
StoredConfig config = repository.getConfig();
String branchName = Repository.shortenRefName(ref.getName());
BranchConfig branchConfig = new BranchConfig(config, branchName);
String merge = branchConfig.getMerge();
if (!branchConfig.isRemoteLocal() && merge != null
&& merge.startsWith(Constants.R_HEADS))
return Repository.shortenRefName(merge);
return branchName;
} else {
return ""; //$NON-NLS-1$
}
}
private void setRefAssist(RemoteConfig config) {
if (config != null && config.getURIs().size() > 0) {
this.assist = new RefContentAssistProvider(
PushBranchPage.this.repository, config.getURIs().get(0),
getShell());
}
}
private boolean hasDifferentUpstreamConfiguration() {
String branchName = Repository.shortenRefName(ref.getName());
BranchConfig branchConfig = new BranchConfig(repository.getConfig(),
branchName);
String remote = branchConfig.getRemote();
// No upstream config -> don't show warning
if (remote == null) {
return false;
}
if (!remote.equals(remoteConfig.getName())) {
return true;
}
String merge = branchConfig.getMerge();
if (merge == null || !merge.equals(getFullRemoteReference())) {
return true;
}
if (branchConfig.getRebaseMode() != upstreamConfig) {
return true;
}
return false;
}
private void handleError(URISyntaxException e) {
Activator.handleError(e.getMessage(), e, false);
setErrorMessage(e.getMessage());
}
@Override
public void dispose() {
super.dispose();
for (Resource disposable : this.disposables)
disposable.dispose();
}
}