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
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
;
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
;
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
>(
111 private static final int TOPIC_PROPOSALS_MAXIMUM
= 20;
114 protected boolean removeEldestEntry(Map
.Entry
<String
, String
> eldest
) {
115 return size() > TOPIC_PROPOSALS_MAXIMUM
;
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
;
134 protected IDialogSettings
getDialogSettings() {
135 return GerritDialogSettings
136 .getSection(GerritDialogSettings
.PUSH_TO_GERRIT_SECTION
);
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)
149 uriCombo
.addModifyListener(new ModifyListener() {
151 public void modifyText(ModifyEvent e
) {
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() {
169 public void modifyText(ModifyEvent e
) {
174 // give focus to the branchText if label is activated using the mnemonic
175 branchTextlabel
.addTraverseListener(new TraverseListener() {
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)
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)
193 topicText
.addModifyListener(new ModifyListener() {
196 public void modifyText(ModifyEvent e
) {
200 topicLabel
.addTraverseListener(new TraverseListener() {
203 public void keyTraversed(TraverseEvent e
) {
204 topicText
.setFocus();
205 topicText
.selectAll();
209 useTopic
.addSelectionListener(new SelectionAdapter() {
212 public void widgetSelected(SelectionEvent e
) {
213 topicText
.setEnabled(useTopic
.getSelection());
218 // get all available Gerrit URIs from the repository
219 SortedSet
<String
> uris
= new TreeSet
<>();
221 for (RemoteConfig rc
: RemoteConfig
.getAllRemoteConfigs(repository
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
) {
241 initializeTopic(branchText
.getText());
242 addTopicProposal(topicText
);
243 branchText
.setFocus();
244 Dialog
.applyDialogFont(main
);
248 private void loadKnownRemoteRefs() {
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
,
274 boolean isValid
= validateTopic(topic
) == null;
275 if (topic
.equals(branch
)) {
277 } else if (topic
.isEmpty()) {
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();
288 config
.setBoolean(ConfigConstants
.CONFIG_BRANCH_SECTION
, branch
,
289 GERRIT_TOPIC_USE_KEY
, enabled
);
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
,
297 } else if (isValid
) {
298 config
.setString(ConfigConstants
.CONFIG_BRANCH_SECTION
, branch
,
299 GERRIT_TOPIC_KEY
, topic
);
303 } catch (IOException e
) {
305 NLS
.bind(UIText
.PushToGerritPage_TopicSaveFailure
,
312 private void selectLastUsedUri() {
313 String lastUri
= settings
.get(lastUriKey
);
314 if (lastUri
!= null) {
315 int i
= uriCombo
.indexOf(lastUri
);
324 private void setLastUsedBranch() {
325 String lastBranch
= settings
.get(lastBranchKey
);
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;
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
)) {
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
,
364 if (storedTopic
== null || storedTopic
.isEmpty()) {
365 if (branch
!= null && !branch
.isEmpty()
366 && !branch
.equals(remoteBranch
)) {
367 topicText
.setText(branch
);
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);
389 if (uriCombo
.getText().length() == 0) {
390 setErrorMessage(UIText
.PushToGerritPage_MissingUriMessage
);
393 if (branchText
.getText().trim().isEmpty()) {
394 setErrorMessage(UIText
.PushToGerritPage_MissingBranchMessage
);
397 if (topicText
.isEnabled()) {
398 setErrorMessage(validateTopic(topicText
.getText().trim()));
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('%');
416 withTopic
= withTopic
.substring(0, i
);
418 withTopic
+= '/' + topic
;
419 if (knownRemoteRefs
.contains(withTopic
)) {
420 return NLS
.bind(UIText
.PushToGerritPage_TopicCollidesWithBranch
,
427 private String
setTopicInRef(String ref
, String topic
) {
430 int i
= ref
.indexOf('%');
432 baseRef
= ref
.substring(0, i
);
433 options
= ref
.substring(i
+ 1);
434 options
= options
.replaceAll("topic=[^,]*", ""); //$NON-NLS-1$ //$NON-NLS-2$
437 options
= ""; //$NON-NLS-1$
439 if (topic
.indexOf(',') >= 0) {
440 // Cannot use %topic=, since Gerrit splits on commas
441 baseRef
+= '/' + topic
;
443 if (!options
.isEmpty()) {
446 options
+= "topic=" + topic
; //$NON-NLS-1$
448 if (!options
.isEmpty()) {
449 return baseRef
+ '%' + options
;
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
,
470 storeLastUsedUri(uriCombo
.getText());
471 storeLastUsedBranch(branchText
.getText());
472 storeLastUsedTopic(topicText
.isEnabled(),
473 topicText
.getText().trim(), repository
.getBranch());
474 op
.setPushMode(PushMode
.GERRIT
);
476 } catch (URISyntaxException
| IOException e
) {
477 Activator
.handleError(e
.getMessage(), e
, true);
481 private void addTopicProposal(Text textField
) {
482 if (topicProposals
.isEmpty()) {
485 KeyStroke stroke
= UIUtils
.getKeystrokeOfBestActiveBindingFor(
486 IWorkbenchCommandConstants
.EDIT_CONTENT_ASSIST
);
487 if (stroke
!= null) {
488 UIUtils
.addBulbDecorator(textField
,
490 UIText
.PushToGerritPage_TopicContentProposalHoverText
,
493 String
[] recentTopics
= topicProposals
.keySet()
494 .toArray(new String
[topicProposals
.size()]);
495 Arrays
.sort(recentTopics
, CommonUtils
.STRING_ASCENDING_COMPARATOR
);
496 SimpleContentProposalProvider proposalProvider
= new SimpleContentProposalProvider(
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
) -> {
509 && !pattern
.matcher(refName
).matches()) {
512 return new ContentProposal(refName
);
513 }, null, UIText
.PushToGerritPage_ContentProposalStartTypingText
,
514 UIText
.PushToGerritPage_ContentProposalHoverText
);