FetchGerritChangePage: also try to determine the patch set number
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / fetch / FetchGerritChangePage.java
blob141e82fba1e198e3fe71b7fbc8e49dcb03292cb8
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
8 * Contributors:
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;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Set;
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;
182 * @param repository
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;
189 setTitle(NLS
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);
203 @Override
204 protected IDialogSettings getDialogSettings() {
205 return GerritDialogSettings
206 .getSection(GerritDialogSettings.FETCH_FROM_GERRIT_SECTION);
209 @Override
210 public void createControl(Composite parent) {
211 parent.addDisposeListener(event -> {
212 for (ChangeList l : changeRefs.values()) {
213 l.cancel(ChangeList.CancelMode.INTERRUPT);
215 changeRefs.clear();
217 Clipboard clipboard = new Clipboard(parent.getDisplay());
218 String clipText = (String) clipboard.getContents(TextTransfer
219 .getInstance());
220 clipboard.dispose();
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);
231 } else {
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() {
243 @Override
244 public void widgetSelected(SelectionEvent e) {
245 String uriText = uriCombo.getText();
246 ChangeList list = changeRefs.get(uriText);
247 if (list != null) {
248 list.cancel(ChangeList.CancelMode.INTERRUPT);
250 list = new ChangeList(repository, uriText);
251 changeRefs.put(uriText, list);
252 preFetch(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$
264 .trim();
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() {
278 @Override
279 public void widgetSelected(SelectionEvent e) {
280 checkPage();
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());
288 branchCheckoutButton
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() {
301 @Override
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() {
312 @Override
313 public void modifyText(ModifyEvent e) {
314 checkPage();
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() {
323 @Override
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
331 .getRefName()));
332 branchTextEdited = true;
333 } else {
334 // force calling branchText's modify listeners
335 branchText.setText(branchText.getText());
339 GridDataFactory.defaultsFor(branchEditButton).exclude(false)
340 .applyTo(branchEditButton);
342 // radio: create tag
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() {
347 @Override
348 public void widgetSelected(SelectionEvent e) {
349 checkPage();
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)
359 .applyTo(tagText);
360 tagText.addKeyListener(new KeyAdapter() {
362 @Override
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() {
373 @Override
374 public void modifyText(ModifyEvent e) {
375 checkPage();
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() {
387 @Override
388 public void widgetSelected(SelectionEvent e) {
389 checkPage();
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() {
398 @Override
399 public void widgetSelected(SelectionEvent e) {
400 checkPage();
404 if ("checkout".equals(defaultCommand)) { //$NON-NLS-1$
405 checkoutFetchHead.setSelection(true);
406 } else {
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,
417 SWT.CHECK);
418 activateAdditionalRefs
419 .setText(UIText.FetchGerritChangePage_ActivateAdditionalRefsButton);
420 activateAdditionalRefs
421 .setToolTipText(
422 UIText.FetchGerritChangePage_ActivateAdditionalRefsTooltip);
424 ActionUtils.setGlobalActions(refText, ActionUtils.createGlobalAction(
425 ActionFactory.PASTE, () -> doPaste(refText)));
426 refText.addModifyListener(new ModifyListener() {
427 @Override
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);
443 checkPage();
446 if (defaultChange != null) {
447 refText.setText(defaultChange);
448 } else if (candidateChange != null) {
449 String ref = candidateChange.getRefName();
450 if (ref != null) {
451 refText.setText(ref);
452 } else {
453 refText.setText(candidateChange.getChangeNumber().toString());
457 // get all available Gerrit URIs from the repository
458 SortedSet<String> uris = new TreeSet<>();
459 try {
460 for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(repository
461 .getConfig())) {
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) {
477 uriCombo.add(aUri);
478 changeRefs.put(aUri, new ChangeList(repository, aUri));
480 if (defaultUri != null) {
481 uriCombo.setText(defaultUri);
482 } else {
483 selectLastUsedUri();
485 String currentUri = uriCombo.getText();
486 ChangeList list = changeRefs.get(currentUri);
487 if (list == null) {
488 list = new ChangeList(repository, currentUri);
489 changeRefs.put(currentUri, list);
491 preFetch(list);
492 refText.setFocus();
493 Dialog.applyDialogFont(main);
494 setControl(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() {
501 @Override
502 public void pageChanged(PageChangedEvent event) {
503 if (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() {
510 @Override
511 public void run() {
512 Control control = getControl();
513 if (control != null
514 && !control.isDisposed()) {
515 contentProposer
516 .openProposalPopup();
525 checkPage();
528 private void preFetch(ChangeList list) {
529 try {
530 list.fetch();
531 } catch (InvocationTargetException e) {
532 Activator.handleError(e.getLocalizedMessage(), e.getCause(), true);
537 * Tries to determine a Gerrit change number from an input string.
539 * @param input
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) {
545 if (input == null) {
546 return null;
548 try {
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
560 // change
561 // number
562 return Change.create(Integer.parseInt(first),
563 Integer.parseInt(second));
564 } else {
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
570 // many patch sets.
571 int firstNum = Integer.parseInt(first);
572 int secondNum = Integer.parseInt(second);
573 if (firstNum > secondNum) {
574 return Change.create(firstNum, secondNum);
575 } else {
576 return Change.create(secondNum);
579 } else {
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?
592 return null;
595 private void doPaste(Text text) {
596 Clipboard clipboard = new Clipboard(text.getDisplay());
597 try {
598 String clipText = (String) clipboard
599 .getContents(TextTransfer.getInstance());
600 if (clipText != null) {
601 Change input = determineChangeFromString(
602 clipText.trim());
603 if (input != null) {
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();
610 } else {
611 toInsert = toInsert + '/'
612 + input.getPatchSetNumber();
615 clipboard.setContents(new Object[] { toInsert },
616 new Transfer[] { TextTransfer.getInstance() });
617 try {
618 text.paste();
619 } finally {
620 clipboard.setContents(new Object[] { clipText },
621 new Transfer[] { TextTransfer.getInstance() });
623 } else {
624 text.paste();
627 } finally {
628 clipboard.dispose();
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);
640 if (i != -1) {
641 uriCombo.select(i);
642 return;
645 uriCombo.select(0);
648 @Override
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
683 .getSelection())
684 && !Activator
685 .getDefault()
686 .getPreferenceStore()
687 .getBoolean(
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);
696 try {
697 if (refText.getText().length() > 0) {
698 Change change = Change.fromRef(refText.getText());
699 if (change == null) {
700 setErrorMessage(UIText.FetchGerritChangePage_MissingChangeMessage);
701 return;
703 ChangeList list = changeRefs.get(uriCombo.getText());
704 if (list != null && list.isDone()
705 && !list.getResult().contains(change)) {
706 setErrorMessage(
707 UIText.FetchGerritChangePage_UnknownChangeRefMessage);
708 return;
710 } else {
711 setErrorMessage(UIText.FetchGerritChangePage_MissingChangeMessage);
712 return;
715 if (createBranchSelected) {
716 setErrorMessage(branchValidator.isValid(branchText.getText()));
717 } else if (createTagSelected) {
718 setErrorMessage(tagValidator.isValid(tagText.getText()));
720 } finally {
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()) {
740 return;
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
745 return;
747 // If we do have results now, open the proposals.
748 Job showProposals = new WorkbenchJob(
749 UIText.FetchGerritChangePage_ShowingProposalsJobName) {
751 @Override
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
755 // the same
756 try {
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) {
771 // Disposed already
772 return Status.CANCEL_STATUS;
773 } finally {
774 uiMonitor.done();
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));
786 } else {
787 container.run(true, true, operation);
789 return null;
791 return list.get();
794 boolean doFetch() {
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) {
808 @Override
809 public IStatus runInWorkspace(IProgressMonitor monitor) {
810 try {
811 SubMonitor progress = SubMonitor.convert(monitor,
812 UIText.FetchGerritChangePage_GetChangeTaskName,
813 getTotalWork(mode));
814 RevCommit commit = fetchChange(uri, spec,
815 progress.newChild(1));
816 switch (mode) {
817 case CHECKOUT_FETCH_HEAD:
818 checkout(commit.name(), progress.newChild(1));
819 break;
820 case CREATE_TAG:
821 createTag(spec, textForTag, commit,
822 progress.newChild(1));
823 checkout(commit.name(), progress.newChild(1));
824 break;
825 case CREATE_BRANCH:
826 createBranch(textForBranch, doCheckoutNewBranch, commit,
827 progress.newChild(1));
828 break;
829 default:
830 break;
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
838 // already.
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(),
847 } finally {
848 monitor.done();
850 return Status.OK_STATUS;
853 private int getTotalWork(final CheckoutMode m) {
854 switch (m) {
855 case CHECKOUT_FETCH_HEAD:
856 case CREATE_BRANCH:
857 return 2;
858 case CREATE_TAG:
859 return 3;
860 default:
861 return 1;
865 @Override
866 public boolean belongsTo(Object family) {
867 if (JobFamilies.FETCH.equals(family))
868 return true;
869 return super.belongsTo(family);
872 job.setUser(true);
873 job.schedule();
874 return true;
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;
890 } else {
891 return CheckoutMode.NOCHECKOUT;
895 private RevCommit fetchChange(String uri, RefSpec spec,
896 IProgressMonitor monitor) throws CoreException, URISyntaxException,
897 IOException {
898 int timeout = Activator.getDefault().getPreferenceStore()
899 .getInt(UIPreferences.REMOTE_CONNECTION_TIMEOUT);
901 List<RefSpec> specs = new ArrayList<>(1);
902 specs.add(spec);
904 String taskName = NLS
905 .bind(UIText.FetchGerritChangePage_FetchingTaskName,
906 spec.getSource());
907 monitor.subTask(taskName);
908 FetchResult fetchRes = new FetchOperationUI(repository,
909 new URIish(uri), specs, timeout, false).execute(monitor);
911 monitor.worked(1);
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,
928 spec.getSource()));
929 tag.setObjectId(commit);
930 new TagOperation(repository, tag, false).execute(monitor);
931 monitor.worked(1);
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));
941 if (doCheckout) {
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);
950 monitor.worked(1);
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() {
957 @Override
958 public void run() {
959 Activator
960 .getDefault()
961 .getPreferenceStore()
962 .setValue(
963 UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS,
964 true);
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,
976 stroke.format()));
978 IContentProposalProvider cp = new IContentProposalProvider() {
980 @Override
981 public IContentProposal[] getProposals(String contents, int position) {
982 Collection<Change> proposals;
983 try {
984 proposals = getRefsForContentAssist();
985 } catch (InvocationTargetException e) {
986 Activator.handleError(e.getMessage(), e, true);
987 return null;
988 } catch (InterruptedException e) {
989 return null;
992 if (proposals == null) {
993 return 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())
1005 .matches()) {
1006 continue;
1008 resultList.add(new ChangeContentProposal(ref));
1010 return resultList
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);
1019 return adapter;
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,
1029 keyStroke, null);
1032 @Override
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) {
1047 try {
1048 if (refName == null
1049 || !refName.startsWith(GERRIT_CHANGE_REF_PREFIX)) {
1050 return null;
1052 String[] tokens = refName
1053 .substring(GERRIT_CHANGE_REF_PREFIX.length())
1054 .split("/"); //$NON-NLS-1$
1055 if (tokens.length != 3) {
1056 return null;
1058 Integer subdir = Integer.valueOf(tokens[0]);
1059 Integer changeNumber = Integer.valueOf(tokens[1]);
1060 if (subdir.intValue() != changeNumber.intValue() % 100) {
1061 return null;
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
1067 return null;
1068 } catch (IndexOutOfBoundsException e) {
1069 // if we can't parse this, just return null
1070 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;
1080 return new Change(
1081 GERRIT_CHANGE_REF_PREFIX + subDir + '/' + changeNumber + '/'
1082 + patchSetNumber,
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() {
1095 return refName;
1098 public Integer getChangeNumber() {
1099 return changeNumber;
1102 public Integer getPatchSetNumber() {
1103 return patchSetNumber;
1106 @Override
1107 public String toString() {
1108 return refName;
1111 @Override
1112 public boolean equals(Object obj) {
1113 if (!(obj instanceof Change)) {
1114 return false;
1116 return compareTo((Change) obj) == 0;
1119 @Override
1120 public int hashCode() {
1121 return Objects.hash(changeNumber, patchSetNumber);
1124 @Override
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) {
1131 return 1;
1133 changeDiff = this.patchSetNumber
1134 .compareTo(o.getPatchSetNumber());
1136 return changeDiff;
1140 private final static class ChangeContentProposal implements
1141 IContentProposal {
1142 private final Change myChange;
1144 ChangeContentProposal(Change change) {
1145 myChange = change;
1148 @Override
1149 public String getContent() {
1150 return myChange.getRefName();
1153 @Override
1154 public int getCursorPosition() {
1155 return 0;
1158 @Override
1159 public String getDescription() {
1160 return NLS.bind(
1161 UIText.FetchGerritChangePage_ContentAssistDescription,
1162 myChange.getPatchSetNumber(), myChange.getChangeNumber());
1165 @Override
1166 public String getLabel() {
1167 return NLS
1168 .bind("{0} - {1}", myChange.getChangeNumber(), myChange.getPatchSetNumber()); //$NON-NLS-1$
1171 /* (non-Javadoc)
1172 * @see java.lang.Object#toString()
1174 @Override
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()}
1185 * directly.
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.
1199 CANCEL,
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
1204 * the meantime.
1206 ABANDON,
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.
1213 INTERRUPT
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
1248 : cancellation;
1249 switch (state) {
1250 case PRISTINE:
1251 finish(false);
1252 return true;
1253 case SCHEDULED:
1254 state = State.CANCELING;
1255 boolean canceled = job.cancel();
1256 if (canceled) {
1257 state = State.CANCELED;
1258 } else if (mode == CancelMode.INTERRUPT) {
1259 interrupt();
1260 } else if (mode == CancelMode.ABANDON) {
1261 notifyAll();
1263 return canceled;
1264 case CANCELING:
1265 // cancel(CANCEL|ABANDON) was called before.
1266 if (mode == CancelMode.INTERRUPT) {
1267 interrupt();
1268 } else if (mode == CancelMode.ABANDON) {
1269 notifyAll();
1271 return false;
1272 case INTERRUPT:
1273 if (mode != CancelMode.CANCEL) {
1274 notifyAll();
1276 return false;
1277 case CANCELED:
1278 return true;
1279 default:
1280 return false;
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
1298 * canceled
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 {
1306 switch (state) {
1307 case DONE:
1308 case CANCELED:
1309 return result;
1310 case PRISTINE:
1311 fetch();
1312 return get();
1313 default:
1314 wait();
1315 if (state == State.CANCELING || state == State.INTERRUPT) {
1316 // canceled with ABANDON or INTERRUPT
1317 throw new InterruptedException();
1319 return get();
1323 public synchronized Collection<Change> getResult() {
1324 if (isFinished()) {
1325 return result;
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;
1333 job = null;
1334 notifyAll(); // We're done, wake up all outstanding get() calls
1337 private synchronized void interrupt() {
1338 state = State.INTERRUPT;
1339 job.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) {
1352 return;
1354 ListRemoteOperation listOp;
1355 try {
1356 listOp = new ListRemoteOperation(repository,
1357 new URIish(uriText),
1358 Activator.getDefault().getPreferenceStore().getInt(
1359 UIPreferences.REMOTE_CONNECTION_TIMEOUT));
1360 } catch (URISyntaxException e) {
1361 finish(false);
1362 throw new InvocationTargetException(e);
1364 job = new InterruptibleJob(MessageFormat.format(
1365 UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
1366 uriText)) {
1368 @Override
1369 protected IStatus run(IProgressMonitor monitor) {
1370 try {
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
1382 // anymore.
1383 return Status.CANCEL_STATUS;
1386 return Activator
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() {
1404 @Override
1405 public void done(IJobChangeEvent event) {
1406 IStatus status = event.getResult();
1407 finish(status != null && status.isOK());
1411 job.setUser(false);
1412 job.setSystem(true);
1413 state = State.SCHEDULED;
1414 job.schedule();
1417 private static abstract class InterruptibleJob extends Job {
1419 public InterruptibleJob(String name) {
1420 super(name);
1423 public void interrupt() {
1424 Thread thread = getThread();
1425 if (thread != null) {
1426 thread.interrupt();