/*******************************************************************************
* Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch>
*
* 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.dialogs;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.ActionUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.components.TitleAndImageDialog;
import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings;
import org.eclipse.egit.ui.internal.push.RefSpecDialog;
import org.eclipse.egit.ui.internal.push.RefSpecWizard;
import org.eclipse.egit.ui.internal.repository.SelectUriWizard;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
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.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
/**
* Common super class for fetch and push remote configuration dialogs. The
* dialog has, besides the usual OK and CANCEL buttons, three more buttons:
* {@link #DRY_RUN} to do a dry-run of the operation, {@link #SAVE_ONLY} to save
* the {@link RemoteConfig} without performing the operation, and
* {@link #REVERT} to undo all configuration changes and re-load the
* {@link RemoteConfig} from the {@link Repository}. The OK button saves the
* {@link RemoteConfig} <em>and</em> performs the operation.
* <p>
* Provides UI for the common URI and changing it, and also for adding,
* changing, and deleting {@link RefSpec}s. Can be extended via
* {@link #createAdditionalUriArea(Composite)} to provide more UI components.
* </p>
*/
public abstract class AbstractConfigureRemoteDialog
extends TitleAndImageDialog {
/** Button ID for the dry-run button. */
protected static final int DRY_RUN = 98;
/** Button ID for the "save only" button. */
protected static final int SAVE_ONLY = 97;
/** Button ID for the revert button. */
protected static final int REVERT = 96;
private final boolean isPush;
private final Repository repository;
private RemoteConfig config;
private final boolean showBranchInfo;
// UI components
/** A {@link Text} field for the URI. */
protected Text commonUriText;
/** A {@link TableViewer} showing {@link RefSpec}s. */
protected TableViewer specViewer;
/** An {@link IAction} that allows changing the common URI. */
protected IAction changeCommonUriAction;
/** An {@link IAction} that allows deleting the common URI. */
protected IAction deleteCommonUriAction;
/**
* An {@link IAction} to add a new {@link RefSpec} to the
* {@link #specViewer}.
*/
protected IAction addRefSpecAction;
/**
* An {@link IAction} to change an existing {@link RefSpec} in the
* {@link #specViewer}.
*/
protected IAction changeRefSpecAction;
/**
* An {@link IAction} opening an advanced {@link RefSpec} dialog.
*/
protected IAction addRefSpecAdvancedAction;
/**
* Create a new {@link AbstractConfigureRemoteDialog}.
*
* @param parent
* SWT {@link Shell} to parent the dialog on
* @param repository
* the remote belongs to
* @param config
* of the remote to be configured
* @param showBranchInfo
* whether to show additional branch information
* @param isPush
* whether this dialog is for configuring push
*/
protected AbstractConfigureRemoteDialog(Shell parent, Repository repository,
RemoteConfig config, boolean showBranchInfo, boolean isPush) {
super(parent, isPush ? UIIcons.WIZBAN_PUSH : UIIcons.WIZBAN_FETCH);
setHelpAvailable(false);
setShellStyle(getShellStyle() | SWT.SHELL_TRIM);
this.repository = repository;
this.config = config;
this.showBranchInfo = showBranchInfo;
this.isPush = isPush;
}
/**
* Retrieves the {@link Repository} for which the remote is to be
* configured.
*
* @return the {@link Repository}
*/
protected Repository getRepository() {
return repository;
}
/**
* Retrieves the {@link RemoteConfig} as configured currently.
*
* @return the {@link RemoteConfig}
*/
protected RemoteConfig getConfig() {
return config;
}
/**
* Performs a dry-run of the operation.
*
* @param monitor
* for progress reporting and cancellation, never {@code null}
*/
protected abstract void dryRun(IProgressMonitor monitor);
/**
* Performs the operation for real. Invoked in the UI thread; lengthy
* operations should be performed in a background job.
*/
protected abstract void performOperation();
/**
* Creates the OK button.
*
* @param parent
* {@link Composite} containing the buttons
*/
protected abstract void createOkButton(Composite parent);
/**
* Asks the user for a new {@link RefSpec} to be added to the current
* {@link RemoteConfig}.
*
* @return the new {@link RefSpec}, or {@code null} if none.
*/
protected abstract RefSpec getNewRefSpec();
@Override
protected Control createDialogArea(Composite parent) {
final Composite main = new Composite(parent, SWT.NONE);
GridLayoutFactory.fillDefaults().applyTo(main);
GridDataFactory.fillDefaults().grab(true, true)
.minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(main);
if (showBranchInfo) {
Composite branchArea = new Composite(main, SWT.NONE);
GridLayoutFactory.swtDefaults().numColumns(2).equalWidth(false)
.applyTo(branchArea);
GridDataFactory.fillDefaults().grab(true, false)
.applyTo(branchArea);
Label branchLabel = new Label(branchArea, SWT.NONE);
branchLabel.setText(UIText.AbstractConfigureRemoteDialog_BranchLabel);
String branch;
try {
branch = getRepository().getBranch();
} catch (IOException e) {
branch = null;
}
if (branch == null || ObjectId.isId(branch)) {
branch = UIText.AbstractConfigureRemoteDialog_DetachedHeadMessage;
}
Text branchText = new Text(branchArea, SWT.BORDER | SWT.READ_ONLY);
GridDataFactory.fillDefaults().grab(true, false)
.applyTo(branchText);
branchText.setText(branch);
addDefaultOriginWarning(main);
}
final Composite sameUriDetails = new Composite(main, SWT.NONE);
GridLayoutFactory.fillDefaults().numColumns(4).equalWidth(false)
.applyTo(sameUriDetails);
GridDataFactory.fillDefaults().grab(true, false)
.applyTo(sameUriDetails);
Label commonUriLabel = new Label(sameUriDetails, SWT.NONE);
commonUriLabel.setText(UIText.AbstractConfigureRemoteDialog_UriLabel);
commonUriText = new Text(sameUriDetails, SWT.BORDER | SWT.READ_ONLY);
GridDataFactory.fillDefaults().grab(true, false).applyTo(commonUriText);
changeCommonUriAction = new Action(
UIText.AbstractConfigureRemoteDialog_ChangeUriLabel) {
@Override
public void run() {
SelectUriWizard wiz;
if (!commonUriText.getText().isEmpty()) {
wiz = new SelectUriWizard(true, commonUriText.getText());
} else {
wiz = new SelectUriWizard(true);
}
if (new WizardDialog(getShell(), wiz).open() == Window.OK) {
if (!commonUriText.getText().isEmpty()) {
try {
getConfig().removeURI(
new URIish(commonUriText.getText()));
} catch (URISyntaxException ex) {
Activator.handleError(ex.getMessage(), ex, true);
}
}
getConfig().addURI(wiz.getUri());
updateControls();
}
}
};
deleteCommonUriAction = new Action(
UIText.AbstractConfigureRemoteDialog_DeleteUriLabel) {
@Override
public void run() {
getConfig().removeURI(getConfig().getURIs().get(0));
updateControls();
}
};
createActionButton(sameUriDetails, SWT.PUSH, changeCommonUriAction);
createActionButton(sameUriDetails, SWT.PUSH, deleteCommonUriAction)
.setEnabled(false);
commonUriText.addModifyListener(event -> deleteCommonUriAction
.setEnabled(!commonUriText.getText().isEmpty()));
createAdditionalUriArea(main);
final Group refSpecGroup = new Group(main, SWT.SHADOW_ETCHED_IN);
GridDataFactory.fillDefaults().grab(true, true)
.minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(refSpecGroup);
refSpecGroup.setText(UIText.AbstractConfigureRemoteDialog_RefMappingGroup);
GridLayoutFactory.fillDefaults().numColumns(2).applyTo(refSpecGroup);
specViewer = new TableViewer(refSpecGroup, SWT.BORDER | SWT.MULTI);
specViewer.setContentProvider(ArrayContentProvider.getInstance());
GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 150)
.minSize(SWT.DEFAULT, 30).grab(true, true)
.applyTo(specViewer.getTable());
addRefSpecAction = new Action(
UIText.AbstractConfigureRemoteDialog_AddRefSpecLabel) {
@Override
public void run() {
doAddRefSpec();
}
};
changeRefSpecAction = new Action(
UIText.AbstractConfigureRemoteDialog_ChangeRefSpecLabel) {
@Override
public void run() {
doChangeRefSpec();
}
};
addRefSpecAdvancedAction = new Action(
UIText.AbstractConfigureRemoteDialog_EditAdvancedLabel) {
@Override
public void run() {
doAdvanced();
}
};
IAction deleteRefSpecAction = ActionUtils.createGlobalAction(
ActionFactory.DELETE, () -> doDeleteRefSpecs());
IAction copyRefSpecAction = ActionUtils
.createGlobalAction(ActionFactory.COPY, () -> doCopy());
IAction pasteRefSpecAction = ActionUtils
.createGlobalAction(ActionFactory.PASTE, () -> doPaste());
IAction selectAllRefSpecsAction = ActionUtils.createGlobalAction(
ActionFactory.SELECT_ALL,
() -> {
specViewer.getTable().selectAll();
// selectAll doesn't fire a "selection changed" event
specViewer.setSelection(specViewer.getSelection());
});
Composite buttonArea = new Composite(refSpecGroup, SWT.NONE);
GridLayoutFactory.fillDefaults().applyTo(buttonArea);
GridDataFactory.fillDefaults().grab(false, true)
.minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(buttonArea);
createActionButton(buttonArea, SWT.PUSH, addRefSpecAction);
createActionButton(buttonArea, SWT.PUSH, changeRefSpecAction);
createActionButton(buttonArea, SWT.PUSH, deleteRefSpecAction);
createActionButton(buttonArea, SWT.PUSH, copyRefSpecAction);
createActionButton(buttonArea, SWT.PUSH, pasteRefSpecAction);
createActionButton(buttonArea, SWT.PUSH, addRefSpecAdvancedAction);
MenuManager contextMenu = new MenuManager();
contextMenu.setRemoveAllWhenShown(true);
contextMenu.addMenuListener(manager -> {
specViewer.getTable().setFocus();
if (addRefSpecAction.isEnabled()) {
manager.add(addRefSpecAction);
}
if (changeRefSpecAction.isEnabled()) {
manager.add(changeRefSpecAction);
}
if (deleteRefSpecAction.isEnabled()) {
manager.add(deleteRefSpecAction);
}
manager.add(new Separator());
manager.add(copyRefSpecAction);
manager.add(pasteRefSpecAction);
manager.add(selectAllRefSpecsAction);
});
specViewer.getTable()
.setMenu(contextMenu.createContextMenu(specViewer.getTable()));
ActionUtils.setGlobalActions(specViewer.getTable(), deleteRefSpecAction,
copyRefSpecAction, pasteRefSpecAction, selectAllRefSpecsAction);
specViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
IStructuredSelection sel = (IStructuredSelection) specViewer
.getSelection();
copyRefSpecAction.setEnabled(sel.size() == 1);
changeRefSpecAction.setEnabled(sel.size() == 1);
deleteRefSpecAction.setEnabled(!sel.isEmpty());
selectAllRefSpecsAction.setEnabled(specViewer.getTable()
.getItemCount() > 0
&& sel.size() != specViewer.getTable().getItemCount());
}
});
// Initial action enablement (no selection in the specViewer):
copyRefSpecAction.setEnabled(false);
changeRefSpecAction.setEnabled(false);
deleteRefSpecAction.setEnabled(false);
applyDialogFont(main);
return main;
}
/**
* Add a warning about this remote being used by other branches
*
* @param parent
*/
private void addDefaultOriginWarning(Composite parent) {
List<String> otherBranches = new ArrayList<>();
String currentBranch;
try {
currentBranch = getRepository().getBranch();
} catch (IOException e) {
// just don't show this warning
return;
}
String currentRemote = getConfig().getName();
Config repositoryConfig = getRepository().getConfig();
Set<String> branches = repositoryConfig
.getSubsections(ConfigConstants.CONFIG_BRANCH_SECTION);
for (String branch : branches) {
if (branch.equals(currentBranch)) {
continue;
}
String remote = repositoryConfig.getString(
ConfigConstants.CONFIG_BRANCH_SECTION, branch,
ConfigConstants.CONFIG_KEY_REMOTE);
if ((remote == null
&& currentRemote.equals(Constants.DEFAULT_REMOTE_NAME))
|| (remote != null && remote.equals(currentRemote))) {
otherBranches.add(branch);
}
}
if (otherBranches.isEmpty()) {
return;
}
Composite warningAboutOrigin = new Composite(parent, SWT.NONE);
GridLayoutFactory.fillDefaults().numColumns(2)
.applyTo(warningAboutOrigin);
Label warningLabel = new Label(warningAboutOrigin, SWT.NONE);
warningLabel.setImage(PlatformUI.getWorkbench().getSharedImages()
.getImage(ISharedImages.IMG_OBJS_WARN_TSK));
Text warningText = new Text(warningAboutOrigin, SWT.READ_ONLY);
warningText.setText(NLS.bind(
UIText.AbstractConfigureRemoteDialog_ReusedRemoteWarning,
getConfig().getName(), Integer.valueOf(otherBranches.size())));
warningText.setToolTipText(otherBranches.toString());
GridDataFactory.fillDefaults().grab(true, false).applyTo(warningLabel);
}
private void addRefSpec(RefSpec spec) {
if (isPush) {
getConfig().addPushRefSpec(spec);
} else {
getConfig().addFetchRefSpec(spec);
}
}
private void removeRefSpec(RefSpec spec) {
if (isPush) {
getConfig().removePushRefSpec(spec);
} else {
getConfig().removeFetchRefSpec(spec);
}
}
/**
* Hook method to create an additional area between the URI and the
* {@link RefSpec} viewer. This default implementation does nothing.
*
* @param parent
* {@link Composite} the additional area shall use as parent
* @return the {@link Control}, or {@code null} if none
*/
protected Control createAdditionalUriArea(Composite parent) {
return null;
}
/**
* Validate and enable/disable controls depending on the current state.
*/
protected abstract void updateControls();
@Override
protected final void createButtonsForButtonBar(Composite parent) {
createOkButton(parent);
createButton(parent, SAVE_ONLY,
UIText.AbstractConfigureRemoteDialog_SaveButton, false);
createButton(parent, DRY_RUN,
UIText.AbstractConfigureRemoteDialog_DryRunButton, false);
createButton(parent, REVERT,
UIText.AbstractConfigureRemoteDialog_RevertButton, false);
createButton(parent, IDialogConstants.CANCEL_ID,
IDialogConstants.CANCEL_LABEL, false);
}
@Override
protected final void buttonPressed(int buttonId) {
switch (buttonId) {
case DRY_RUN:
try {
new ProgressMonitorDialog(getShell()).run(true, true,
(monitor) -> dryRun(monitor));
} catch (InvocationTargetException e) {
Activator.showError(e.getMessage(), e);
} catch (InterruptedException e1) {
// Ignore cancellation here
}
return;
case REVERT:
try {
config = new RemoteConfig(repository.getConfig(),
config.getName());
updateControls();
} catch (URISyntaxException e) {
Activator.handleError(e.getMessage(), e, true);
}
return;
case OK:
case SAVE_ONLY:
StoredConfig repoConfig = getRepository().getConfig();
boolean saved = false;
try {
config.update(repoConfig);
repoConfig.save();
saved = true;
} catch (IOException e) {
Activator.handleError(e.getMessage(), e, true);
}
if (saved) {
GerritDialogSettings.updateRemoteConfig(repository, config);
}
if (buttonId == OK) {
performOperation();
}
okPressed();
return;
default:
break;
}
super.buttonPressed(buttonId);
}
private void doPaste() {
Clipboard clipboard = new Clipboard(getShell().getDisplay());
try {
String content = (String) clipboard
.getContents(TextTransfer.getInstance());
if (content == null) {
MessageDialog.openConfirm(getShell(),
UIText.AbstractConfigureRemoteDialog_EmptyClipboardDialogTitle,
UIText.AbstractConfigureRemoteDialog_EmptyClipboardDialogMessage);
}
try {
RefSpec spec = new RefSpec(content);
Ref source;
try {
// TODO better checks for wild-cards and such
source = getRepository().findRef(isPush ? spec.getSource()
: spec.getDestination());
} catch (IOException e) {
source = null;
}
if (source != null || MessageDialog.openQuestion(getShell(),
UIText.AbstractConfigureRemoteDialog_InvalidRefDialogTitle,
NLS.bind(
UIText.AbstractConfigureRemoteDialog_InvalidRefDialogMessage,
spec.toString()))) {
addRefSpec(spec);
}
updateControls();
} catch (IllegalArgumentException e) {
MessageDialog.openError(getShell(),
UIText.AbstractConfigureRemoteDialog_NoRefSpecDialogTitle,
UIText.AbstractConfigureRemoteDialog_NoRefSpecDialogMessage);
}
} finally {
clipboard.dispose();
}
}
private void doCopy() {
String toCopy = ((IStructuredSelection) specViewer.getSelection())
.getFirstElement().toString();
Clipboard clipboard = new Clipboard(getShell().getDisplay());
try {
clipboard.setContents(new String[] { toCopy },
new TextTransfer[] { TextTransfer.getInstance() });
} finally {
clipboard.dispose();
}
}
private void doAddRefSpec() {
RefSpec spec = getNewRefSpec();
if (spec != null) {
addRefSpec(spec);
updateControls();
}
}
private void doChangeRefSpec() {
RefSpec oldSpec = (RefSpec) ((IStructuredSelection) specViewer
.getSelection()).getFirstElement();
RefSpecDialog dlg = new RefSpecDialog(getShell(), getRepository(),
getConfig(), oldSpec, isPush);
if (dlg.open() == Window.OK) {
removeRefSpec(oldSpec);
addRefSpec(dlg.getSpec());
}
updateControls();
}
private void doDeleteRefSpecs() {
for (Object spec : ((IStructuredSelection) specViewer.getSelection())
.toArray()) {
removeRefSpec((RefSpec) spec);
}
updateControls();
}
private void doAdvanced() {
RefSpecWizard wizard = new RefSpecWizard(getRepository(), getConfig(),
isPush);
if (new WizardDialog(getShell(), wizard).open() == Window.OK) {
updateControls();
}
}
private Button createActionButton(Composite parent, int style,
IAction action) {
Button button = new Button(parent, style);
button.setText(action.getText());
button.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
action.run();
}
});
IPropertyChangeListener listener = event -> {
if (IAction.ENABLED.equals(event.getProperty())) {
if (!button.isDisposed()) {
if (Display.getCurrent() == null) {
button.getShell().getDisplay().syncExec(() -> {
if (!button.isDisposed()) {
button.setEnabled(action.isEnabled());
}
});
} else {
button.setEnabled(action.isEnabled());
}
}
}
};
button.addDisposeListener(
event -> action.removePropertyChangeListener(listener));
action.addPropertyChangeListener(listener);
GridDataFactory.fillDefaults().applyTo(button);
return button;
}
}