/*******************************************************************************
* Copyright (c) 2012, 2016 SAP SE 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
* Christian Georgi (SAP SE) - Bug 466900 (Make PushResultDialog amodal)
* Thomas Wolf <thomas.wolf@paranor.ch> - Bug 449493: Topic input
*******************************************************************************/
package org.eclipse.egit.ui.internal.push;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;
import org.eclipse.egit.core.internal.gerrit.GerritUtil;
import org.eclipse.egit.core.op.PushOperationSpecification;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.fieldassist.ContentProposal;
import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.lib.BranchConfig;
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.RemoteConfig;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
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.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchCommandConstants;
/**
* Push the current HEAD to Gerrit
*/
public class PushToGerritPage extends WizardPage {
private static final String LAST_BRANCH_POSTFIX = ".lastBranch"; //$NON-NLS-1$
private static final String LAST_TOPICS_POSTFIX = ".lastTopics"; //$NON-NLS-1$
private static final String GERRIT_TOPIC_KEY = "gerritTopic"; //$NON-NLS-1$
private static final String GERRIT_TOPIC_USE_KEY = "gerritTopicUse"; //$NON-NLS-1$
private static final Pattern WHITESPACE = Pattern
.compile("\\p{javaWhitespace}"); //$NON-NLS-1$
private final Repository repository;
private final IDialogSettings settings;
private final String lastUriKey;
private final String lastBranchKey;
private Combo uriCombo;
private Combo prefixCombo;
private Label branchTextlabel;
private Text branchText;
private Button useTopic;
private Label topicLabel;
private Text topicText;
private Set<String> knownRemoteRefs = new TreeSet<>(
String.CASE_INSENSITIVE_ORDER);
@SuppressWarnings("serial")
private Map<String, String> topicProposals = new LinkedHashMap<String, String>(
30, 0.75f, true) {
private static final int TOPIC_PROPOSALS_MAXIMUM = 20;
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > TOPIC_PROPOSALS_MAXIMUM;
}
};
/**
* @param repository
*/
PushToGerritPage(Repository repository) {
super(PushToGerritPage.class.getName());
this.repository = repository;
setTitle(NLS.bind(UIText.PushToGerritPage_Title, Activator.getDefault()
.getRepositoryUtil().getRepositoryName(repository)));
setMessage(UIText.PushToGerritPage_Message);
settings = getDialogSettings();
lastUriKey = repository + GerritDialogSettings.LAST_URI_SUFFIX;
lastBranchKey = repository + LAST_BRANCH_POSTFIX;
}
@Override
protected IDialogSettings getDialogSettings() {
return GerritDialogSettings
.getSection(GerritDialogSettings.PUSH_TO_GERRIT_SECTION);
}
@Override
public void createControl(Composite parent) {
loadKnownRemoteRefs();
Composite main = new Composite(parent, SWT.NONE);
main.setLayout(new GridLayout(3, false));
GridDataFactory.fillDefaults().grab(true, true).applyTo(main);
new Label(main, SWT.NONE).setText(UIText.PushToGerritPage_UriLabel);
uriCombo = new Combo(main, SWT.DROP_DOWN);
GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
.applyTo(uriCombo);
uriCombo.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
branchTextlabel = new Label(main, SWT.NONE);
// we visualize the prefix here
prefixCombo = new Combo(main, SWT.READ_ONLY | SWT.DROP_DOWN);
prefixCombo.add(GerritUtil.REFS_FOR);
prefixCombo.add(GerritUtil.REFS_DRAFTS);
prefixCombo.select(0);
branchTextlabel.setText(UIText.PushToGerritPage_BranchLabel);
branchText = new Text(main, SWT.SINGLE | SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false).applyTo(branchText);
branchText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
// give focus to the branchText if label is activated using the mnemonic
branchTextlabel.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e) {
branchText.setFocus();
branchText.selectAll();
}
});
addRefContentProposalToText(branchText);
useTopic = new Button(main, SWT.CHECK | SWT.LEFT);
useTopic.setText(UIText.PushToGerritPage_TopicUseLabel);
GridDataFactory.fillDefaults().grab(true, false).span(3, 1)
.applyTo(useTopic);
topicLabel = new Label(main, SWT.NONE);
topicLabel.setText(UIText.PushToGerritPage_TopicLabel);
topicText = new Text(main, SWT.SINGLE | SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
.applyTo(topicText);
topicText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
topicLabel.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e) {
topicText.setFocus();
topicText.selectAll();
}
});
useTopic.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
topicText.setEnabled(useTopic.getSelection());
checkPage();
}
});
// get all available Gerrit URIs from the repository
SortedSet<String> uris = new TreeSet<>();
try {
for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(repository
.getConfig())) {
if (GerritUtil.isGerritPush(rc)) {
if (rc.getURIs().size() > 0) {
uris.add(rc.getURIs().get(0).toPrivateString());
}
for (URIish u : rc.getPushURIs()) {
uris.add(u.toPrivateString());
}
}
}
} catch (URISyntaxException e) {
Activator.handleError(e.getMessage(), e, false);
setErrorMessage(e.getMessage());
}
for (String aUri : uris) {
uriCombo.add(aUri);
}
selectLastUsedUri();
setLastUsedBranch();
initializeTopic(branchText.getText());
addTopicProposal(topicText);
branchText.setFocus();
Dialog.applyDialogFont(main);
setControl(main);
}
private void loadKnownRemoteRefs() {
try {
Set<String> remotes = repository.getRefDatabase()
.getRefs(Constants.R_REMOTES).keySet();
for (String remote : remotes) {
// these are "origin/master", "origin/xxx"...
int slashIndex = remote.indexOf('/');
if (slashIndex > 0 && slashIndex < remote.length() - 1) {
knownRemoteRefs.add(remote.substring(slashIndex + 1));
}
}
} catch (IOException e) {
// simply ignore, no proposals and no topic check then
}
}
private void storeLastUsedUri(String uri) {
settings.put(lastUriKey, uri.trim());
}
private void storeLastUsedBranch(String branch) {
settings.put(lastBranchKey, branch.trim());
}
private void storeLastUsedTopic(boolean enabled, String topic,
String branch) {
boolean isValid = validateTopic(topic) == null;
if (topic.equals(branch)) {
topic = null;
} else if (topic.isEmpty()) {
enabled = false;
} else if (isValid) {
topicProposals.put(topic, null);
settings.put(repository + LAST_TOPICS_POSTFIX, topicProposals
.keySet().toArray(new String[topicProposals.size()]));
}
if (branch != null && !ObjectId.isId(branch)) {
// Don't store on detached HEAD
StoredConfig config = repository.getConfig();
if (enabled) {
config.setBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
GERRIT_TOPIC_USE_KEY, enabled);
} else {
config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
GERRIT_TOPIC_USE_KEY);
}
if (topic == null || topic.isEmpty()) {
config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
GERRIT_TOPIC_KEY);
} else if (isValid) {
config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
GERRIT_TOPIC_KEY, topic);
}
try {
config.save();
} catch (IOException e) {
Activator.logError(
NLS.bind(UIText.PushToGerritPage_TopicSaveFailure,
repository),
e);
}
}
}
private void selectLastUsedUri() {
String lastUri = settings.get(lastUriKey);
if (lastUri != null) {
int i = uriCombo.indexOf(lastUri);
if (i != -1) {
uriCombo.select(i);
return;
}
}
uriCombo.select(0);
}
private void setLastUsedBranch() {
String lastBranch = settings.get(lastBranchKey);
try {
// use upstream if the current branch is tracking a branch
final BranchConfig branchConfig = new BranchConfig(
repository.getConfig(), repository.getBranch());
final String trackedBranch = branchConfig.getMerge();
if (trackedBranch != null) {
lastBranch = trackedBranch.replace(Constants.R_HEADS, ""); //$NON-NLS-1$
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
if (lastBranch != null) {
branchText.setText(lastBranch);
}
}
private void initializeTopic(String remoteBranch) {
boolean enabled = false;
String storedTopic = null;
String branch = null;
try {
branch = repository.getBranch();
// On detached HEAD don't do anything: "Use topic" will be disabled
// and the topic field empty.
if (ObjectId.isId(branch)) {
branch = null;
}
} catch (final IOException e) {
Activator.logError(e.getLocalizedMessage(), e);
}
if (branch != null) {
StoredConfig config = repository.getConfig();
enabled = config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
branch, GERRIT_TOPIC_USE_KEY, false);
storedTopic = config.getString(
ConfigConstants.CONFIG_BRANCH_SECTION, branch,
GERRIT_TOPIC_KEY);
}
if (storedTopic == null || storedTopic.isEmpty()) {
if (branch != null && !branch.isEmpty()
&& !branch.equals(remoteBranch)) {
topicText.setText(branch);
}
} else {
topicText.setText(storedTopic);
}
useTopic.setSelection(enabled);
topicText.setEnabled(enabled);
// Load topicProposals from settings.
String[] proposals = settings
.getArray(repository + LAST_TOPICS_POSTFIX);
if (proposals != null) {
for (int i = proposals.length - 1; i >= 0; i--) {
if (!proposals[i].isEmpty()) {
topicProposals.put(proposals[i], null);
}
}
}
}
private void checkPage() {
setErrorMessage(null);
try {
if (uriCombo.getText().length() == 0) {
setErrorMessage(UIText.PushToGerritPage_MissingUriMessage);
return;
}
if (branchText.getText().trim().isEmpty()) {
setErrorMessage(UIText.PushToGerritPage_MissingBranchMessage);
return;
}
if (topicText.isEnabled()) {
setErrorMessage(validateTopic(topicText.getText().trim()));
}
} finally {
setPageComplete(getErrorMessage() == null);
}
}
private String validateTopic(String topic) {
if (WHITESPACE.matcher(topic).find()) {
return UIText.PushToGerritPage_TopicHasWhitespace;
}
if (topic.indexOf(',') >= 0) {
if (topic.indexOf('%') >= 0) {
return UIText.PushToGerritPage_TopicInvalidCharacters;
}
String withTopic = branchText.getText().trim();
int i = withTopic.indexOf('%');
if (i >= 0) {
withTopic = withTopic.substring(0, i);
}
withTopic += '/' + topic;
if (knownRemoteRefs.contains(withTopic)) {
return NLS.bind(UIText.PushToGerritPage_TopicCollidesWithBranch,
withTopic);
}
}
return null;
}
private String setTopicInRef(String ref, String topic) {
String baseRef;
String options;
int i = ref.indexOf('%');
if (i >= 0) {
baseRef = ref.substring(0, i);
options = ref.substring(i + 1);
options = options.replaceAll("topic=[^,]*", ""); //$NON-NLS-1$ //$NON-NLS-2$
} else {
baseRef = ref;
options = ""; //$NON-NLS-1$
}
if (topic.indexOf(',') >= 0) {
// Cannot use %topic=, since Gerrit splits on commas
baseRef += '/' + topic;
} else {
if (!options.isEmpty()) {
options += ',';
}
options += "topic=" + topic; //$NON-NLS-1$
}
if (!options.isEmpty()) {
return baseRef + '%' + options;
}
return baseRef;
}
void doPush() {
try {
URIish uri = new URIish(uriCombo.getText());
Ref currentHead = repository.exactRef(Constants.HEAD);
String ref = prefixCombo.getItem(prefixCombo.getSelectionIndex())
+ branchText.getText().trim();
if (topicText.isEnabled()) {
ref = setTopicInRef(ref, topicText.getText().trim());
}
RemoteRefUpdate update = new RemoteRefUpdate(repository,
currentHead, ref, false, null, null);
PushOperationSpecification spec = new PushOperationSpecification();
spec.addURIRefUpdates(uri, Arrays.asList(update));
final PushOperationUI op = new PushOperationUI(repository, spec,
false);
storeLastUsedUri(uriCombo.getText());
storeLastUsedBranch(branchText.getText());
storeLastUsedTopic(topicText.isEnabled(),
topicText.getText().trim(), repository.getBranch());
op.setPushMode(PushMode.GERRIT);
op.start();
} catch (URISyntaxException | IOException e) {
Activator.handleError(e.getMessage(), e, true);
}
}
private void addTopicProposal(Text textField) {
if (topicProposals.isEmpty()) {
return;
}
KeyStroke stroke = UIUtils.getKeystrokeOfBestActiveBindingFor(
IWorkbenchCommandConstants.EDIT_CONTENT_ASSIST);
if (stroke != null) {
UIUtils.addBulbDecorator(textField,
NLS.bind(
UIText.PushToGerritPage_TopicContentProposalHoverText,
stroke.format()));
}
String[] recentTopics = topicProposals.keySet()
.toArray(new String[topicProposals.size()]);
Arrays.sort(recentTopics, CommonUtils.STRING_ASCENDING_COMPARATOR);
SimpleContentProposalProvider proposalProvider = new SimpleContentProposalProvider(
recentTopics);
proposalProvider.setFiltering(true);
ContentProposalAdapter adapter = new ContentProposalAdapter(textField,
new TextContentAdapter(), proposalProvider, stroke, null);
adapter.setProposalAcceptanceStyle(
ContentProposalAdapter.PROPOSAL_REPLACE);
}
private void addRefContentProposalToText(final Text textField) {
UIUtils.<String> addContentProposalToText(textField,
() -> knownRemoteRefs, (pattern, refName) -> {
if (pattern != null
&& !pattern.matcher(refName).matches()) {
return null;
}
return new ContentProposal(refName);
}, UIText.PushToGerritPage_ContentProposalStartTypingText,
UIText.PushToGerritPage_ContentProposalHoverText);
}
}