1 /*******************************************************************************
2 * Copyright (c) 2010, 2017 SAP AG 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 * Marc Khouzam (Ericsson) - Add an option not to checkout the new branch
11 * Thomas Wolf <thomas.wolf@paranor.ch> - Bug 493935, 495777
12 *******************************************************************************/
13 package org
.eclipse
.egit
.ui
.internal
.fetch
;
15 import java
.io
.IOException
;
16 import java
.lang
.reflect
.InvocationTargetException
;
17 import java
.net
.URISyntaxException
;
18 import java
.text
.MessageFormat
;
19 import java
.util
.ArrayList
;
20 import java
.util
.Collection
;
21 import java
.util
.Collections
;
22 import java
.util
.HashMap
;
23 import java
.util
.LinkedHashSet
;
24 import java
.util
.List
;
26 import java
.util
.Objects
;
28 import java
.util
.SortedSet
;
29 import java
.util
.TreeSet
;
30 import java
.util
.regex
.Matcher
;
31 import java
.util
.regex
.Pattern
;
33 import org
.eclipse
.core
.resources
.WorkspaceJob
;
34 import org
.eclipse
.core
.runtime
.CoreException
;
35 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
36 import org
.eclipse
.core
.runtime
.IStatus
;
37 import org
.eclipse
.core
.runtime
.Status
;
38 import org
.eclipse
.core
.runtime
.SubMonitor
;
39 import org
.eclipse
.core
.runtime
.jobs
.IJobChangeEvent
;
40 import org
.eclipse
.core
.runtime
.jobs
.Job
;
41 import org
.eclipse
.core
.runtime
.jobs
.JobChangeAdapter
;
42 import org
.eclipse
.egit
.core
.internal
.gerrit
.GerritUtil
;
43 import org
.eclipse
.egit
.core
.op
.CreateLocalBranchOperation
;
44 import org
.eclipse
.egit
.core
.op
.ListRemoteOperation
;
45 import org
.eclipse
.egit
.core
.op
.TagOperation
;
46 import org
.eclipse
.egit
.ui
.Activator
;
47 import org
.eclipse
.egit
.ui
.JobFamilies
;
48 import org
.eclipse
.egit
.ui
.UIPreferences
;
49 import org
.eclipse
.egit
.ui
.UIUtils
;
50 import org
.eclipse
.egit
.ui
.internal
.ActionUtils
;
51 import org
.eclipse
.egit
.ui
.internal
.UIText
;
52 import org
.eclipse
.egit
.ui
.internal
.ValidationUtils
;
53 import org
.eclipse
.egit
.ui
.internal
.branch
.BranchOperationUI
;
54 import org
.eclipse
.egit
.ui
.internal
.components
.BranchNameNormalizer
;
55 import org
.eclipse
.egit
.ui
.internal
.dialogs
.AbstractBranchSelectionDialog
;
56 import org
.eclipse
.egit
.ui
.internal
.dialogs
.BranchEditDialog
;
57 import org
.eclipse
.egit
.ui
.internal
.dialogs
.NonBlockingWizardDialog
;
58 import org
.eclipse
.egit
.ui
.internal
.gerrit
.GerritDialogSettings
;
59 import org
.eclipse
.jface
.bindings
.keys
.KeyStroke
;
60 import org
.eclipse
.jface
.dialogs
.Dialog
;
61 import org
.eclipse
.jface
.dialogs
.IDialogSettings
;
62 import org
.eclipse
.jface
.dialogs
.IInputValidator
;
63 import org
.eclipse
.jface
.dialogs
.IPageChangeProvider
;
64 import org
.eclipse
.jface
.dialogs
.IPageChangedListener
;
65 import org
.eclipse
.jface
.dialogs
.PageChangedEvent
;
66 import org
.eclipse
.jface
.fieldassist
.ContentProposalAdapter
;
67 import org
.eclipse
.jface
.fieldassist
.IContentProposal
;
68 import org
.eclipse
.jface
.fieldassist
.IContentProposalProvider
;
69 import org
.eclipse
.jface
.fieldassist
.TextContentAdapter
;
70 import org
.eclipse
.jface
.layout
.GridDataFactory
;
71 import org
.eclipse
.jface
.operation
.IRunnableWithProgress
;
72 import org
.eclipse
.jface
.resource
.JFaceResources
;
73 import org
.eclipse
.jface
.window
.Window
;
74 import org
.eclipse
.jface
.wizard
.IWizardContainer
;
75 import org
.eclipse
.jface
.wizard
.WizardPage
;
76 import org
.eclipse
.jgit
.lib
.Constants
;
77 import org
.eclipse
.jgit
.lib
.PersonIdent
;
78 import org
.eclipse
.jgit
.lib
.Ref
;
79 import org
.eclipse
.jgit
.lib
.Repository
;
80 import org
.eclipse
.jgit
.lib
.TagBuilder
;
81 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
82 import org
.eclipse
.jgit
.revwalk
.RevWalk
;
83 import org
.eclipse
.jgit
.transport
.FetchResult
;
84 import org
.eclipse
.jgit
.transport
.RefSpec
;
85 import org
.eclipse
.jgit
.transport
.RemoteConfig
;
86 import org
.eclipse
.jgit
.transport
.URIish
;
87 import org
.eclipse
.osgi
.util
.NLS
;
88 import org
.eclipse
.swt
.SWT
;
89 import org
.eclipse
.swt
.SWTException
;
90 import org
.eclipse
.swt
.dnd
.Clipboard
;
91 import org
.eclipse
.swt
.dnd
.TextTransfer
;
92 import org
.eclipse
.swt
.dnd
.Transfer
;
93 import org
.eclipse
.swt
.events
.KeyAdapter
;
94 import org
.eclipse
.swt
.events
.KeyEvent
;
95 import org
.eclipse
.swt
.events
.ModifyEvent
;
96 import org
.eclipse
.swt
.events
.ModifyListener
;
97 import org
.eclipse
.swt
.events
.SelectionAdapter
;
98 import org
.eclipse
.swt
.events
.SelectionEvent
;
99 import org
.eclipse
.swt
.layout
.GridData
;
100 import org
.eclipse
.swt
.layout
.GridLayout
;
101 import org
.eclipse
.swt
.widgets
.Button
;
102 import org
.eclipse
.swt
.widgets
.Combo
;
103 import org
.eclipse
.swt
.widgets
.Composite
;
104 import org
.eclipse
.swt
.widgets
.Control
;
105 import org
.eclipse
.swt
.widgets
.Group
;
106 import org
.eclipse
.swt
.widgets
.Label
;
107 import org
.eclipse
.swt
.widgets
.Text
;
108 import org
.eclipse
.ui
.IWorkbenchCommandConstants
;
109 import org
.eclipse
.ui
.PlatformUI
;
110 import org
.eclipse
.ui
.actions
.ActionFactory
;
111 import org
.eclipse
.ui
.progress
.WorkbenchJob
;
114 * Fetch a change from Gerrit
116 public class FetchGerritChangePage
extends WizardPage
{
118 private static final String GERRIT_CHANGE_REF_PREFIX
= "refs/changes/"; //$NON-NLS-1$
120 private static final Pattern GERRIT_FETCH_PATTERN
= Pattern
.compile(
121 "git fetch (\\w+:\\S+) (refs/changes/\\d+/\\d+/\\d+) && git (\\w+) FETCH_HEAD"); //$NON-NLS-1$
123 private static final Pattern GERRIT_URL_PATTERN
= Pattern
.compile(
124 "(?:https?://\\S+?/|/)?([1-9][0-9]*)(?:/([1-9][0-9]*)(?:/([1-9][0-9]*)(?:\\.\\.\\d+)?)?)?(?:/\\S*)?"); //$NON-NLS-1$
126 private static final Pattern GERRIT_CHANGE_REF_PATTERN
= Pattern
127 .compile("refs/changes/\\d+/(\\d+)(?:/(\\d+))?"); //$NON-NLS-1$
129 private enum CheckoutMode
{
130 CREATE_BRANCH
, CREATE_TAG
, CHECKOUT_FETCH_HEAD
, NOCHECKOUT
133 private final Repository repository
;
135 private final IDialogSettings settings
;
137 private final String lastUriKey
;
139 private Combo uriCombo
;
141 private Map
<String
, ChangeList
> changeRefs
= new HashMap
<>();
143 private Text refText
;
145 private Button createBranch
;
147 private Button createTag
;
149 private Button checkoutFetchHead
;
151 private Button updateFetchHead
;
153 private Label tagTextlabel
;
155 private Text tagText
;
157 private Label branchTextlabel
;
159 private Text branchText
;
161 private String refName
;
163 private Composite warningAdditionalRefNotActive
;
165 private Button activateAdditionalRefs
;
167 private IInputValidator branchValidator
;
169 private IInputValidator tagValidator
;
171 private Button branchEditButton
;
173 private Button branchCheckoutButton
;
175 private ExplicitContentProposalAdapter contentProposer
;
177 private boolean branchTextEdited
;
179 private boolean tagTextEdited
;
183 * @param refName initial value for the ref field
185 public FetchGerritChangePage(Repository repository
, String refName
) {
186 super(FetchGerritChangePage
.class.getName());
187 this.repository
= repository
;
188 this.refName
= refName
;
190 .bind(UIText
.FetchGerritChangePage_PageTitle
,
191 Activator
.getDefault().getRepositoryUtil()
192 .getRepositoryName(repository
)));
193 setMessage(UIText
.FetchGerritChangePage_PageMessage
);
194 settings
= getDialogSettings();
195 lastUriKey
= repository
+ GerritDialogSettings
.LAST_URI_SUFFIX
;
197 branchValidator
= ValidationUtils
.getRefNameInputValidator(repository
,
198 Constants
.R_HEADS
, true);
199 tagValidator
= ValidationUtils
.getRefNameInputValidator(repository
,
200 Constants
.R_TAGS
, true);
204 protected IDialogSettings
getDialogSettings() {
205 return GerritDialogSettings
206 .getSection(GerritDialogSettings
.FETCH_FROM_GERRIT_SECTION
);
210 public void createControl(Composite parent
) {
211 parent
.addDisposeListener(event
-> {
212 for (ChangeList l
: changeRefs
.values()) {
213 l
.cancel(ChangeList
.CancelMode
.INTERRUPT
);
217 Clipboard clipboard
= new Clipboard(parent
.getDisplay());
218 String clipText
= (String
) clipboard
.getContents(TextTransfer
221 String defaultUri
= null;
222 String defaultCommand
= null;
223 String defaultChange
= null;
224 Change candidateChange
= null;
225 if (clipText
!= null) {
226 Matcher matcher
= GERRIT_FETCH_PATTERN
.matcher(clipText
);
227 if (matcher
.matches()) {
228 defaultUri
= matcher
.group(1);
229 defaultChange
= matcher
.group(2);
230 defaultCommand
= matcher
.group(3);
232 candidateChange
= determineChangeFromString(clipText
.trim());
235 Composite main
= new Composite(parent
, SWT
.NONE
);
236 main
.setLayout(new GridLayout(2, false));
237 GridDataFactory
.fillDefaults().grab(true, true).applyTo(main
);
238 new Label(main
, SWT
.NONE
)
239 .setText(UIText
.FetchGerritChangePage_UriLabel
);
240 uriCombo
= new Combo(main
, SWT
.DROP_DOWN
);
241 GridDataFactory
.fillDefaults().grab(true, false).applyTo(uriCombo
);
242 uriCombo
.addSelectionListener(new SelectionAdapter() {
244 public void widgetSelected(SelectionEvent e
) {
245 String uriText
= uriCombo
.getText();
246 ChangeList list
= changeRefs
.get(uriText
);
248 list
.cancel(ChangeList
.CancelMode
.INTERRUPT
);
250 list
= new ChangeList(repository
, uriText
);
251 changeRefs
.put(uriText
, list
);
255 new Label(main
, SWT
.NONE
)
256 .setText(UIText
.FetchGerritChangePage_ChangeLabel
);
257 refText
= new Text(main
, SWT
.SINGLE
| SWT
.BORDER
);
258 GridDataFactory
.fillDefaults().grab(true, false).applyTo(refText
);
259 contentProposer
= addRefContentProposalToText(refText
);
260 refText
.addVerifyListener(event
-> {
261 event
.text
= event
.text
262 // C.f. https://bugs.eclipse.org/bugs/show_bug.cgi?id=273470
263 .replaceAll("\\v", " ") //$NON-NLS-1$ //$NON-NLS-2$
267 final Group checkoutGroup
= new Group(main
, SWT
.SHADOW_ETCHED_IN
);
268 checkoutGroup
.setLayout(new GridLayout(3, false));
269 GridDataFactory
.fillDefaults().span(3, 1).grab(true, false)
270 .applyTo(checkoutGroup
);
271 checkoutGroup
.setText(UIText
.FetchGerritChangePage_AfterFetchGroup
);
273 // radio: create local branch
274 createBranch
= new Button(checkoutGroup
, SWT
.RADIO
);
275 GridDataFactory
.fillDefaults().span(1, 1).applyTo(createBranch
);
276 createBranch
.setText(UIText
.FetchGerritChangePage_LocalBranchRadio
);
277 createBranch
.addSelectionListener(new SelectionAdapter() {
279 public void widgetSelected(SelectionEvent e
) {
284 branchCheckoutButton
= new Button(checkoutGroup
, SWT
.CHECK
);
285 GridDataFactory
.fillDefaults().span(2, 1).align(SWT
.END
, SWT
.CENTER
)
286 .applyTo(branchCheckoutButton
);
287 branchCheckoutButton
.setFont(JFaceResources
.getDialogFont());
289 .setText(UIText
.FetchGerritChangePage_LocalBranchCheckout
);
290 branchCheckoutButton
.setSelection(true);
292 branchTextlabel
= new Label(checkoutGroup
, SWT
.NONE
);
293 GridDataFactory
.defaultsFor(branchTextlabel
).exclude(false)
294 .applyTo(branchTextlabel
);
295 branchTextlabel
.setText(UIText
.FetchGerritChangePage_BranchNameText
);
296 branchText
= new Text(checkoutGroup
, SWT
.SINGLE
| SWT
.BORDER
);
297 GridDataFactory
.fillDefaults().grab(true, false)
298 .align(SWT
.FILL
, SWT
.CENTER
).applyTo(branchText
);
299 branchText
.addKeyListener(new KeyAdapter() {
302 public void keyPressed(KeyEvent e
) {
303 branchTextEdited
= true;
306 branchText
.addVerifyListener(event
-> {
307 if (event
.text
.isEmpty()) {
308 branchTextEdited
= false;
311 branchText
.addModifyListener(new ModifyListener() {
313 public void modifyText(ModifyEvent e
) {
317 BranchNameNormalizer normalizer
= new BranchNameNormalizer(branchText
);
318 normalizer
.setVisible(false);
319 branchEditButton
= new Button(checkoutGroup
, SWT
.PUSH
);
320 branchEditButton
.setFont(JFaceResources
.getDialogFont());
321 branchEditButton
.setText(UIText
.FetchGerritChangePage_BranchEditButton
);
322 branchEditButton
.addSelectionListener(new SelectionAdapter() {
324 public void widgetSelected(SelectionEvent selectionEvent
) {
325 String txt
= branchText
.getText();
326 String refToMark
= "".equals(txt
) ?
null : Constants
.R_HEADS
+ txt
; //$NON-NLS-1$
327 AbstractBranchSelectionDialog dlg
= new BranchEditDialog(
328 checkoutGroup
.getShell(), repository
, refToMark
);
329 if (dlg
.open() == Window
.OK
) {
330 branchText
.setText(Repository
.shortenRefName(dlg
332 branchTextEdited
= true;
334 // force calling branchText's modify listeners
335 branchText
.setText(branchText
.getText());
339 GridDataFactory
.defaultsFor(branchEditButton
).exclude(false)
340 .applyTo(branchEditButton
);
343 createTag
= new Button(checkoutGroup
, SWT
.RADIO
);
344 GridDataFactory
.fillDefaults().span(3, 1).applyTo(createTag
);
345 createTag
.setText(UIText
.FetchGerritChangePage_TagRadio
);
346 createTag
.addSelectionListener(new SelectionAdapter() {
348 public void widgetSelected(SelectionEvent e
) {
353 tagTextlabel
= new Label(checkoutGroup
, SWT
.NONE
);
354 GridDataFactory
.defaultsFor(tagTextlabel
).exclude(true)
355 .applyTo(tagTextlabel
);
356 tagTextlabel
.setText(UIText
.FetchGerritChangePage_TagNameText
);
357 tagText
= new Text(checkoutGroup
, SWT
.SINGLE
| SWT
.BORDER
);
358 GridDataFactory
.fillDefaults().exclude(true).grab(true, false)
360 tagText
.addKeyListener(new KeyAdapter() {
363 public void keyPressed(KeyEvent e
) {
364 tagTextEdited
= true;
367 tagText
.addVerifyListener(event
-> {
368 if (event
.text
.isEmpty()) {
369 tagTextEdited
= false;
372 tagText
.addModifyListener(new ModifyListener() {
374 public void modifyText(ModifyEvent e
) {
378 BranchNameNormalizer tagNormalizer
= new BranchNameNormalizer(tagText
,
379 UIText
.BranchNameNormalizer_TooltipForTag
);
380 tagNormalizer
.setVisible(false);
382 // radio: checkout FETCH_HEAD
383 checkoutFetchHead
= new Button(checkoutGroup
, SWT
.RADIO
);
384 GridDataFactory
.fillDefaults().span(3, 1).applyTo(checkoutFetchHead
);
385 checkoutFetchHead
.setText(UIText
.FetchGerritChangePage_CheckoutRadio
);
386 checkoutFetchHead
.addSelectionListener(new SelectionAdapter() {
388 public void widgetSelected(SelectionEvent e
) {
393 // radio: don't checkout
394 updateFetchHead
= new Button(checkoutGroup
, SWT
.RADIO
);
395 GridDataFactory
.fillDefaults().span(3, 1).applyTo(updateFetchHead
);
396 updateFetchHead
.setText(UIText
.FetchGerritChangePage_UpdateRadio
);
397 updateFetchHead
.addSelectionListener(new SelectionAdapter() {
399 public void widgetSelected(SelectionEvent e
) {
404 if ("checkout".equals(defaultCommand
)) { //$NON-NLS-1$
405 checkoutFetchHead
.setSelection(true);
407 createBranch
.setSelection(true);
410 warningAdditionalRefNotActive
= new Composite(main
, SWT
.NONE
);
411 GridDataFactory
.fillDefaults().span(2, 1).grab(true, false)
412 .exclude(true).applyTo(warningAdditionalRefNotActive
);
413 warningAdditionalRefNotActive
.setLayout(new GridLayout(2, false));
414 warningAdditionalRefNotActive
.setVisible(false);
416 activateAdditionalRefs
= new Button(warningAdditionalRefNotActive
,
418 activateAdditionalRefs
419 .setText(UIText
.FetchGerritChangePage_ActivateAdditionalRefsButton
);
420 activateAdditionalRefs
422 UIText
.FetchGerritChangePage_ActivateAdditionalRefsTooltip
);
424 ActionUtils
.setGlobalActions(refText
, ActionUtils
.createGlobalAction(
425 ActionFactory
.PASTE
, () -> doPaste(refText
)));
426 refText
.addModifyListener(new ModifyListener() {
428 public void modifyText(ModifyEvent e
) {
429 Change change
= Change
.fromRef(refText
.getText());
430 String suggestion
= ""; //$NON-NLS-1$
431 if (change
!= null) {
432 suggestion
= NLS
.bind(
433 UIText
.FetchGerritChangePage_SuggestedRefNamePattern
,
434 change
.getChangeNumber(),
435 change
.getPatchSetNumber());
437 if (!branchTextEdited
) {
438 branchText
.setText(suggestion
);
440 if (!tagTextEdited
) {
441 tagText
.setText(suggestion
);
446 if (defaultChange
!= null) {
447 refText
.setText(defaultChange
);
448 } else if (candidateChange
!= null) {
449 String ref
= candidateChange
.getRefName();
451 refText
.setText(ref
);
453 refText
.setText(candidateChange
.getChangeNumber().toString());
457 // get all available Gerrit URIs from the repository
458 SortedSet
<String
> uris
= new TreeSet
<>();
460 for (RemoteConfig rc
: RemoteConfig
.getAllRemoteConfigs(repository
462 if (GerritUtil
.isGerritFetch(rc
)) {
463 if (rc
.getURIs().size() > 0) {
464 uris
.add(rc
.getURIs().get(0).toPrivateString());
466 for (URIish u
: rc
.getPushURIs()) {
467 uris
.add(u
.toPrivateString());
472 } catch (URISyntaxException e
) {
473 Activator
.handleError(e
.getMessage(), e
, false);
474 setErrorMessage(e
.getMessage());
476 for (String aUri
: uris
) {
478 changeRefs
.put(aUri
, new ChangeList(repository
, aUri
));
480 if (defaultUri
!= null) {
481 uriCombo
.setText(defaultUri
);
485 String currentUri
= uriCombo
.getText();
486 ChangeList list
= changeRefs
.get(currentUri
);
488 list
= new ChangeList(repository
, currentUri
);
489 changeRefs
.put(currentUri
, list
);
493 Dialog
.applyDialogFont(main
);
495 if (candidateChange
!= null) {
496 // Launch content assist when the page is displayed
497 final IWizardContainer container
= getContainer();
498 if (container
instanceof IPageChangeProvider
) {
499 ((IPageChangeProvider
) container
)
500 .addPageChangedListener(new IPageChangedListener() {
502 public void pageChanged(PageChangedEvent event
) {
504 .getSelectedPage() == FetchGerritChangePage
.this) {
505 // Only the first time: remove myself
506 event
.getPageChangeProvider()
507 .removePageChangedListener(this);
508 getControl().getDisplay()
509 .asyncExec(new Runnable() {
512 Control control
= getControl();
514 && !control
.isDisposed()) {
516 .openProposalPopup();
528 private void preFetch(ChangeList list
) {
531 } catch (InvocationTargetException e
) {
532 Activator
.handleError(e
.getLocalizedMessage(), e
.getCause(), true);
537 * Tries to determine a Gerrit change number from an input string.
540 * string to derive a change number from
541 * @return the change number and possibly also the patch set number, or
542 * {@code null} if none could be determined.
544 protected static Change
determineChangeFromString(String input
) {
549 Matcher matcher
= GERRIT_URL_PATTERN
.matcher(input
);
550 if (matcher
.matches()) {
551 String first
= matcher
.group(1);
552 String second
= matcher
.group(2);
553 String third
= matcher
.group(3);
554 if (second
!= null && !second
.isEmpty()) {
555 if (third
!= null && !third
.isEmpty()) {
556 return Change
.create(Integer
.parseInt(second
),
557 Integer
.parseInt(third
));
558 } else if (input
.startsWith("http")) { //$NON-NLS-1$
559 // A URL ending with two digits: take the first as
562 return Change
.create(Integer
.parseInt(first
),
563 Integer
.parseInt(second
));
565 // Take the numerically larger. Might be a fragment like
566 // /10/65510 as in refs/changes/10/65510/6, or /65510/6
567 // as in https://git.eclipse.org/r/#/c/65510/6. This is
568 // a heuristic, it might go wrong on a Gerrit where
569 // there are not many changes (yet), and one of them has
571 int firstNum
= Integer
.parseInt(first
);
572 int secondNum
= Integer
.parseInt(second
);
573 if (firstNum
> secondNum
) {
574 return Change
.create(firstNum
, secondNum
);
576 return Change
.create(secondNum
);
580 return Change
.create(Integer
.parseInt(first
));
583 matcher
= GERRIT_CHANGE_REF_PATTERN
.matcher(input
);
584 if (matcher
.matches()) {
585 int firstNum
= Integer
.parseInt(matcher
.group(1));
586 int secondNum
= Integer
.parseInt(matcher
.group(2));
587 return Change
.create(firstNum
, secondNum
);
589 } catch (NumberFormatException e
) {
590 // Numerical overflow?
595 private void doPaste(Text text
) {
596 Clipboard clipboard
= new Clipboard(text
.getDisplay());
598 String clipText
= (String
) clipboard
599 .getContents(TextTransfer
.getInstance());
600 if (clipText
!= null) {
601 Change input
= determineChangeFromString(
604 String toInsert
= input
.getChangeNumber().toString();
605 if (input
.getPatchSetNumber() != null) {
606 if (text
.getText().trim().isEmpty() || text
607 .getSelectionText().equals(text
.getText())) {
608 // Paste will replace everything
609 toInsert
= input
.getRefName();
611 toInsert
= toInsert
+ '/'
612 + input
.getPatchSetNumber();
615 clipboard
.setContents(new Object
[] { toInsert
},
616 new Transfer
[] { TextTransfer
.getInstance() });
620 clipboard
.setContents(new Object
[] { clipText
},
621 new Transfer
[] { TextTransfer
.getInstance() });
632 private void storeLastUsedUri(String uri
) {
633 settings
.put(lastUriKey
, uri
.trim());
636 private void selectLastUsedUri() {
637 String lastUri
= settings
.get(lastUriKey
);
638 if (lastUri
!= null) {
639 int i
= uriCombo
.indexOf(lastUri
);
649 public void setVisible(boolean visible
) {
650 super.setVisible(visible
);
651 if (visible
&& refName
!= null)
652 refText
.setText(refName
);
655 private void checkPage() {
656 boolean createBranchSelected
= createBranch
.getSelection();
657 branchText
.setEnabled(createBranchSelected
);
658 branchText
.setVisible(createBranchSelected
);
659 branchTextlabel
.setVisible(createBranchSelected
);
660 branchEditButton
.setVisible(createBranchSelected
);
661 branchCheckoutButton
.setVisible(createBranchSelected
);
662 GridData gd
= (GridData
) branchText
.getLayoutData();
663 gd
.exclude
= !createBranchSelected
;
664 gd
= (GridData
) branchTextlabel
.getLayoutData();
665 gd
.exclude
= !createBranchSelected
;
666 gd
= (GridData
) branchEditButton
.getLayoutData();
667 gd
.exclude
= !createBranchSelected
;
668 gd
= (GridData
) branchCheckoutButton
.getLayoutData();
669 gd
.exclude
= !createBranchSelected
;
671 boolean createTagSelected
= createTag
.getSelection();
672 tagText
.setEnabled(createTagSelected
);
673 tagText
.setVisible(createTagSelected
);
674 tagTextlabel
.setVisible(createTagSelected
);
675 gd
= (GridData
) tagText
.getLayoutData();
676 gd
.exclude
= !createTagSelected
;
677 gd
= (GridData
) tagTextlabel
.getLayoutData();
678 gd
.exclude
= !createTagSelected
;
679 branchText
.getParent().layout(true);
681 boolean showActivateAdditionalRefs
= false;
682 showActivateAdditionalRefs
= (checkoutFetchHead
.getSelection() || updateFetchHead
686 .getPreferenceStore()
688 UIPreferences
.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS
);
690 gd
= (GridData
) warningAdditionalRefNotActive
.getLayoutData();
691 gd
.exclude
= !showActivateAdditionalRefs
;
692 warningAdditionalRefNotActive
.setVisible(showActivateAdditionalRefs
);
693 warningAdditionalRefNotActive
.getParent().layout(true);
695 setErrorMessage(null);
697 if (refText
.getText().length() > 0) {
698 Change change
= Change
.fromRef(refText
.getText());
699 if (change
== null) {
700 setErrorMessage(UIText
.FetchGerritChangePage_MissingChangeMessage
);
703 ChangeList list
= changeRefs
.get(uriCombo
.getText());
704 if (list
!= null && list
.isDone()
705 && !list
.getResult().contains(change
)) {
707 UIText
.FetchGerritChangePage_UnknownChangeRefMessage
);
711 setErrorMessage(UIText
.FetchGerritChangePage_MissingChangeMessage
);
715 if (createBranchSelected
) {
716 setErrorMessage(branchValidator
.isValid(branchText
.getText()));
717 } else if (createTagSelected
) {
718 setErrorMessage(tagValidator
.isValid(tagText
.getText()));
721 setPageComplete(getErrorMessage() == null);
725 private Collection
<Change
> getRefsForContentAssist()
726 throws InvocationTargetException
, InterruptedException
{
727 String uriText
= uriCombo
.getText();
728 if (!changeRefs
.containsKey(uriText
)) {
729 changeRefs
.put(uriText
, new ChangeList(repository
, uriText
));
731 ChangeList list
= changeRefs
.get(uriText
);
732 if (!list
.isFinished()) {
733 IWizardContainer container
= getContainer();
734 IRunnableWithProgress operation
= monitor
-> {
735 monitor
.beginTask(MessageFormat
.format(
736 UIText
.FetchGerritChangePage_FetchingRemoteRefsMessage
,
737 uriText
), IProgressMonitor
.UNKNOWN
);
738 Collection
<Change
> result
= list
.get();
739 if (monitor
.isCanceled()) {
742 // If we get here, the ChangeList future is done.
743 if (result
== null || result
.isEmpty()) {
744 // Don't bother if we didn't get any results
747 // If we do have results now, open the proposals.
748 Job showProposals
= new WorkbenchJob(
749 UIText
.FetchGerritChangePage_ShowingProposalsJobName
) {
752 public IStatus
runInUIThread(IProgressMonitor uiMonitor
) {
753 // But only if we're not disposed, the focus is still
754 // (or again) in the Change field, and the uri is still
757 if (container
instanceof NonBlockingWizardDialog
) {
758 // Otherwise the dialog was blocked anyway, and
759 // focus will be restored
760 if (refText
!= refText
.getDisplay()
761 .getFocusControl()) {
762 return Status
.CANCEL_STATUS
;
764 String uriNow
= uriCombo
.getText();
765 if (!uriNow
.equals(uriText
)) {
766 return Status
.CANCEL_STATUS
;
769 contentProposer
.openProposalPopup();
770 } catch (SWTException e
) {
772 return Status
.CANCEL_STATUS
;
776 return Status
.OK_STATUS
;
780 showProposals
.schedule();
782 if (container
instanceof NonBlockingWizardDialog
) {
783 NonBlockingWizardDialog dialog
= (NonBlockingWizardDialog
) container
;
784 dialog
.run(operation
,
785 () -> list
.cancel(ChangeList
.CancelMode
.ABANDON
));
787 container
.run(true, true, operation
);
795 final RefSpec spec
= new RefSpec().setSource(refText
.getText())
796 .setDestination(Constants
.FETCH_HEAD
);
797 final String uri
= uriCombo
.getText();
798 final CheckoutMode mode
= getCheckoutMode();
799 final boolean doCheckoutNewBranch
= (mode
== CheckoutMode
.CREATE_BRANCH
)
800 && branchCheckoutButton
.getSelection();
801 final boolean doActivateAdditionalRefs
= showAdditionalRefs();
802 final String textForTag
= tagText
.getText();
803 final String textForBranch
= branchText
.getText();
805 Job job
= new WorkspaceJob(
806 UIText
.FetchGerritChangePage_GetChangeTaskName
) {
809 public IStatus
runInWorkspace(IProgressMonitor monitor
) {
811 SubMonitor progress
= SubMonitor
.convert(monitor
,
812 UIText
.FetchGerritChangePage_GetChangeTaskName
,
814 RevCommit commit
= fetchChange(uri
, spec
,
815 progress
.newChild(1));
817 case CHECKOUT_FETCH_HEAD
:
818 checkout(commit
.name(), progress
.newChild(1));
821 createTag(spec
, textForTag
, commit
,
822 progress
.newChild(1));
823 checkout(commit
.name(), progress
.newChild(1));
826 createBranch(textForBranch
, doCheckoutNewBranch
, commit
,
827 progress
.newChild(1));
832 if (doActivateAdditionalRefs
) {
833 activateAdditionalRefs();
835 if (mode
== CheckoutMode
.NOCHECKOUT
) {
836 // Tell the world that FETCH_HEAD only changed. In other
837 // cases, JGit will have sent a RefsChangeEvent
839 repository
.fireEvent(new FetchHeadChangedEvent());
841 storeLastUsedUri(uri
);
842 } catch (CoreException ce
) {
843 return ce
.getStatus();
844 } catch (Exception e
) {
845 return Activator
.createErrorStatus(e
.getLocalizedMessage(),
850 return Status
.OK_STATUS
;
853 private int getTotalWork(final CheckoutMode m
) {
855 case CHECKOUT_FETCH_HEAD
:
866 public boolean belongsTo(Object family
) {
867 if (JobFamilies
.FETCH
.equals(family
))
869 return super.belongsTo(family
);
877 private boolean showAdditionalRefs() {
878 return (checkoutFetchHead
.getSelection()
879 || updateFetchHead
.getSelection())
880 && activateAdditionalRefs
.getSelection();
883 private CheckoutMode
getCheckoutMode() {
884 if (createBranch
.getSelection()) {
885 return CheckoutMode
.CREATE_BRANCH
;
886 } else if (createTag
.getSelection()) {
887 return CheckoutMode
.CREATE_TAG
;
888 } else if (checkoutFetchHead
.getSelection()) {
889 return CheckoutMode
.CHECKOUT_FETCH_HEAD
;
891 return CheckoutMode
.NOCHECKOUT
;
895 private RevCommit
fetchChange(String uri
, RefSpec spec
,
896 IProgressMonitor monitor
) throws CoreException
, URISyntaxException
,
898 int timeout
= Activator
.getDefault().getPreferenceStore()
899 .getInt(UIPreferences
.REMOTE_CONNECTION_TIMEOUT
);
901 List
<RefSpec
> specs
= new ArrayList
<>(1);
904 String taskName
= NLS
905 .bind(UIText
.FetchGerritChangePage_FetchingTaskName
,
907 monitor
.subTask(taskName
);
908 FetchResult fetchRes
= new FetchOperationUI(repository
,
909 new URIish(uri
), specs
, timeout
, false).execute(monitor
);
912 try (RevWalk rw
= new RevWalk(repository
)) {
913 return rw
.parseCommit(
914 fetchRes
.getAdvertisedRef(spec
.getSource()).getObjectId());
918 private void createTag(final RefSpec spec
, final String textForTag
,
919 RevCommit commit
, IProgressMonitor monitor
) throws CoreException
{
920 monitor
.subTask(UIText
.FetchGerritChangePage_CreatingTagTaskName
);
921 final TagBuilder tag
= new TagBuilder();
922 PersonIdent personIdent
= new PersonIdent(repository
);
924 tag
.setTag(textForTag
);
925 tag
.setTagger(personIdent
);
926 tag
.setMessage(NLS
.bind(
927 UIText
.FetchGerritChangePage_GeneratedTagMessage
,
929 tag
.setObjectId(commit
);
930 new TagOperation(repository
, tag
, false).execute(monitor
);
934 private void createBranch(final String textForBranch
, boolean doCheckout
,
935 RevCommit commit
, IProgressMonitor monitor
) throws CoreException
{
936 SubMonitor progress
= SubMonitor
.convert(monitor
, doCheckout ?
10 : 2);
937 progress
.subTask(UIText
.FetchGerritChangePage_CreatingBranchTaskName
);
938 CreateLocalBranchOperation bop
= new CreateLocalBranchOperation(
939 repository
, textForBranch
, commit
);
940 bop
.execute(progress
.newChild(2));
942 checkout(textForBranch
, progress
.newChild(8));
946 private void checkout(String targetName
, IProgressMonitor monitor
)
947 throws CoreException
{
948 monitor
.subTask(UIText
.FetchGerritChangePage_CheckingOutTaskName
);
949 BranchOperationUI
.checkout(repository
, targetName
).run(monitor
);
953 private void activateAdditionalRefs() {
954 // do this in the UI thread as it results in a
955 // refresh() on the history page
956 PlatformUI
.getWorkbench().getDisplay().asyncExec(new Runnable() {
961 .getPreferenceStore()
963 UIPreferences
.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS
,
969 private ExplicitContentProposalAdapter
addRefContentProposalToText(
970 final Text textField
) {
971 KeyStroke stroke
= UIUtils
972 .getKeystrokeOfBestActiveBindingFor(IWorkbenchCommandConstants
.EDIT_CONTENT_ASSIST
);
973 if (stroke
!= null) {
974 UIUtils
.addBulbDecorator(textField
, NLS
.bind(
975 UIText
.FetchGerritChangePage_ContentAssistTooltip
,
978 IContentProposalProvider cp
= new IContentProposalProvider() {
981 public IContentProposal
[] getProposals(String contents
, int position
) {
982 Collection
<Change
> proposals
;
984 proposals
= getRefsForContentAssist();
985 } catch (InvocationTargetException e
) {
986 Activator
.handleError(e
.getMessage(), e
, true);
988 } catch (InterruptedException e
) {
992 if (proposals
== null) {
995 List
<IContentProposal
> resultList
= new ArrayList
<>();
996 String input
= contents
;
997 Matcher matcher
= GERRIT_CHANGE_REF_PATTERN
.matcher(contents
);
998 if (matcher
.find()) {
999 input
= matcher
.group(1);
1001 Pattern pattern
= UIUtils
.createProposalPattern(input
);
1002 for (final Change ref
: proposals
) {
1003 if (pattern
!= null && !pattern
1004 .matcher(ref
.getChangeNumber().toString())
1008 resultList
.add(new ChangeContentProposal(ref
));
1011 .toArray(new IContentProposal
[resultList
.size()]);
1015 ExplicitContentProposalAdapter adapter
= new ExplicitContentProposalAdapter(
1016 textField
, cp
, stroke
);
1017 // set the acceptance style to always replace the complete content
1018 adapter
.setProposalAcceptanceStyle(ContentProposalAdapter
.PROPOSAL_REPLACE
);
1022 private static class ExplicitContentProposalAdapter
1023 extends ContentProposalAdapter
{
1025 public ExplicitContentProposalAdapter(Control control
,
1026 IContentProposalProvider proposalProvider
,
1027 KeyStroke keyStroke
) {
1028 super(control
, new TextContentAdapter(), proposalProvider
,
1033 public void openProposalPopup() {
1034 // Make this method accessible
1035 super.openProposalPopup();
1039 final static class Change
implements Comparable
<Change
> {
1040 private final String refName
;
1042 private final Integer changeNumber
;
1044 private final Integer patchSetNumber
;
1046 static Change
fromRef(String refName
) {
1049 || !refName
.startsWith(GERRIT_CHANGE_REF_PREFIX
)) {
1052 String
[] tokens
= refName
1053 .substring(GERRIT_CHANGE_REF_PREFIX
.length())
1054 .split("/"); //$NON-NLS-1$
1055 if (tokens
.length
!= 3) {
1058 Integer subdir
= Integer
.valueOf(tokens
[0]);
1059 Integer changeNumber
= Integer
.valueOf(tokens
[1]);
1060 if (subdir
.intValue() != changeNumber
.intValue() % 100) {
1063 Integer patchSetNumber
= Integer
.valueOf(tokens
[2]);
1064 return new Change(refName
, changeNumber
, patchSetNumber
);
1065 } catch (NumberFormatException e
) {
1066 // if we can't parse this, just return null
1068 } catch (IndexOutOfBoundsException e
) {
1069 // if we can't parse this, just return null
1074 static Change
create(int changeNumber
) {
1075 return new Change(null, Integer
.valueOf(changeNumber
), null);
1078 static Change
create(int changeNumber
, int patchSetNumber
) {
1079 int subDir
= changeNumber
% 100;
1081 GERRIT_CHANGE_REF_PREFIX
+ subDir
+ '/' + changeNumber
+ '/'
1083 Integer
.valueOf(changeNumber
),
1084 Integer
.valueOf(patchSetNumber
));
1087 private Change(String refName
, Integer changeNumber
,
1088 Integer patchSetNumber
) {
1089 this.refName
= refName
;
1090 this.changeNumber
= changeNumber
;
1091 this.patchSetNumber
= patchSetNumber
;
1094 public String
getRefName() {
1098 public Integer
getChangeNumber() {
1099 return changeNumber
;
1102 public Integer
getPatchSetNumber() {
1103 return patchSetNumber
;
1107 public String
toString() {
1112 public boolean equals(Object obj
) {
1113 if (!(obj
instanceof Change
)) {
1116 return compareTo((Change
) obj
) == 0;
1120 public int hashCode() {
1121 return Objects
.hash(changeNumber
, patchSetNumber
);
1125 public int compareTo(Change o
) {
1126 int changeDiff
= this.changeNumber
.compareTo(o
.getChangeNumber());
1127 if (changeDiff
== 0) {
1128 if (patchSetNumber
== null) {
1129 return o
.getPatchSetNumber() != null ?
-1 : 0;
1130 } else if (o
.getPatchSetNumber() == null) {
1133 changeDiff
= this.patchSetNumber
1134 .compareTo(o
.getPatchSetNumber());
1140 private final static class ChangeContentProposal
implements
1142 private final Change myChange
;
1144 ChangeContentProposal(Change change
) {
1149 public String
getContent() {
1150 return myChange
.getRefName();
1154 public int getCursorPosition() {
1159 public String
getDescription() {
1161 UIText
.FetchGerritChangePage_ContentAssistDescription
,
1162 myChange
.getPatchSetNumber(), myChange
.getChangeNumber());
1166 public String
getLabel() {
1168 .bind("{0} - {1}", myChange
.getChangeNumber(), myChange
.getPatchSetNumber()); //$NON-NLS-1$
1172 * @see java.lang.Object#toString()
1175 public String
toString() {
1176 return getContent();
1181 * A {@code ChangeList} is a "Future", loading the list of change refs
1182 * asynchronously from the remote repository. The {@link ChangeList#get()
1183 * get()} method blocks until the result is available or the future is
1184 * canceled. Pre-fetching is possible by calling {@link ChangeList#fetch()}
1187 private static class ChangeList
{
1190 * Determines how to cancel a not-yet-completed future. Irrespective of
1191 * the mechanism, the job may actually terminate normally, and
1192 * subsequent calls to get() may return a result.
1194 public static enum CancelMode
{
1196 * Tries to cancel the job, which may decide to ignore the request.
1197 * Callers to get() will remain blocked until the job terminates.
1201 * Tries to cancel the job, which may decide to ignore the request.
1202 * Outstanding get() calls will be woken up and may throw
1203 * InterruptedException or return a result if the job terminated in
1208 * Tries to cancel the job, and if that doesn't succeed immediately,
1209 * interrupts the job's thread. Outstanding calls to get() will be
1210 * woken up and may throw InterruptedException or return a result if
1211 * the job terminated in the meantime.
1216 private static enum State
{
1217 PRISTINE
, SCHEDULED
, CANCELING
, INTERRUPT
, CANCELED
, DONE
1220 private final Repository repository
;
1222 private final String uriText
;
1224 private State state
= State
.PRISTINE
;
1226 private Set
<Change
> result
;
1228 private InterruptibleJob job
;
1230 public ChangeList(Repository repository
, String uriText
) {
1231 this.repository
= repository
;
1232 this.uriText
= uriText
;
1236 * Tries to cancel the future. {@code cancel(false)} tries a normal job
1237 * cancellation, which may or may not terminated the job (it may decide
1238 * not to react to cancellation requests).
1240 * @param cancellation
1241 * {@link CancelMode} defining how to cancel
1243 * @return {@code true} if the future was canceled (its job is not
1244 * running anymore), {@code false} otherwise.
1246 public synchronized boolean cancel(CancelMode cancellation
) {
1247 CancelMode mode
= cancellation
== null ? CancelMode
.CANCEL
1254 state
= State
.CANCELING
;
1255 boolean canceled
= job
.cancel();
1257 state
= State
.CANCELED
;
1258 } else if (mode
== CancelMode
.INTERRUPT
) {
1260 } else if (mode
== CancelMode
.ABANDON
) {
1265 // cancel(CANCEL|ABANDON) was called before.
1266 if (mode
== CancelMode
.INTERRUPT
) {
1268 } else if (mode
== CancelMode
.ABANDON
) {
1273 if (mode
!= CancelMode
.CANCEL
) {
1284 public synchronized boolean isFinished() {
1285 return state
== State
.CANCELED
|| state
== State
.DONE
;
1288 public synchronized boolean isDone() {
1289 return state
== State
.DONE
;
1293 * Retrieves the result. If the result is not yet available, the method
1294 * blocks until it is or {@link #cancel(CancelMode)} is called with
1295 * {@link CancelMode#ABANDON} or {@link CancelMode#INTERRUPT}.
1297 * @return the result, which may be {@code null} if the future was
1299 * @throws InterruptedException
1300 * if waiting was interrupted
1301 * @throws InvocationTargetException
1302 * if the future's job cannot be created
1304 public synchronized Collection
<Change
> get()
1305 throws InterruptedException
, InvocationTargetException
{
1315 if (state
== State
.CANCELING
|| state
== State
.INTERRUPT
) {
1316 // canceled with ABANDON or INTERRUPT
1317 throw new InterruptedException();
1323 public synchronized Collection
<Change
> getResult() {
1327 throw new IllegalStateException(
1328 "Fetching change list is not finished"); //$NON-NLS-1$
1331 private synchronized void finish(boolean done
) {
1332 state
= done ? State
.DONE
: State
.CANCELED
;
1334 notifyAll(); // We're done, wake up all outstanding get() calls
1337 private synchronized void interrupt() {
1338 state
= State
.INTERRUPT
;
1340 notifyAll(); // Abandon outstanding get() calls
1344 * On the first call, starts a background job to fetch the result.
1345 * Subsequent calls do nothing and return immediately.
1347 * @throws InvocationTargetException
1348 * if starting the job fails
1350 public synchronized void fetch() throws InvocationTargetException
{
1351 if (job
!= null || state
!= State
.PRISTINE
) {
1354 ListRemoteOperation listOp
;
1356 listOp
= new ListRemoteOperation(repository
,
1357 new URIish(uriText
),
1358 Activator
.getDefault().getPreferenceStore().getInt(
1359 UIPreferences
.REMOTE_CONNECTION_TIMEOUT
));
1360 } catch (URISyntaxException e
) {
1362 throw new InvocationTargetException(e
);
1364 job
= new InterruptibleJob(MessageFormat
.format(
1365 UIText
.FetchGerritChangePage_FetchingRemoteRefsMessage
,
1369 protected IStatus
run(IProgressMonitor monitor
) {
1371 listOp
.run(monitor
);
1372 } catch (InterruptedException e
) {
1373 return Status
.CANCEL_STATUS
;
1374 } catch (InvocationTargetException e
) {
1375 synchronized (ChangeList
.this) {
1376 if (state
== State
.CANCELING
1377 || state
== State
.INTERRUPT
) {
1378 // JGit may report a TransportException when the
1379 // thread is interrupted. Let's just pretend we
1380 // canceled before. Also, if the user canceled
1381 // already, he's not interested in errors
1383 return Status
.CANCEL_STATUS
;
1387 .createErrorStatus(e
.getLocalizedMessage(), e
);
1389 List
<Change
> changes
= new ArrayList
<>();
1390 for (Ref ref
: listOp
.getRemoteRefs()) {
1391 Change change
= Change
.fromRef(ref
.getName());
1392 if (change
!= null) {
1393 changes
.add(change
);
1396 Collections
.sort(changes
, Collections
.reverseOrder());
1397 result
= new LinkedHashSet
<>(changes
);
1398 return Status
.OK_STATUS
;
1402 job
.addJobChangeListener(new JobChangeAdapter() {
1405 public void done(IJobChangeEvent event
) {
1406 IStatus status
= event
.getResult();
1407 finish(status
!= null && status
.isOK());
1412 job
.setSystem(true);
1413 state
= State
.SCHEDULED
;
1417 private static abstract class InterruptibleJob
extends Job
{
1419 public InterruptibleJob(String name
) {
1423 public void interrupt() {
1424 Thread thread
= getThread();
1425 if (thread
!= null) {