Generalize UIUtils.addContentProposalToText a bit more
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / push / PushToGerritPage.java
blob034b86efba961aa256d2e6bdc34551f4a1ea17d8
1 /*******************************************************************************
2 * Copyright (c) 2012, 2016 SAP SE and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
8 * Contributors:
9 * Mathias Kinzler (SAP AG) - initial implementation
10 * Christian Georgi (SAP SE) - Bug 466900 (Make PushResultDialog amodal)
11 * Thomas Wolf <thomas.wolf@paranor.ch> - Bug 449493: Topic input
12 *******************************************************************************/
13 package org.eclipse.egit.ui.internal.push;
15 import java.io.IOException;
16 import java.net.URISyntaxException;
17 import java.util.Arrays;
18 import java.util.LinkedHashMap;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.SortedSet;
22 import java.util.TreeSet;
23 import java.util.regex.Pattern;
25 import org.eclipse.egit.core.internal.gerrit.GerritUtil;
26 import org.eclipse.egit.core.op.PushOperationSpecification;
27 import org.eclipse.egit.ui.Activator;
28 import org.eclipse.egit.ui.UIUtils;
29 import org.eclipse.egit.ui.internal.CommonUtils;
30 import org.eclipse.egit.ui.internal.UIText;
31 import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings;
32 import org.eclipse.jface.bindings.keys.KeyStroke;
33 import org.eclipse.jface.dialogs.Dialog;
34 import org.eclipse.jface.dialogs.IDialogSettings;
35 import org.eclipse.jface.fieldassist.ContentProposal;
36 import org.eclipse.jface.fieldassist.ContentProposalAdapter;
37 import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
38 import org.eclipse.jface.fieldassist.TextContentAdapter;
39 import org.eclipse.jface.layout.GridDataFactory;
40 import org.eclipse.jface.wizard.WizardPage;
41 import org.eclipse.jgit.lib.BranchConfig;
42 import org.eclipse.jgit.lib.ConfigConstants;
43 import org.eclipse.jgit.lib.Constants;
44 import org.eclipse.jgit.lib.ObjectId;
45 import org.eclipse.jgit.lib.Ref;
46 import org.eclipse.jgit.lib.Repository;
47 import org.eclipse.jgit.lib.StoredConfig;
48 import org.eclipse.jgit.transport.RemoteConfig;
49 import org.eclipse.jgit.transport.RemoteRefUpdate;
50 import org.eclipse.jgit.transport.URIish;
51 import org.eclipse.osgi.util.NLS;
52 import org.eclipse.swt.SWT;
53 import org.eclipse.swt.events.ModifyEvent;
54 import org.eclipse.swt.events.ModifyListener;
55 import org.eclipse.swt.events.SelectionAdapter;
56 import org.eclipse.swt.events.SelectionEvent;
57 import org.eclipse.swt.events.TraverseEvent;
58 import org.eclipse.swt.events.TraverseListener;
59 import org.eclipse.swt.layout.GridLayout;
60 import org.eclipse.swt.widgets.Button;
61 import org.eclipse.swt.widgets.Combo;
62 import org.eclipse.swt.widgets.Composite;
63 import org.eclipse.swt.widgets.Label;
64 import org.eclipse.swt.widgets.Text;
65 import org.eclipse.ui.IWorkbenchCommandConstants;
67 /**
68 * Push the current HEAD to Gerrit
70 public class PushToGerritPage extends WizardPage {
71 private static final String LAST_BRANCH_POSTFIX = ".lastBranch"; //$NON-NLS-1$
73 private static final String LAST_TOPICS_POSTFIX = ".lastTopics"; //$NON-NLS-1$
75 private static final String GERRIT_TOPIC_KEY = "gerritTopic"; //$NON-NLS-1$
77 private static final String GERRIT_TOPIC_USE_KEY = "gerritTopicUse"; //$NON-NLS-1$
79 private static final Pattern WHITESPACE = Pattern
80 .compile("\\p{javaWhitespace}"); //$NON-NLS-1$
82 private final Repository repository;
84 private final IDialogSettings settings;
86 private final String lastUriKey;
88 private final String lastBranchKey;
90 private Combo uriCombo;
92 private Combo prefixCombo;
94 private Label branchTextlabel;
96 private Text branchText;
98 private Button useTopic;
100 private Label topicLabel;
102 private Text topicText;
104 private Set<String> knownRemoteRefs = new TreeSet<>(
105 String.CASE_INSENSITIVE_ORDER);
107 @SuppressWarnings("serial")
108 private Map<String, String> topicProposals = new LinkedHashMap<String, String>(
109 30, 0.75f, true) {
111 private static final int TOPIC_PROPOSALS_MAXIMUM = 20;
113 @Override
114 protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
115 return size() > TOPIC_PROPOSALS_MAXIMUM;
120 * @param repository
122 PushToGerritPage(Repository repository) {
123 super(PushToGerritPage.class.getName());
124 this.repository = repository;
125 setTitle(NLS.bind(UIText.PushToGerritPage_Title, Activator.getDefault()
126 .getRepositoryUtil().getRepositoryName(repository)));
127 setMessage(UIText.PushToGerritPage_Message);
128 settings = getDialogSettings();
129 lastUriKey = repository + GerritDialogSettings.LAST_URI_SUFFIX;
130 lastBranchKey = repository + LAST_BRANCH_POSTFIX;
133 @Override
134 protected IDialogSettings getDialogSettings() {
135 return GerritDialogSettings
136 .getSection(GerritDialogSettings.PUSH_TO_GERRIT_SECTION);
139 @Override
140 public void createControl(Composite parent) {
141 loadKnownRemoteRefs();
142 Composite main = new Composite(parent, SWT.NONE);
143 main.setLayout(new GridLayout(3, false));
144 GridDataFactory.fillDefaults().grab(true, true).applyTo(main);
145 new Label(main, SWT.NONE).setText(UIText.PushToGerritPage_UriLabel);
146 uriCombo = new Combo(main, SWT.DROP_DOWN);
147 GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
148 .applyTo(uriCombo);
149 uriCombo.addModifyListener(new ModifyListener() {
150 @Override
151 public void modifyText(ModifyEvent e) {
152 checkPage();
156 branchTextlabel = new Label(main, SWT.NONE);
158 // we visualize the prefix here
159 prefixCombo = new Combo(main, SWT.READ_ONLY | SWT.DROP_DOWN);
160 prefixCombo.add(GerritUtil.REFS_FOR);
161 prefixCombo.add(GerritUtil.REFS_DRAFTS);
162 prefixCombo.select(0);
164 branchTextlabel.setText(UIText.PushToGerritPage_BranchLabel);
165 branchText = new Text(main, SWT.SINGLE | SWT.BORDER);
166 GridDataFactory.fillDefaults().grab(true, false).applyTo(branchText);
167 branchText.addModifyListener(new ModifyListener() {
168 @Override
169 public void modifyText(ModifyEvent e) {
170 checkPage();
174 // give focus to the branchText if label is activated using the mnemonic
175 branchTextlabel.addTraverseListener(new TraverseListener() {
176 @Override
177 public void keyTraversed(TraverseEvent e) {
178 branchText.setFocus();
179 branchText.selectAll();
182 addRefContentProposalToText(branchText);
184 useTopic = new Button(main, SWT.CHECK | SWT.LEFT);
185 useTopic.setText(UIText.PushToGerritPage_TopicUseLabel);
186 GridDataFactory.fillDefaults().grab(true, false).span(3, 1)
187 .applyTo(useTopic);
188 topicLabel = new Label(main, SWT.NONE);
189 topicLabel.setText(UIText.PushToGerritPage_TopicLabel);
190 topicText = new Text(main, SWT.SINGLE | SWT.BORDER);
191 GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
192 .applyTo(topicText);
193 topicText.addModifyListener(new ModifyListener() {
195 @Override
196 public void modifyText(ModifyEvent e) {
197 checkPage();
200 topicLabel.addTraverseListener(new TraverseListener() {
202 @Override
203 public void keyTraversed(TraverseEvent e) {
204 topicText.setFocus();
205 topicText.selectAll();
209 useTopic.addSelectionListener(new SelectionAdapter() {
211 @Override
212 public void widgetSelected(SelectionEvent e) {
213 topicText.setEnabled(useTopic.getSelection());
214 checkPage();
218 // get all available Gerrit URIs from the repository
219 SortedSet<String> uris = new TreeSet<>();
220 try {
221 for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(repository
222 .getConfig())) {
223 if (GerritUtil.isGerritPush(rc)) {
224 if (rc.getURIs().size() > 0) {
225 uris.add(rc.getURIs().get(0).toPrivateString());
227 for (URIish u : rc.getPushURIs()) {
228 uris.add(u.toPrivateString());
232 } catch (URISyntaxException e) {
233 Activator.handleError(e.getMessage(), e, false);
234 setErrorMessage(e.getMessage());
236 for (String aUri : uris) {
237 uriCombo.add(aUri);
239 selectLastUsedUri();
240 setLastUsedBranch();
241 initializeTopic(branchText.getText());
242 addTopicProposal(topicText);
243 branchText.setFocus();
244 Dialog.applyDialogFont(main);
245 setControl(main);
248 private void loadKnownRemoteRefs() {
249 try {
250 Set<String> remotes = repository.getRefDatabase()
251 .getRefs(Constants.R_REMOTES).keySet();
252 for (String remote : remotes) {
253 // these are "origin/master", "origin/xxx"...
254 int slashIndex = remote.indexOf('/');
255 if (slashIndex > 0 && slashIndex < remote.length() - 1) {
256 knownRemoteRefs.add(remote.substring(slashIndex + 1));
259 } catch (IOException e) {
260 // simply ignore, no proposals and no topic check then
264 private void storeLastUsedUri(String uri) {
265 settings.put(lastUriKey, uri.trim());
268 private void storeLastUsedBranch(String branch) {
269 settings.put(lastBranchKey, branch.trim());
272 private void storeLastUsedTopic(boolean enabled, String topic,
273 String branch) {
274 boolean isValid = validateTopic(topic) == null;
275 if (topic.equals(branch)) {
276 topic = null;
277 } else if (topic.isEmpty()) {
278 enabled = false;
279 } else if (isValid) {
280 topicProposals.put(topic, null);
281 settings.put(repository + LAST_TOPICS_POSTFIX, topicProposals
282 .keySet().toArray(new String[topicProposals.size()]));
284 if (branch != null && !ObjectId.isId(branch)) {
285 // Don't store on detached HEAD
286 StoredConfig config = repository.getConfig();
287 if (enabled) {
288 config.setBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
289 GERRIT_TOPIC_USE_KEY, enabled);
290 } else {
291 config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
292 GERRIT_TOPIC_USE_KEY);
294 if (topic == null || topic.isEmpty()) {
295 config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
296 GERRIT_TOPIC_KEY);
297 } else if (isValid) {
298 config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
299 GERRIT_TOPIC_KEY, topic);
301 try {
302 config.save();
303 } catch (IOException e) {
304 Activator.logError(
305 NLS.bind(UIText.PushToGerritPage_TopicSaveFailure,
306 repository),
312 private void selectLastUsedUri() {
313 String lastUri = settings.get(lastUriKey);
314 if (lastUri != null) {
315 int i = uriCombo.indexOf(lastUri);
316 if (i != -1) {
317 uriCombo.select(i);
318 return;
321 uriCombo.select(0);
324 private void setLastUsedBranch() {
325 String lastBranch = settings.get(lastBranchKey);
326 try {
327 // use upstream if the current branch is tracking a branch
328 final BranchConfig branchConfig = new BranchConfig(
329 repository.getConfig(), repository.getBranch());
330 final String trackedBranch = branchConfig.getMerge();
331 if (trackedBranch != null) {
332 lastBranch = trackedBranch.replace(Constants.R_HEADS, ""); //$NON-NLS-1$
334 } catch (final IOException e) {
335 throw new RuntimeException(e);
337 if (lastBranch != null) {
338 branchText.setText(lastBranch);
342 private void initializeTopic(String remoteBranch) {
343 boolean enabled = false;
344 String storedTopic = null;
345 String branch = null;
346 try {
347 branch = repository.getBranch();
348 // On detached HEAD don't do anything: "Use topic" will be disabled
349 // and the topic field empty.
350 if (ObjectId.isId(branch)) {
351 branch = null;
353 } catch (final IOException e) {
354 Activator.logError(e.getLocalizedMessage(), e);
356 if (branch != null) {
357 StoredConfig config = repository.getConfig();
358 enabled = config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
359 branch, GERRIT_TOPIC_USE_KEY, false);
360 storedTopic = config.getString(
361 ConfigConstants.CONFIG_BRANCH_SECTION, branch,
362 GERRIT_TOPIC_KEY);
364 if (storedTopic == null || storedTopic.isEmpty()) {
365 if (branch != null && !branch.isEmpty()
366 && !branch.equals(remoteBranch)) {
367 topicText.setText(branch);
369 } else {
370 topicText.setText(storedTopic);
372 useTopic.setSelection(enabled);
373 topicText.setEnabled(enabled);
374 // Load topicProposals from settings.
375 String[] proposals = settings
376 .getArray(repository + LAST_TOPICS_POSTFIX);
377 if (proposals != null) {
378 for (int i = proposals.length - 1; i >= 0; i--) {
379 if (!proposals[i].isEmpty()) {
380 topicProposals.put(proposals[i], null);
386 private void checkPage() {
387 setErrorMessage(null);
388 try {
389 if (uriCombo.getText().length() == 0) {
390 setErrorMessage(UIText.PushToGerritPage_MissingUriMessage);
391 return;
393 if (branchText.getText().trim().isEmpty()) {
394 setErrorMessage(UIText.PushToGerritPage_MissingBranchMessage);
395 return;
397 if (topicText.isEnabled()) {
398 setErrorMessage(validateTopic(topicText.getText().trim()));
400 } finally {
401 setPageComplete(getErrorMessage() == null);
405 private String validateTopic(String topic) {
406 if (WHITESPACE.matcher(topic).find()) {
407 return UIText.PushToGerritPage_TopicHasWhitespace;
409 if (topic.indexOf(',') >= 0) {
410 if (topic.indexOf('%') >= 0) {
411 return UIText.PushToGerritPage_TopicInvalidCharacters;
413 String withTopic = branchText.getText().trim();
414 int i = withTopic.indexOf('%');
415 if (i >= 0) {
416 withTopic = withTopic.substring(0, i);
418 withTopic += '/' + topic;
419 if (knownRemoteRefs.contains(withTopic)) {
420 return NLS.bind(UIText.PushToGerritPage_TopicCollidesWithBranch,
421 withTopic);
424 return null;
427 private String setTopicInRef(String ref, String topic) {
428 String baseRef;
429 String options;
430 int i = ref.indexOf('%');
431 if (i >= 0) {
432 baseRef = ref.substring(0, i);
433 options = ref.substring(i + 1);
434 options = options.replaceAll("topic=[^,]*", ""); //$NON-NLS-1$ //$NON-NLS-2$
435 } else {
436 baseRef = ref;
437 options = ""; //$NON-NLS-1$
439 if (topic.indexOf(',') >= 0) {
440 // Cannot use %topic=, since Gerrit splits on commas
441 baseRef += '/' + topic;
442 } else {
443 if (!options.isEmpty()) {
444 options += ',';
446 options += "topic=" + topic; //$NON-NLS-1$
448 if (!options.isEmpty()) {
449 return baseRef + '%' + options;
451 return baseRef;
454 void doPush() {
455 try {
456 URIish uri = new URIish(uriCombo.getText());
457 Ref currentHead = repository.exactRef(Constants.HEAD);
458 String ref = prefixCombo.getItem(prefixCombo.getSelectionIndex())
459 + branchText.getText().trim();
460 if (topicText.isEnabled()) {
461 ref = setTopicInRef(ref, topicText.getText().trim());
463 RemoteRefUpdate update = new RemoteRefUpdate(repository,
464 currentHead, ref, false, null, null);
465 PushOperationSpecification spec = new PushOperationSpecification();
467 spec.addURIRefUpdates(uri, Arrays.asList(update));
468 final PushOperationUI op = new PushOperationUI(repository, spec,
469 false);
470 storeLastUsedUri(uriCombo.getText());
471 storeLastUsedBranch(branchText.getText());
472 storeLastUsedTopic(topicText.isEnabled(),
473 topicText.getText().trim(), repository.getBranch());
474 op.setPushMode(PushMode.GERRIT);
475 op.start();
476 } catch (URISyntaxException | IOException e) {
477 Activator.handleError(e.getMessage(), e, true);
481 private void addTopicProposal(Text textField) {
482 if (topicProposals.isEmpty()) {
483 return;
485 KeyStroke stroke = UIUtils.getKeystrokeOfBestActiveBindingFor(
486 IWorkbenchCommandConstants.EDIT_CONTENT_ASSIST);
487 if (stroke != null) {
488 UIUtils.addBulbDecorator(textField,
489 NLS.bind(
490 UIText.PushToGerritPage_TopicContentProposalHoverText,
491 stroke.format()));
493 String[] recentTopics = topicProposals.keySet()
494 .toArray(new String[topicProposals.size()]);
495 Arrays.sort(recentTopics, CommonUtils.STRING_ASCENDING_COMPARATOR);
496 SimpleContentProposalProvider proposalProvider = new SimpleContentProposalProvider(
497 recentTopics);
498 proposalProvider.setFiltering(true);
499 ContentProposalAdapter adapter = new ContentProposalAdapter(textField,
500 new TextContentAdapter(), proposalProvider, stroke, null);
501 adapter.setProposalAcceptanceStyle(
502 ContentProposalAdapter.PROPOSAL_REPLACE);
505 private void addRefContentProposalToText(final Text textField) {
506 UIUtils.<String> addContentProposalToText(textField,
507 () -> knownRemoteRefs, (pattern, refName) -> {
508 if (pattern != null
509 && !pattern.matcher(refName).matches()) {
510 return null;
512 return new ContentProposal(refName);
513 }, null, UIText.PushToGerritPage_ContentProposalStartTypingText,
514 UIText.PushToGerritPage_ContentProposalHoverText);