Generalize UIUtils.addContentProposalToText a bit more
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / fetch / FetchGerritChangePage.java
blob0754420a71fb270041014ff8cc029b2ea890637b
1 /*******************************************************************************
2 * Copyright (c) 2010, 2018 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, 518492
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.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.HashMap;
25 import java.util.LinkedHashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.Set;
30 import java.util.SortedSet;
31 import java.util.TreeSet;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
35 import org.eclipse.core.resources.IWorkspace;
36 import org.eclipse.core.resources.IWorkspaceRunnable;
37 import org.eclipse.core.resources.ResourcesPlugin;
38 import org.eclipse.core.runtime.CoreException;
39 import org.eclipse.core.runtime.IProgressMonitor;
40 import org.eclipse.core.runtime.IStatus;
41 import org.eclipse.core.runtime.OperationCanceledException;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.core.runtime.SubMonitor;
44 import org.eclipse.core.runtime.jobs.Job;
45 import org.eclipse.egit.core.internal.gerrit.GerritUtil;
46 import org.eclipse.egit.core.op.CreateLocalBranchOperation;
47 import org.eclipse.egit.core.op.ListRemoteOperation;
48 import org.eclipse.egit.core.op.TagOperation;
49 import org.eclipse.egit.ui.Activator;
50 import org.eclipse.egit.ui.JobFamilies;
51 import org.eclipse.egit.ui.UIPreferences;
52 import org.eclipse.egit.ui.UIUtils;
53 import org.eclipse.egit.ui.UIUtils.ExplicitContentProposalAdapter;
54 import org.eclipse.egit.ui.internal.ActionUtils;
55 import org.eclipse.egit.ui.internal.UIText;
56 import org.eclipse.egit.ui.internal.ValidationUtils;
57 import org.eclipse.egit.ui.internal.branch.BranchOperationUI;
58 import org.eclipse.egit.ui.internal.components.BranchNameNormalizer;
59 import org.eclipse.egit.ui.internal.dialogs.AbstractBranchSelectionDialog;
60 import org.eclipse.egit.ui.internal.dialogs.BranchEditDialog;
61 import org.eclipse.egit.ui.internal.dialogs.CancelableFuture;
62 import org.eclipse.egit.ui.internal.dialogs.NonBlockingWizardDialog;
63 import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings;
64 import org.eclipse.jface.dialogs.Dialog;
65 import org.eclipse.jface.dialogs.IDialogSettings;
66 import org.eclipse.jface.dialogs.IInputValidator;
67 import org.eclipse.jface.dialogs.IPageChangeProvider;
68 import org.eclipse.jface.dialogs.IPageChangedListener;
69 import org.eclipse.jface.dialogs.PageChangedEvent;
70 import org.eclipse.jface.fieldassist.IContentProposal;
71 import org.eclipse.jface.layout.GridDataFactory;
72 import org.eclipse.jface.operation.IRunnableWithProgress;
73 import org.eclipse.jface.resource.JFaceResources;
74 import org.eclipse.jface.window.Window;
75 import org.eclipse.jface.wizard.IWizardContainer;
76 import org.eclipse.jface.wizard.WizardPage;
77 import org.eclipse.jgit.lib.Constants;
78 import org.eclipse.jgit.lib.PersonIdent;
79 import org.eclipse.jgit.lib.Ref;
80 import org.eclipse.jgit.lib.Repository;
81 import org.eclipse.jgit.lib.TagBuilder;
82 import org.eclipse.jgit.revwalk.RevCommit;
83 import org.eclipse.jgit.revwalk.RevWalk;
84 import org.eclipse.jgit.transport.FetchResult;
85 import org.eclipse.jgit.transport.RefSpec;
86 import org.eclipse.jgit.transport.RemoteConfig;
87 import org.eclipse.jgit.transport.URIish;
88 import org.eclipse.osgi.util.NLS;
89 import org.eclipse.swt.SWT;
90 import org.eclipse.swt.SWTException;
91 import org.eclipse.swt.dnd.Clipboard;
92 import org.eclipse.swt.dnd.TextTransfer;
93 import org.eclipse.swt.dnd.Transfer;
94 import org.eclipse.swt.events.KeyAdapter;
95 import org.eclipse.swt.events.KeyEvent;
96 import org.eclipse.swt.events.ModifyEvent;
97 import org.eclipse.swt.events.ModifyListener;
98 import org.eclipse.swt.events.SelectionAdapter;
99 import org.eclipse.swt.events.SelectionEvent;
100 import org.eclipse.swt.layout.GridData;
101 import org.eclipse.swt.layout.GridLayout;
102 import org.eclipse.swt.widgets.Button;
103 import org.eclipse.swt.widgets.Combo;
104 import org.eclipse.swt.widgets.Composite;
105 import org.eclipse.swt.widgets.Control;
106 import org.eclipse.swt.widgets.Group;
107 import org.eclipse.swt.widgets.Label;
108 import org.eclipse.swt.widgets.Text;
109 import org.eclipse.ui.actions.ActionFactory;
110 import org.eclipse.ui.progress.WorkbenchJob;
113 * Fetch a change from Gerrit
115 public class FetchGerritChangePage extends WizardPage {
117 private static final String GERRIT_CHANGE_REF_PREFIX = "refs/changes/"; //$NON-NLS-1$
119 private static final Pattern GERRIT_FETCH_PATTERN = Pattern.compile(
120 "git fetch (\\w+:\\S+) (refs/changes/\\d+/\\d+/\\d+) && git (\\w+) FETCH_HEAD"); //$NON-NLS-1$
122 private static final Pattern GERRIT_URL_PATTERN = Pattern.compile(
123 "(?:https?://\\S+?/|/)?([1-9][0-9]*)(?:/([1-9][0-9]*)(?:/([1-9][0-9]*)(?:\\.\\.\\d+)?)?)?(?:/\\S*)?"); //$NON-NLS-1$
125 private static final Pattern GERRIT_CHANGE_REF_PATTERN = Pattern
126 .compile("refs/changes/(\\d\\d)/([1-9][0-9]*)(?:/([1-9][0-9]*)?)?"); //$NON-NLS-1$
128 private static final SimpleDateFormat SIMPLE_TIMESTAMP = new SimpleDateFormat(
129 "yyyyMMddHHmmss"); //$NON-NLS-1$
131 private enum CheckoutMode {
132 CREATE_BRANCH, CREATE_TAG, CHECKOUT_FETCH_HEAD, NOCHECKOUT
135 private final Repository repository;
137 private final IDialogSettings settings;
139 private final String lastUriKey;
141 private Combo uriCombo;
143 private Map<String, ChangeList> changeRefs = new HashMap<>();
145 private Text refText;
147 private Button createBranch;
149 private Button createTag;
151 private Button checkoutFetchHead;
153 private Button updateFetchHead;
155 private Label tagTextlabel;
157 private Text tagText;
159 private Label branchTextlabel;
161 private Text branchText;
163 private String refName;
165 private Composite warningAdditionalRefNotActive;
167 private Button activateAdditionalRefs;
169 private IInputValidator branchValidator;
171 private IInputValidator tagValidator;
173 private Button branchEditButton;
175 private Button branchCheckoutButton;
177 private ExplicitContentProposalAdapter contentProposer;
179 private boolean branchTextEdited;
181 private boolean tagTextEdited;
183 private boolean fetching;
185 private boolean doAutoFill = true;
188 * @param repository
189 * @param refName initial value for the ref field
191 public FetchGerritChangePage(Repository repository, String refName) {
192 super(FetchGerritChangePage.class.getName());
193 this.repository = repository;
194 this.refName = refName;
195 setTitle(NLS
196 .bind(UIText.FetchGerritChangePage_PageTitle,
197 Activator.getDefault().getRepositoryUtil()
198 .getRepositoryName(repository)));
199 setMessage(UIText.FetchGerritChangePage_PageMessage);
200 settings = getDialogSettings();
201 lastUriKey = repository + GerritDialogSettings.LAST_URI_SUFFIX;
203 branchValidator = ValidationUtils.getRefNameInputValidator(repository,
204 Constants.R_HEADS, true);
205 tagValidator = ValidationUtils.getRefNameInputValidator(repository,
206 Constants.R_TAGS, true);
209 @Override
210 protected IDialogSettings getDialogSettings() {
211 return GerritDialogSettings
212 .getSection(GerritDialogSettings.FETCH_FROM_GERRIT_SECTION);
215 @Override
216 public void createControl(Composite parent) {
217 parent.addDisposeListener(event -> {
218 for (ChangeList l : changeRefs.values()) {
219 l.cancel(ChangeList.CancelMode.INTERRUPT);
221 changeRefs.clear();
223 Clipboard clipboard = new Clipboard(parent.getDisplay());
224 String clipText = (String) clipboard.getContents(TextTransfer
225 .getInstance());
226 clipboard.dispose();
227 String defaultUri = null;
228 String defaultCommand = null;
229 String defaultChange = null;
230 Change candidateChange = null;
231 if (clipText != null) {
232 Matcher matcher = GERRIT_FETCH_PATTERN.matcher(clipText);
233 if (matcher.matches()) {
234 defaultUri = matcher.group(1);
235 defaultChange = matcher.group(2);
236 defaultCommand = matcher.group(3);
237 } else {
238 candidateChange = determineChangeFromString(clipText.trim());
241 Composite main = new Composite(parent, SWT.NONE);
242 main.setLayout(new GridLayout(2, false));
243 GridDataFactory.fillDefaults().grab(true, true).applyTo(main);
244 new Label(main, SWT.NONE)
245 .setText(UIText.FetchGerritChangePage_UriLabel);
246 uriCombo = new Combo(main, SWT.DROP_DOWN);
247 GridDataFactory.fillDefaults().grab(true, false).applyTo(uriCombo);
248 uriCombo.addSelectionListener(new SelectionAdapter() {
249 @Override
250 public void widgetSelected(SelectionEvent e) {
251 String uriText = uriCombo.getText();
252 ChangeList list = changeRefs.get(uriText);
253 if (list != null) {
254 list.cancel(ChangeList.CancelMode.INTERRUPT);
256 list = new ChangeList(repository, uriText);
257 changeRefs.put(uriText, list);
258 preFetch(list);
261 new Label(main, SWT.NONE)
262 .setText(UIText.FetchGerritChangePage_ChangeLabel);
263 refText = new Text(main, SWT.SINGLE | SWT.BORDER);
264 GridDataFactory.fillDefaults().grab(true, false).applyTo(refText);
265 contentProposer = addRefContentProposalToText(refText);
266 refText.addVerifyListener(event -> {
267 event.text = event.text
268 // C.f. https://bugs.eclipse.org/bugs/show_bug.cgi?id=273470
269 .replaceAll("\\v", " ") //$NON-NLS-1$ //$NON-NLS-2$
270 .trim();
273 final Group checkoutGroup = new Group(main, SWT.SHADOW_ETCHED_IN);
274 checkoutGroup.setLayout(new GridLayout(3, false));
275 GridDataFactory.fillDefaults().span(3, 1).grab(true, false)
276 .applyTo(checkoutGroup);
277 checkoutGroup.setText(UIText.FetchGerritChangePage_AfterFetchGroup);
279 // radio: create local branch
280 createBranch = new Button(checkoutGroup, SWT.RADIO);
281 GridDataFactory.fillDefaults().span(1, 1).applyTo(createBranch);
282 createBranch.setText(UIText.FetchGerritChangePage_LocalBranchRadio);
283 createBranch.addSelectionListener(new SelectionAdapter() {
284 @Override
285 public void widgetSelected(SelectionEvent e) {
286 checkPage();
290 branchCheckoutButton = new Button(checkoutGroup, SWT.CHECK);
291 GridDataFactory.fillDefaults().span(2, 1).align(SWT.END, SWT.CENTER)
292 .applyTo(branchCheckoutButton);
293 branchCheckoutButton.setFont(JFaceResources.getDialogFont());
294 branchCheckoutButton
295 .setText(UIText.FetchGerritChangePage_LocalBranchCheckout);
296 branchCheckoutButton.setSelection(true);
298 branchTextlabel = new Label(checkoutGroup, SWT.NONE);
299 GridDataFactory.defaultsFor(branchTextlabel).exclude(false)
300 .applyTo(branchTextlabel);
301 branchTextlabel.setText(UIText.FetchGerritChangePage_BranchNameText);
302 branchText = new Text(checkoutGroup, SWT.SINGLE | SWT.BORDER);
303 GridDataFactory.fillDefaults().grab(true, false)
304 .align(SWT.FILL, SWT.CENTER).applyTo(branchText);
305 branchText.addKeyListener(new KeyAdapter() {
307 @Override
308 public void keyPressed(KeyEvent e) {
309 branchTextEdited = true;
312 branchText.addVerifyListener(event -> {
313 if (event.text.isEmpty()) {
314 branchTextEdited = false;
317 branchText.addModifyListener(new ModifyListener() {
318 @Override
319 public void modifyText(ModifyEvent e) {
320 checkPage();
323 BranchNameNormalizer normalizer = new BranchNameNormalizer(branchText);
324 normalizer.setVisible(false);
325 branchEditButton = new Button(checkoutGroup, SWT.PUSH);
326 branchEditButton.setFont(JFaceResources.getDialogFont());
327 branchEditButton.setText(UIText.FetchGerritChangePage_BranchEditButton);
328 branchEditButton.addSelectionListener(new SelectionAdapter() {
329 @Override
330 public void widgetSelected(SelectionEvent selectionEvent) {
331 String txt = branchText.getText();
332 String refToMark = "".equals(txt) ? null : Constants.R_HEADS + txt; //$NON-NLS-1$
333 AbstractBranchSelectionDialog dlg = new BranchEditDialog(
334 checkoutGroup.getShell(), repository, refToMark);
335 if (dlg.open() == Window.OK) {
336 branchText.setText(Repository.shortenRefName(dlg
337 .getRefName()));
338 branchTextEdited = true;
339 } else {
340 // force calling branchText's modify listeners
341 branchText.setText(branchText.getText());
345 GridDataFactory.defaultsFor(branchEditButton).exclude(false)
346 .applyTo(branchEditButton);
348 // radio: create tag
349 createTag = new Button(checkoutGroup, SWT.RADIO);
350 GridDataFactory.fillDefaults().span(3, 1).applyTo(createTag);
351 createTag.setText(UIText.FetchGerritChangePage_TagRadio);
352 createTag.addSelectionListener(new SelectionAdapter() {
353 @Override
354 public void widgetSelected(SelectionEvent e) {
355 checkPage();
359 tagTextlabel = new Label(checkoutGroup, SWT.NONE);
360 GridDataFactory.defaultsFor(tagTextlabel).exclude(true)
361 .applyTo(tagTextlabel);
362 tagTextlabel.setText(UIText.FetchGerritChangePage_TagNameText);
363 tagText = new Text(checkoutGroup, SWT.SINGLE | SWT.BORDER);
364 GridDataFactory.fillDefaults().exclude(true).grab(true, false)
365 .applyTo(tagText);
366 tagText.addKeyListener(new KeyAdapter() {
368 @Override
369 public void keyPressed(KeyEvent e) {
370 tagTextEdited = true;
373 tagText.addVerifyListener(event -> {
374 if (event.text.isEmpty()) {
375 tagTextEdited = false;
378 tagText.addModifyListener(new ModifyListener() {
379 @Override
380 public void modifyText(ModifyEvent e) {
381 checkPage();
384 BranchNameNormalizer tagNormalizer = new BranchNameNormalizer(tagText,
385 UIText.BranchNameNormalizer_TooltipForTag);
386 tagNormalizer.setVisible(false);
388 // radio: checkout FETCH_HEAD
389 checkoutFetchHead = new Button(checkoutGroup, SWT.RADIO);
390 GridDataFactory.fillDefaults().span(3, 1).applyTo(checkoutFetchHead);
391 checkoutFetchHead.setText(UIText.FetchGerritChangePage_CheckoutRadio);
392 checkoutFetchHead.addSelectionListener(new SelectionAdapter() {
393 @Override
394 public void widgetSelected(SelectionEvent e) {
395 checkPage();
399 // radio: don't checkout
400 updateFetchHead = new Button(checkoutGroup, SWT.RADIO);
401 GridDataFactory.fillDefaults().span(3, 1).applyTo(updateFetchHead);
402 updateFetchHead.setText(UIText.FetchGerritChangePage_UpdateRadio);
403 updateFetchHead.addSelectionListener(new SelectionAdapter() {
404 @Override
405 public void widgetSelected(SelectionEvent e) {
406 checkPage();
410 if ("checkout".equals(defaultCommand)) { //$NON-NLS-1$
411 checkoutFetchHead.setSelection(true);
412 } else {
413 createBranch.setSelection(true);
416 warningAdditionalRefNotActive = new Composite(main, SWT.NONE);
417 GridDataFactory.fillDefaults().span(2, 1).grab(true, false)
418 .exclude(true).applyTo(warningAdditionalRefNotActive);
419 warningAdditionalRefNotActive.setLayout(new GridLayout(2, false));
420 warningAdditionalRefNotActive.setVisible(false);
422 activateAdditionalRefs = new Button(warningAdditionalRefNotActive,
423 SWT.CHECK);
424 activateAdditionalRefs
425 .setText(UIText.FetchGerritChangePage_ActivateAdditionalRefsButton);
426 activateAdditionalRefs
427 .setToolTipText(
428 UIText.FetchGerritChangePage_ActivateAdditionalRefsTooltip);
430 ActionUtils.setGlobalActions(refText, ActionUtils.createGlobalAction(
431 ActionFactory.PASTE, () -> doPaste(refText)));
432 refText.addModifyListener(new ModifyListener() {
433 @Override
434 public void modifyText(ModifyEvent e) {
435 Change change = determineChangeFromString(refText.getText());
436 String suggestion = ""; //$NON-NLS-1$
437 if (change != null) {
438 Object ps = change.getPatchSetNumber();
439 if (ps == null) {
440 ps = SIMPLE_TIMESTAMP.format(new Date());
442 suggestion = NLS.bind(
443 UIText.FetchGerritChangePage_SuggestedRefNamePattern,
444 change.getChangeNumber(),
445 ps);
447 if (!branchTextEdited) {
448 branchText.setText(suggestion);
450 if (!tagTextEdited) {
451 tagText.setText(suggestion);
453 checkPage();
456 if (defaultChange != null) {
457 refText.setText(defaultChange);
458 } else if (candidateChange != null) {
459 String ref = candidateChange.getRefName();
460 if (ref != null) {
461 refText.setText(ref);
462 } else {
463 refText.setText(candidateChange.getChangeNumber().toString());
467 // get all available Gerrit URIs from the repository
468 SortedSet<String> uris = new TreeSet<>();
469 try {
470 for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(repository
471 .getConfig())) {
472 if (GerritUtil.isGerritFetch(rc)) {
473 if (rc.getURIs().size() > 0) {
474 uris.add(rc.getURIs().get(0).toPrivateString());
476 for (URIish u : rc.getPushURIs()) {
477 uris.add(u.toPrivateString());
482 } catch (URISyntaxException e) {
483 Activator.handleError(e.getMessage(), e, false);
484 setErrorMessage(e.getMessage());
486 for (String aUri : uris) {
487 uriCombo.add(aUri);
488 changeRefs.put(aUri, new ChangeList(repository, aUri));
490 if (defaultUri != null) {
491 uriCombo.setText(defaultUri);
492 } else {
493 selectLastUsedUri();
495 String currentUri = uriCombo.getText();
496 ChangeList list = changeRefs.get(currentUri);
497 if (list == null) {
498 list = new ChangeList(repository, currentUri);
499 changeRefs.put(currentUri, list);
501 preFetch(list);
502 refText.setFocus();
503 Dialog.applyDialogFont(main);
504 setControl(main);
505 if (candidateChange != null) {
506 // Launch content assist when the page is displayed
507 final IWizardContainer container = getContainer();
508 if (container instanceof IPageChangeProvider) {
509 ((IPageChangeProvider) container)
510 .addPageChangedListener(new IPageChangedListener() {
511 @Override
512 public void pageChanged(PageChangedEvent event) {
513 if (event
514 .getSelectedPage() == FetchGerritChangePage.this) {
515 // Only the first time: remove myself
516 event.getPageChangeProvider()
517 .removePageChangedListener(this);
518 getControl().getDisplay()
519 .asyncExec(new Runnable() {
520 @Override
521 public void run() {
522 Control control = getControl();
523 if (control != null
524 && !control.isDisposed()) {
525 contentProposer
526 .openProposalPopup();
535 checkPage();
538 private void preFetch(ChangeList list) {
539 try {
540 list.start();
541 } catch (InvocationTargetException e) {
542 Activator.handleError(e.getLocalizedMessage(), e.getCause(), true);
547 * Tries to determine a Gerrit change number from an input string.
549 * @param input
550 * string to derive a change number from
551 * @return the change number and possibly also the patch set number, or
552 * {@code null} if none could be determined.
554 protected static Change determineChangeFromString(String input) {
555 if (input == null) {
556 return null;
558 try {
559 Matcher matcher = GERRIT_URL_PATTERN.matcher(input);
560 if (matcher.matches()) {
561 String first = matcher.group(1);
562 String second = matcher.group(2);
563 String third = matcher.group(3);
564 if (second != null && !second.isEmpty()) {
565 if (third != null && !third.isEmpty()) {
566 return Change.create(Integer.parseInt(second),
567 Integer.parseInt(third));
568 } else if (input.startsWith("http")) { //$NON-NLS-1$
569 // A URL ending with two digits: take the first as
570 // change number
571 return Change.create(Integer.parseInt(first),
572 Integer.parseInt(second));
573 } else {
574 // Take the numerically larger. Might be a fragment like
575 // /10/65510 as in refs/changes/10/65510/6, or /65510/6
576 // as in https://git.eclipse.org/r/#/c/65510/6. This is
577 // a heuristic, it might go wrong on a Gerrit where
578 // there are not many changes (yet), and one of them has
579 // many patch sets.
580 int firstNum = Integer.parseInt(first);
581 int secondNum = Integer.parseInt(second);
582 if (firstNum > secondNum) {
583 return Change.create(firstNum, secondNum);
584 } else {
585 return Change.create(secondNum);
588 } else {
589 return Change.create(Integer.parseInt(first));
592 matcher = GERRIT_CHANGE_REF_PATTERN.matcher(input);
593 if (matcher.matches()) {
594 int firstNum = Integer.parseInt(matcher.group(2));
595 String second = matcher.group(3);
596 if (second != null) {
597 return Change.create(firstNum, Integer.parseInt(second));
598 } else {
599 return Change.create(firstNum);
602 } catch (NumberFormatException e) {
603 // Numerical overflow?
605 return null;
608 private void doPaste(Text text) {
609 Clipboard clipboard = new Clipboard(text.getDisplay());
610 try {
611 String clipText = (String) clipboard
612 .getContents(TextTransfer.getInstance());
613 if (clipText != null) {
614 Change input = determineChangeFromString(
615 clipText.trim());
616 if (input != null) {
617 String toInsert = input.getChangeNumber().toString();
618 if (input.getPatchSetNumber() != null) {
619 if (text.getText().trim().isEmpty() || text
620 .getSelectionText().equals(text.getText())) {
621 // Paste will replace everything
622 toInsert = input.getRefName();
623 } else {
624 toInsert = toInsert + '/'
625 + input.getPatchSetNumber();
628 clipboard.setContents(new Object[] { toInsert },
629 new Transfer[] { TextTransfer.getInstance() });
630 try {
631 text.paste();
632 } finally {
633 clipboard.setContents(new Object[] { clipText },
634 new Transfer[] { TextTransfer.getInstance() });
636 } else {
637 text.paste();
640 } finally {
641 clipboard.dispose();
645 private void storeLastUsedUri(String uri) {
646 settings.put(lastUriKey, uri.trim());
649 private void selectLastUsedUri() {
650 String lastUri = settings.get(lastUriKey);
651 if (lastUri != null) {
652 int i = uriCombo.indexOf(lastUri);
653 if (i != -1) {
654 uriCombo.select(i);
655 return;
658 uriCombo.select(0);
661 @Override
662 public void setVisible(boolean visible) {
663 super.setVisible(visible);
664 if (visible && refName != null)
665 refText.setText(refName);
668 private void checkPage() {
669 boolean createBranchSelected = createBranch.getSelection();
670 branchText.setEnabled(createBranchSelected);
671 branchText.setVisible(createBranchSelected);
672 branchTextlabel.setVisible(createBranchSelected);
673 branchEditButton.setVisible(createBranchSelected);
674 branchCheckoutButton.setVisible(createBranchSelected);
675 GridData gd = (GridData) branchText.getLayoutData();
676 gd.exclude = !createBranchSelected;
677 gd = (GridData) branchTextlabel.getLayoutData();
678 gd.exclude = !createBranchSelected;
679 gd = (GridData) branchEditButton.getLayoutData();
680 gd.exclude = !createBranchSelected;
681 gd = (GridData) branchCheckoutButton.getLayoutData();
682 gd.exclude = !createBranchSelected;
684 boolean createTagSelected = createTag.getSelection();
685 tagText.setEnabled(createTagSelected);
686 tagText.setVisible(createTagSelected);
687 tagTextlabel.setVisible(createTagSelected);
688 gd = (GridData) tagText.getLayoutData();
689 gd.exclude = !createTagSelected;
690 gd = (GridData) tagTextlabel.getLayoutData();
691 gd.exclude = !createTagSelected;
692 branchText.getParent().layout(true);
694 boolean showActivateAdditionalRefs = false;
695 showActivateAdditionalRefs = (checkoutFetchHead.getSelection() || updateFetchHead
696 .getSelection())
697 && !Activator
698 .getDefault()
699 .getPreferenceStore()
700 .getBoolean(
701 UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS);
703 gd = (GridData) warningAdditionalRefNotActive.getLayoutData();
704 gd.exclude = !showActivateAdditionalRefs;
705 warningAdditionalRefNotActive.setVisible(showActivateAdditionalRefs);
706 warningAdditionalRefNotActive.getParent().layout(true);
708 setErrorMessage(null);
709 try {
710 if (refText.getText().length() > 0) {
711 Change change = Change.fromRef(refText.getText());
712 if (change == null) {
713 change = determineChangeFromString(refText.getText());
714 if (change == null) {
715 setErrorMessage(
716 UIText.FetchGerritChangePage_MissingChangeMessage);
717 return;
720 ChangeList list = changeRefs.get(uriCombo.getText());
721 if (list != null && list.isDone()) {
722 try {
723 if (change.getPatchSetNumber() != null) {
724 if (!list.get().contains(change)) {
725 setErrorMessage(
726 UIText.FetchGerritChangePage_UnknownChangeRefMessage);
727 return;
729 } else {
730 Change fromGerrit = findHighestPatchSet(list.get(),
731 change.getChangeNumber().intValue());
732 if (fromGerrit == null) {
733 setErrorMessage(NLS.bind(
734 UIText.FetchGerritChangePage_NoSuchChangeMessage,
735 change.getChangeNumber()));
736 return;
739 } catch (InterruptedException
740 | InvocationTargetException e) {
741 // Ignore: since we're done, this should never occur
744 } else {
745 setErrorMessage(UIText.FetchGerritChangePage_MissingChangeMessage);
746 return;
749 if (createBranchSelected) {
750 setErrorMessage(branchValidator.isValid(branchText.getText()));
751 } else if (createTagSelected) {
752 setErrorMessage(tagValidator.isValid(tagText.getText()));
754 } finally {
755 setPageComplete(getErrorMessage() == null);
759 private Collection<Change> getRefsForContentAssist(String originalRefText)
760 throws InvocationTargetException, InterruptedException {
761 String uriText = uriCombo.getText();
762 if (!changeRefs.containsKey(uriText)) {
763 changeRefs.put(uriText, new ChangeList(repository, uriText));
765 ChangeList list = changeRefs.get(uriText);
766 if (!list.isFinished()) {
767 IWizardContainer container = getContainer();
768 IRunnableWithProgress operation = monitor -> {
769 monitor.beginTask(MessageFormat.format(
770 UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
771 uriText), IProgressMonitor.UNKNOWN);
772 Collection<Change> result = list.get();
773 if (monitor.isCanceled()) {
774 return;
776 // If we get here, the ChangeList future is done.
777 if (result == null || result.isEmpty() || fetching) {
778 // Don't bother if we didn't get any results
779 return;
781 // If we do have results now, open the proposals.
782 Job showProposals = new WorkbenchJob(
783 UIText.FetchGerritChangePage_ShowingProposalsJobName) {
785 @Override
786 public boolean shouldRun() {
787 return super.shouldRun() && !fetching;
790 @Override
791 public IStatus runInUIThread(IProgressMonitor uiMonitor) {
792 // But only if we're not disposed, the focus is still
793 // (or again) in the Change field, and the uri is still
794 // the same
795 try {
796 if (container instanceof NonBlockingWizardDialog) {
797 // Otherwise the dialog was blocked anyway, and
798 // focus will be restored
799 if (fetching) {
800 return Status.CANCEL_STATUS;
802 String uriNow = uriCombo.getText();
803 if (!uriNow.equals(uriText)) {
804 return Status.CANCEL_STATUS;
806 if (refText != refText.getDisplay()
807 .getFocusControl()) {
808 fillInPatchSet(result, null);
809 return Status.CANCEL_STATUS;
811 // Try not to interfere with the user's typing.
812 // Only fill in the patch set number if the text
813 // is still the same.
814 fillInPatchSet(result, originalRefText);
815 doAutoFill = false;
816 } else {
817 // Dialog was blocked
818 fillInPatchSet(result, null);
819 doAutoFill = false;
821 contentProposer.openProposalPopup();
822 } catch (SWTException e) {
823 // Disposed already
824 return Status.CANCEL_STATUS;
825 } finally {
826 doAutoFill = true;
827 uiMonitor.done();
829 return Status.OK_STATUS;
833 showProposals.schedule();
835 if (container instanceof NonBlockingWizardDialog) {
836 NonBlockingWizardDialog dialog = (NonBlockingWizardDialog) container;
837 dialog.run(operation,
838 () -> {
839 if (!fetching) {
840 list.cancel(ChangeList.CancelMode.ABANDON);
843 } else {
844 container.run(true, true, operation);
846 return null;
848 // ChangeList is already here, so get() won't block
849 Collection<Change> changes = list.get();
850 if (doAutoFill) {
851 fillInPatchSet(changes, originalRefText);
853 return changes;
856 private void fillInPatchSet(Collection<Change> changes,
857 String originalText) {
858 String currentText = refText.getText();
859 if (contentProposer.isProposalPopupOpen()
860 || originalText != null && !originalText.equals(currentText)) {
861 // User has modified the text: don't interfere
862 return;
864 Change change = determineChangeFromString(currentText);
865 if (change != null && change.getPatchSetNumber() == null) {
866 Change fromGerrit = findHighestPatchSet(changes,
867 change.getChangeNumber().intValue());
868 if (fromGerrit != null) {
869 String fullRef = fromGerrit.getRefName();
870 refText.setText(fullRef);
871 refText.setSelection(fullRef.length());
876 private Change findHighestPatchSet(Collection<Change> changes,
877 int changeNumber) {
878 // We know that the result is sorted by change and
879 // patch set number descending
880 for (Change fromGerrit : changes) {
881 int num = fromGerrit.getChangeNumber().intValue();
882 if (num < changeNumber) {
883 return null; // Doesn't exist
884 } else if (changeNumber == num) {
885 // Must be the one with the highest patch
886 // set number.
887 return fromGerrit;
890 return null;
893 boolean doFetch() {
894 fetching = true;
895 final Change change = determineChangeFromString(refText.getText());
896 final String uri = uriCombo.getText();
897 // If we have an incomplete change (missing patch set number), remove
898 // the change list future from the global map so that it won't be
899 // interrupted when the dialog closes.
900 final ChangeList changeList = change.getPatchSetNumber() == null
901 ? changeRefs.remove(uri) : null;
902 if (changeList != null) {
903 // Make sure a pending get() from the content assist gets aborted
904 changeList.cancel(ChangeList.CancelMode.ABANDON);
906 final CheckoutMode mode = getCheckoutMode();
907 final boolean doCheckoutNewBranch = (mode == CheckoutMode.CREATE_BRANCH)
908 && branchCheckoutButton.getSelection();
909 final boolean doActivateAdditionalRefs = showAdditionalRefs();
910 final String textForTag = tagText.getText();
911 final String textForBranch = branchText.getText();
913 Job job = new Job(
914 UIText.FetchGerritChangePage_GetChangeTaskName) {
916 @Override
917 public IStatus run(IProgressMonitor monitor) {
918 try {
919 int steps = getTotalWork(mode);
920 SubMonitor progress = SubMonitor.convert(monitor,
921 UIText.FetchGerritChangePage_GetChangeTaskName,
922 steps + 1);
923 Change finalChange = completeChange(change,
924 progress.newChild(1));
925 if (finalChange == null) {
926 // Returning an error status would log the message
927 Activator.showError(NLS.bind(
928 UIText.FetchGerritChangePage_NoSuchChangeMessage,
929 change.getChangeNumber()), null);
930 return Status.CANCEL_STATUS;
932 final RefSpec spec = new RefSpec()
933 .setSource(finalChange.getRefName())
934 .setDestination(Constants.FETCH_HEAD);
935 if (progress.isCanceled()) {
936 return Status.CANCEL_STATUS;
938 RevCommit commit = fetchChange(uri, spec,
939 progress.newChild(1));
940 if (mode != CheckoutMode.NOCHECKOUT) {
941 IWorkspace workspace = ResourcesPlugin.getWorkspace();
942 IWorkspaceRunnable operation = new IWorkspaceRunnable() {
944 @Override
945 public void run(IProgressMonitor innerMonitor)
946 throws CoreException {
947 SubMonitor innerProgress = SubMonitor
948 .convert(innerMonitor, steps);
949 switch (mode) {
950 case CHECKOUT_FETCH_HEAD:
951 checkout(commit.name(),
952 innerProgress.newChild(1));
953 break;
954 case CREATE_TAG:
955 createTag(spec, textForTag, commit,
956 innerProgress.newChild(1));
957 checkout(commit.name(),
958 innerProgress.newChild(1));
959 break;
960 case CREATE_BRANCH:
961 createBranch(textForBranch,
962 doCheckoutNewBranch, commit,
963 innerProgress.newChild(1));
964 break;
965 default:
966 break;
970 workspace.run(operation, null, IWorkspace.AVOID_UPDATE,
971 progress.newChild(steps));
973 if (doActivateAdditionalRefs) {
974 activateAdditionalRefs();
976 if (mode == CheckoutMode.NOCHECKOUT) {
977 // Tell the world that FETCH_HEAD only changed. In other
978 // cases, JGit will have sent a RefsChangeEvent
979 // already.
980 repository.fireEvent(new FetchHeadChangedEvent());
982 storeLastUsedUri(uri);
983 } catch (OperationCanceledException oe) {
984 return Status.CANCEL_STATUS;
985 } catch (CoreException ce) {
986 return ce.getStatus();
987 } catch (Exception e) {
988 return Activator.createErrorStatus(e.getLocalizedMessage(),
990 } finally {
991 monitor.done();
993 return Status.OK_STATUS;
996 @Override
997 protected void canceling() {
998 super.canceling();
999 if (changeList != null) {
1000 changeList.cancel(ChangeList.CancelMode.INTERRUPT);
1004 private Change completeChange(Change originalChange,
1005 IProgressMonitor monitor)
1006 throws OperationCanceledException {
1007 if (changeList != null) {
1008 monitor.subTask(NLS.bind(
1009 UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
1010 uri));
1011 Collection<Change> changes;
1012 try {
1013 changes = changeList.get();
1014 } catch (InvocationTargetException
1015 | InterruptedException e) {
1016 throw new OperationCanceledException();
1018 if (monitor.isCanceled()) {
1019 throw new OperationCanceledException();
1021 return findHighestPatchSet(changes,
1022 originalChange.getChangeNumber().intValue());
1024 return originalChange;
1027 private int getTotalWork(final CheckoutMode m) {
1028 switch (m) {
1029 case CHECKOUT_FETCH_HEAD:
1030 case CREATE_BRANCH:
1031 return 2;
1032 case CREATE_TAG:
1033 return 3;
1034 default:
1035 return 1;
1039 @Override
1040 public boolean belongsTo(Object family) {
1041 if (JobFamilies.FETCH.equals(family))
1042 return true;
1043 return super.belongsTo(family);
1046 job.setUser(true);
1047 job.schedule();
1048 return true;
1051 private boolean showAdditionalRefs() {
1052 return (checkoutFetchHead.getSelection()
1053 || updateFetchHead.getSelection())
1054 && activateAdditionalRefs.getSelection();
1057 private CheckoutMode getCheckoutMode() {
1058 if (createBranch.getSelection()) {
1059 return CheckoutMode.CREATE_BRANCH;
1060 } else if (createTag.getSelection()) {
1061 return CheckoutMode.CREATE_TAG;
1062 } else if (checkoutFetchHead.getSelection()) {
1063 return CheckoutMode.CHECKOUT_FETCH_HEAD;
1064 } else {
1065 return CheckoutMode.NOCHECKOUT;
1069 private RevCommit fetchChange(String uri, RefSpec spec,
1070 IProgressMonitor monitor) throws CoreException, URISyntaxException,
1071 IOException {
1072 int timeout = Activator.getDefault().getPreferenceStore()
1073 .getInt(UIPreferences.REMOTE_CONNECTION_TIMEOUT);
1075 List<RefSpec> specs = new ArrayList<>(1);
1076 specs.add(spec);
1078 String taskName = NLS
1079 .bind(UIText.FetchGerritChangePage_FetchingTaskName,
1080 spec.getSource());
1081 monitor.subTask(taskName);
1082 FetchResult fetchRes = new FetchOperationUI(repository,
1083 new URIish(uri), specs, timeout, false).execute(monitor);
1085 monitor.worked(1);
1086 try (RevWalk rw = new RevWalk(repository)) {
1087 return rw.parseCommit(
1088 fetchRes.getAdvertisedRef(spec.getSource()).getObjectId());
1092 private void createTag(final RefSpec spec, final String textForTag,
1093 RevCommit commit, IProgressMonitor monitor) throws CoreException {
1094 monitor.subTask(UIText.FetchGerritChangePage_CreatingTagTaskName);
1095 final TagBuilder tag = new TagBuilder();
1096 PersonIdent personIdent = new PersonIdent(repository);
1098 tag.setTag(textForTag);
1099 tag.setTagger(personIdent);
1100 tag.setMessage(NLS.bind(
1101 UIText.FetchGerritChangePage_GeneratedTagMessage,
1102 spec.getSource()));
1103 tag.setObjectId(commit);
1104 new TagOperation(repository, tag, false).execute(monitor);
1105 monitor.worked(1);
1108 private void createBranch(final String textForBranch, boolean doCheckout,
1109 RevCommit commit, IProgressMonitor monitor) throws CoreException {
1110 SubMonitor progress = SubMonitor.convert(monitor, doCheckout ? 10 : 2);
1111 progress.subTask(UIText.FetchGerritChangePage_CreatingBranchTaskName);
1112 CreateLocalBranchOperation bop = new CreateLocalBranchOperation(
1113 repository, textForBranch, commit);
1114 bop.execute(progress.newChild(2));
1115 if (doCheckout) {
1116 checkout(textForBranch, progress.newChild(8));
1120 private void checkout(String targetName, IProgressMonitor monitor)
1121 throws CoreException {
1122 monitor.subTask(UIText.FetchGerritChangePage_CheckingOutTaskName);
1123 BranchOperationUI.checkout(repository, targetName).run(monitor);
1124 monitor.worked(1);
1127 private void activateAdditionalRefs() {
1128 Activator.getDefault().getPreferenceStore().setValue(
1129 UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS, true);
1132 private ExplicitContentProposalAdapter addRefContentProposalToText(
1133 final Text textField) {
1134 return UIUtils.addContentProposalToText(textField, () -> {
1135 try {
1136 return getRefsForContentAssist(textField.getText());
1137 } catch (InvocationTargetException e) {
1138 Activator.handleError(e.getMessage(), e, true);
1139 return null;
1140 } catch (InterruptedException e) {
1141 return null;
1143 }, (pattern, ref) -> {
1144 if (pattern == null || pattern
1145 .matcher(ref.getChangeNumber().toString()).matches()) {
1146 return new ChangeContentProposal(ref);
1148 return null;
1149 }, s -> {
1150 String input = s;
1151 Matcher matcher = GERRIT_CHANGE_REF_PATTERN.matcher(input);
1152 if (matcher.find()) {
1153 input = matcher.group(2);
1155 return UIUtils.createProposalPattern(input);
1156 }, null, UIText.FetchGerritChangePage_ContentAssistTooltip);
1159 final static class Change implements Comparable<Change> {
1160 private final String refName;
1162 private final Integer changeNumber;
1164 private final Integer patchSetNumber;
1166 static Change fromRef(String refName) {
1167 try {
1168 if (refName == null) {
1169 return null;
1171 Matcher m = GERRIT_CHANGE_REF_PATTERN.matcher(refName);
1172 if (!m.matches() || m.group(3) == null) {
1173 return null;
1175 Integer subdir = Integer.valueOf(m.group(1));
1176 Integer changeNumber = Integer.valueOf(m.group(2));
1177 if (subdir.intValue() != changeNumber.intValue() % 100) {
1178 return null;
1180 Integer patchSetNumber = Integer.valueOf(m.group(3));
1181 return new Change(refName, changeNumber, patchSetNumber);
1182 } catch (NumberFormatException e) {
1183 // if we can't parse this, just return null
1184 return null;
1185 } catch (IndexOutOfBoundsException e) {
1186 // if we can't parse this, just return null
1187 return null;
1191 static Change create(int changeNumber) {
1192 return new Change(null, Integer.valueOf(changeNumber), null);
1195 static Change create(int changeNumber, int patchSetNumber) {
1196 int subDir = changeNumber % 100;
1197 return new Change(
1198 GERRIT_CHANGE_REF_PREFIX
1199 + String.format("%02d", Integer.valueOf(subDir)) //$NON-NLS-1$
1200 + '/' + changeNumber + '/' + patchSetNumber,
1201 Integer.valueOf(changeNumber),
1202 Integer.valueOf(patchSetNumber));
1205 private Change(String refName, Integer changeNumber,
1206 Integer patchSetNumber) {
1207 this.refName = refName;
1208 this.changeNumber = changeNumber;
1209 this.patchSetNumber = patchSetNumber;
1212 public String getRefName() {
1213 return refName;
1216 public Integer getChangeNumber() {
1217 return changeNumber;
1220 public Integer getPatchSetNumber() {
1221 return patchSetNumber;
1224 @Override
1225 public String toString() {
1226 return refName;
1229 @Override
1230 public boolean equals(Object obj) {
1231 if (!(obj instanceof Change)) {
1232 return false;
1234 return compareTo((Change) obj) == 0;
1237 @Override
1238 public int hashCode() {
1239 return Objects.hash(changeNumber, patchSetNumber);
1242 @Override
1243 public int compareTo(Change o) {
1244 int changeDiff = this.changeNumber.compareTo(o.getChangeNumber());
1245 if (changeDiff == 0) {
1246 if (patchSetNumber == null) {
1247 return o.getPatchSetNumber() != null ? -1 : 0;
1248 } else if (o.getPatchSetNumber() == null) {
1249 return 1;
1251 changeDiff = this.patchSetNumber
1252 .compareTo(o.getPatchSetNumber());
1254 return changeDiff;
1258 private final static class ChangeContentProposal implements
1259 IContentProposal {
1260 private final Change myChange;
1262 ChangeContentProposal(Change change) {
1263 myChange = change;
1266 @Override
1267 public String getContent() {
1268 return myChange.getRefName();
1271 @Override
1272 public int getCursorPosition() {
1273 return 0;
1276 @Override
1277 public String getDescription() {
1278 return NLS.bind(
1279 UIText.FetchGerritChangePage_ContentAssistDescription,
1280 myChange.getPatchSetNumber(), myChange.getChangeNumber());
1283 @Override
1284 public String getLabel() {
1285 return NLS
1286 .bind("{0} - {1}", myChange.getChangeNumber(), myChange.getPatchSetNumber()); //$NON-NLS-1$
1289 /* (non-Javadoc)
1290 * @see java.lang.Object#toString()
1292 @Override
1293 public String toString() {
1294 return getContent();
1299 * A {@code ChangeList} loads the list of change refs asynchronously from
1300 * the remote repository.
1302 private static class ChangeList extends CancelableFuture<Set<Change>> {
1304 private final Repository repository;
1306 private final String uriText;
1308 private ListRemoteOperation listOp;
1310 public ChangeList(Repository repository, String uriText) {
1311 this.repository = repository;
1312 this.uriText = uriText;
1315 @Override
1316 protected String getJobTitle() {
1317 return MessageFormat.format(
1318 UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
1319 uriText);
1322 @Override
1323 protected void prepareRun() throws InvocationTargetException {
1324 try {
1325 listOp = new ListRemoteOperation(repository,
1326 new URIish(uriText),
1327 Activator.getDefault().getPreferenceStore().getInt(
1328 UIPreferences.REMOTE_CONNECTION_TIMEOUT));
1329 } catch (URISyntaxException e) {
1330 throw new InvocationTargetException(e);
1334 @Override
1335 protected void run(IProgressMonitor monitor)
1336 throws InterruptedException, InvocationTargetException {
1337 listOp.run(monitor);
1338 List<Change> changes = new ArrayList<>();
1339 for (Ref ref : listOp.getRemoteRefs()) {
1340 Change change = Change.fromRef(ref.getName());
1341 if (change != null) {
1342 changes.add(change);
1345 Collections.sort(changes, Collections.reverseOrder());
1346 set(new LinkedHashSet<>(changes));
1349 @Override
1350 protected void done() {
1351 listOp = null;