/*******************************************************************************
* Copyright (c) 2010, 2017 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mathias Kinzler (SAP AG) - initial implementation
* Marc Khouzam (Ericsson) - Add an option not to checkout the new branch
* Thomas Wolf <thomas.wolf@paranor.ch> - Bug 493935, 495777
*******************************************************************************/
package org.eclipse.egit.ui.internal.fetch;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.egit.core.internal.gerrit.GerritUtil;
import org.eclipse.egit.core.op.CreateLocalBranchOperation;
import org.eclipse.egit.core.op.ListRemoteOperation;
import org.eclipse.egit.core.op.TagOperation;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.ActionUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.ValidationUtils;
import org.eclipse.egit.ui.internal.branch.BranchOperationUI;
import org.eclipse.egit.ui.internal.components.BranchNameNormalizer;
import org.eclipse.egit.ui.internal.dialogs.AbstractBranchSelectionDialog;
import org.eclipse.egit.ui.internal.dialogs.BranchEditDialog;
import org.eclipse.egit.ui.internal.dialogs.NonBlockingWizardDialog;
import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.IPageChangeProvider;
import org.eclipse.jface.dialogs.IPageChangedListener;
import org.eclipse.jface.dialogs.PageChangedEvent;
import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.IWizardContainer;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TagBuilder;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* Fetch a change from Gerrit
*/
public class FetchGerritChangePage extends WizardPage {
private enum CheckoutMode {
CREATE_BRANCH, CREATE_TAG, CHECKOUT_FETCH_HEAD, NOCHECKOUT
}
private final Repository repository;
private final IDialogSettings settings;
private final String lastUriKey;
private Combo uriCombo;
private Map<String, ChangeList> changeRefs = new HashMap<>();
private Text refText;
private Button createBranch;
private Button createTag;
private Button checkoutFetchHead;
private Button updateFetchHead;
private Label tagTextlabel;
private Text tagText;
private Label branchTextlabel;
private Text branchText;
private String refName;
private Composite warningAdditionalRefNotActive;
private Button activateAdditionalRefs;
private IInputValidator branchValidator;
private IInputValidator tagValidator;
private Button branchEditButton;
private Button branchCheckoutButton;
private ExplicitContentProposalAdapter contentProposer;
private boolean branchTextEdited;
private boolean tagTextEdited;
/**
* @param repository
* @param refName initial value for the ref field
*/
public FetchGerritChangePage(Repository repository, String refName) {
super(FetchGerritChangePage.class.getName());
this.repository = repository;
this.refName = refName;
setTitle(NLS
.bind(UIText.FetchGerritChangePage_PageTitle,
Activator.getDefault().getRepositoryUtil()
.getRepositoryName(repository)));
setMessage(UIText.FetchGerritChangePage_PageMessage);
settings = getDialogSettings();
lastUriKey = repository + GerritDialogSettings.LAST_URI_SUFFIX;
branchValidator = ValidationUtils.getRefNameInputValidator(repository,
Constants.R_HEADS, true);
tagValidator = ValidationUtils.getRefNameInputValidator(repository,
Constants.R_TAGS, true);
}
@Override
protected IDialogSettings getDialogSettings() {
return GerritDialogSettings
.getSection(GerritDialogSettings.FETCH_FROM_GERRIT_SECTION);
}
@Override
public void createControl(Composite parent) {
parent.addDisposeListener(event -> {
for (ChangeList l : changeRefs.values()) {
l.cancel(ChangeList.CancelMode.INTERRUPT);
}
changeRefs.clear();
});
Clipboard clipboard = new Clipboard(parent.getDisplay());
String clipText = (String) clipboard.getContents(TextTransfer
.getInstance());
clipboard.dispose();
String defaultUri = null;
String defaultCommand = null;
String defaultChange = null;
String candidateChange = null;
if (clipText != null) {
String pattern = "git fetch (\\w+:\\S+) (refs/changes/\\d+/\\d+/\\d+) && git (\\w+) FETCH_HEAD"; //$NON-NLS-1$
Matcher matcher = Pattern.compile(pattern).matcher(clipText);
if (matcher.matches()) {
defaultUri = matcher.group(1);
defaultChange = matcher.group(2);
defaultCommand = matcher.group(3);
} else {
candidateChange = determineChangeFromString(clipText.trim());
}
}
Composite main = new Composite(parent, SWT.NONE);
main.setLayout(new GridLayout(2, false));
GridDataFactory.fillDefaults().grab(true, true).applyTo(main);
new Label(main, SWT.NONE)
.setText(UIText.FetchGerritChangePage_UriLabel);
uriCombo = new Combo(main, SWT.DROP_DOWN);
GridDataFactory.fillDefaults().grab(true, false).applyTo(uriCombo);
uriCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
String uriText = uriCombo.getText();
ChangeList list = changeRefs.get(uriText);
if (list != null) {
list.cancel(ChangeList.CancelMode.INTERRUPT);
}
list = new ChangeList(repository, uriText);
changeRefs.put(uriText, list);
preFetch(list);
}
});
new Label(main, SWT.NONE)
.setText(UIText.FetchGerritChangePage_ChangeLabel);
refText = new Text(main, SWT.SINGLE | SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false).applyTo(refText);
contentProposer = addRefContentProposalToText(refText);
refText.addVerifyListener(event -> {
event.text = event.text
// C.f. https://bugs.eclipse.org/bugs/show_bug.cgi?id=273470
.replaceAll("\\v", " ") //$NON-NLS-1$ //$NON-NLS-2$
.trim();
});
final Group checkoutGroup = new Group(main, SWT.SHADOW_ETCHED_IN);
checkoutGroup.setLayout(new GridLayout(3, false));
GridDataFactory.fillDefaults().span(3, 1).grab(true, false)
.applyTo(checkoutGroup);
checkoutGroup.setText(UIText.FetchGerritChangePage_AfterFetchGroup);
// radio: create local branch
createBranch = new Button(checkoutGroup, SWT.RADIO);
GridDataFactory.fillDefaults().span(1, 1).applyTo(createBranch);
createBranch.setText(UIText.FetchGerritChangePage_LocalBranchRadio);
createBranch.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkPage();
}
});
branchCheckoutButton = new Button(checkoutGroup, SWT.CHECK);
GridDataFactory.fillDefaults().span(2, 1).align(SWT.END, SWT.CENTER)
.applyTo(branchCheckoutButton);
branchCheckoutButton.setFont(JFaceResources.getDialogFont());
branchCheckoutButton
.setText(UIText.FetchGerritChangePage_LocalBranchCheckout);
branchCheckoutButton.setSelection(true);
branchTextlabel = new Label(checkoutGroup, SWT.NONE);
GridDataFactory.defaultsFor(branchTextlabel).exclude(false)
.applyTo(branchTextlabel);
branchTextlabel.setText(UIText.FetchGerritChangePage_BranchNameText);
branchText = new Text(checkoutGroup, SWT.SINGLE | SWT.BORDER);
GridDataFactory.fillDefaults().grab(true, false)
.align(SWT.FILL, SWT.CENTER).applyTo(branchText);
branchText.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
branchTextEdited = true;
}
});
branchText.addVerifyListener(event -> {
if (event.text.isEmpty()) {
branchTextEdited = false;
}
});
branchText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
BranchNameNormalizer normalizer = new BranchNameNormalizer(branchText);
normalizer.setVisible(false);
branchEditButton = new Button(checkoutGroup, SWT.PUSH);
branchEditButton.setFont(JFaceResources.getDialogFont());
branchEditButton.setText(UIText.FetchGerritChangePage_BranchEditButton);
branchEditButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent selectionEvent) {
String txt = branchText.getText();
String refToMark = "".equals(txt) ? null : Constants.R_HEADS + txt; //$NON-NLS-1$
AbstractBranchSelectionDialog dlg = new BranchEditDialog(
checkoutGroup.getShell(), repository, refToMark);
if (dlg.open() == Window.OK) {
branchText.setText(Repository.shortenRefName(dlg
.getRefName()));
branchTextEdited = true;
} else {
// force calling branchText's modify listeners
branchText.setText(branchText.getText());
}
}
});
GridDataFactory.defaultsFor(branchEditButton).exclude(false)
.applyTo(branchEditButton);
// radio: create tag
createTag = new Button(checkoutGroup, SWT.RADIO);
GridDataFactory.fillDefaults().span(3, 1).applyTo(createTag);
createTag.setText(UIText.FetchGerritChangePage_TagRadio);
createTag.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkPage();
}
});
tagTextlabel = new Label(checkoutGroup, SWT.NONE);
GridDataFactory.defaultsFor(tagTextlabel).exclude(true)
.applyTo(tagTextlabel);
tagTextlabel.setText(UIText.FetchGerritChangePage_TagNameText);
tagText = new Text(checkoutGroup, SWT.SINGLE | SWT.BORDER);
GridDataFactory.fillDefaults().exclude(true).grab(true, false)
.applyTo(tagText);
tagText.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
tagTextEdited = true;
}
});
tagText.addVerifyListener(event -> {
if (event.text.isEmpty()) {
tagTextEdited = false;
}
});
tagText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
checkPage();
}
});
BranchNameNormalizer tagNormalizer = new BranchNameNormalizer(tagText,
UIText.BranchNameNormalizer_TooltipForTag);
tagNormalizer.setVisible(false);
// radio: checkout FETCH_HEAD
checkoutFetchHead = new Button(checkoutGroup, SWT.RADIO);
GridDataFactory.fillDefaults().span(3, 1).applyTo(checkoutFetchHead);
checkoutFetchHead.setText(UIText.FetchGerritChangePage_CheckoutRadio);
checkoutFetchHead.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkPage();
}
});
// radio: don't checkout
updateFetchHead = new Button(checkoutGroup, SWT.RADIO);
GridDataFactory.fillDefaults().span(3, 1).applyTo(updateFetchHead);
updateFetchHead.setText(UIText.FetchGerritChangePage_UpdateRadio);
updateFetchHead.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkPage();
}
});
if ("checkout".equals(defaultCommand)) { //$NON-NLS-1$
checkoutFetchHead.setSelection(true);
} else {
createBranch.setSelection(true);
}
warningAdditionalRefNotActive = new Composite(main, SWT.NONE);
GridDataFactory.fillDefaults().span(2, 1).grab(true, false)
.exclude(true).applyTo(warningAdditionalRefNotActive);
warningAdditionalRefNotActive.setLayout(new GridLayout(2, false));
warningAdditionalRefNotActive.setVisible(false);
activateAdditionalRefs = new Button(warningAdditionalRefNotActive,
SWT.CHECK);
activateAdditionalRefs
.setText(UIText.FetchGerritChangePage_ActivateAdditionalRefsButton);
activateAdditionalRefs
.setToolTipText(
UIText.FetchGerritChangePage_ActivateAdditionalRefsTooltip);
ActionUtils.setGlobalActions(refText, ActionUtils.createGlobalAction(
ActionFactory.PASTE, () -> doPaste(refText)));
refText.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
Change change = Change.fromRef(refText.getText());
String suggestion = ""; //$NON-NLS-1$
if (change != null) {
suggestion = NLS.bind(
UIText.FetchGerritChangePage_SuggestedRefNamePattern,
change.getChangeNumber(),
change.getPatchSetNumber());
}
if (!branchTextEdited) {
branchText.setText(suggestion);
}
if (!tagTextEdited) {
tagText.setText(suggestion);
}
checkPage();
}
});
if (defaultChange != null) {
refText.setText(defaultChange);
} else if (candidateChange != null) {
refText.setText(candidateChange);
}
// get all available Gerrit URIs from the repository
SortedSet<String> uris = new TreeSet<>();
try {
for (RemoteConfig rc : RemoteConfig.getAllRemoteConfigs(repository
.getConfig())) {
if (GerritUtil.isGerritFetch(rc)) {
if (rc.getURIs().size() > 0) {
uris.add(rc.getURIs().get(0).toPrivateString());
}
for (URIish u : rc.getPushURIs()) {
uris.add(u.toPrivateString());
}
}
}
} catch (URISyntaxException e) {
Activator.handleError(e.getMessage(), e, false);
setErrorMessage(e.getMessage());
}
for (String aUri : uris) {
uriCombo.add(aUri);
changeRefs.put(aUri, new ChangeList(repository, aUri));
}
if (defaultUri != null) {
uriCombo.setText(defaultUri);
} else {
selectLastUsedUri();
}
String currentUri = uriCombo.getText();
ChangeList list = changeRefs.get(currentUri);
if (list == null) {
list = new ChangeList(repository, currentUri);
changeRefs.put(currentUri, list);
}
preFetch(list);
refText.setFocus();
Dialog.applyDialogFont(main);
setControl(main);
if (candidateChange != null) {
// Launch content assist when the page is displayed
final IWizardContainer container = getContainer();
if (container instanceof IPageChangeProvider) {
((IPageChangeProvider) container)
.addPageChangedListener(new IPageChangedListener() {
@Override
public void pageChanged(PageChangedEvent event) {
if (event
.getSelectedPage() == FetchGerritChangePage.this) {
// Only the first time: remove myself
event.getPageChangeProvider()
.removePageChangedListener(this);
getControl().getDisplay()
.asyncExec(new Runnable() {
@Override
public void run() {
Control control = getControl();
if (control != null
&& !control.isDisposed()) {
contentProposer
.openProposalPopup();
}
}
});
}
}
});
}
}
checkPage();
}
private void preFetch(ChangeList list) {
try {
list.fetch();
} catch (InvocationTargetException e) {
Activator.handleError(e.getLocalizedMessage(), e.getCause(), true);
}
}
/**
* Tries to determine a Gerrit change number from an input string.
*
* @param input
* string to derive a change number from
* @return the change number as a string, or {@code null} if none could be
* determined.
*/
protected static String determineChangeFromString(String input) {
if (input == null) {
return null;
}
Pattern pattern = Pattern.compile(
"(?:https?://\\S+?/|/)?([1-9][0-9]*)(?:/([1-9][0-9]*)(?:/([1-9][0-9]*)(?:\\.\\.\\d+)?)?)?(?:/\\S*)?"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
String first = matcher.group(1);
String second = matcher.group(2);
String third = matcher.group(3);
if (second != null && !second.isEmpty()) {
if (third != null && !third.isEmpty()) {
return second;
} else if (input.startsWith("http")) { //$NON-NLS-1$
// A URL ending with two digits: take the first.
return first;
} else {
// Take the numerically larger. Might be a fragment like
// /10/65510 as in refs/changes/10/65510/6, or /65510/6 as
// in https://git.eclipse.org/r/#/c/65510/6. This is a
// heuristic, it might go wrong on a Gerrit where there are
// not many changes (yet), and one of them has many patch
// sets.
try {
if (Integer.parseInt(first) > Integer
.parseInt(second)) {
return first;
} else {
return second;
}
} catch (NumberFormatException e) {
// Numerical overflow?
return null;
}
}
} else {
return first;
}
}
return null;
}
private void doPaste(Text text) {
Clipboard clipboard = new Clipboard(text.getDisplay());
try {
String clipText = (String) clipboard
.getContents(TextTransfer.getInstance());
if (clipText != null) {
String toInsert = determineChangeFromString(clipText.trim());
if (toInsert != null) {
clipboard.setContents(new Object[] { toInsert },
new Transfer[] { TextTransfer.getInstance() });
try {
text.paste();
} finally {
clipboard.setContents(new Object[] { clipText },
new Transfer[] { TextTransfer.getInstance() });
}
} else {
text.paste();
}
}
} finally {
clipboard.dispose();
}
}
private void storeLastUsedUri(String uri) {
settings.put(lastUriKey, uri.trim());
}
private void selectLastUsedUri() {
String lastUri = settings.get(lastUriKey);
if (lastUri != null) {
int i = uriCombo.indexOf(lastUri);
if (i != -1) {
uriCombo.select(i);
return;
}
}
uriCombo.select(0);
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible && refName != null)
refText.setText(refName);
}
private void checkPage() {
boolean createBranchSelected = createBranch.getSelection();
branchText.setEnabled(createBranchSelected);
branchText.setVisible(createBranchSelected);
branchTextlabel.setVisible(createBranchSelected);
branchEditButton.setVisible(createBranchSelected);
branchCheckoutButton.setVisible(createBranchSelected);
GridData gd = (GridData) branchText.getLayoutData();
gd.exclude = !createBranchSelected;
gd = (GridData) branchTextlabel.getLayoutData();
gd.exclude = !createBranchSelected;
gd = (GridData) branchEditButton.getLayoutData();
gd.exclude = !createBranchSelected;
gd = (GridData) branchCheckoutButton.getLayoutData();
gd.exclude = !createBranchSelected;
boolean createTagSelected = createTag.getSelection();
tagText.setEnabled(createTagSelected);
tagText.setVisible(createTagSelected);
tagTextlabel.setVisible(createTagSelected);
gd = (GridData) tagText.getLayoutData();
gd.exclude = !createTagSelected;
gd = (GridData) tagTextlabel.getLayoutData();
gd.exclude = !createTagSelected;
branchText.getParent().layout(true);
boolean showActivateAdditionalRefs = false;
showActivateAdditionalRefs = (checkoutFetchHead.getSelection() || updateFetchHead
.getSelection())
&& !Activator
.getDefault()
.getPreferenceStore()
.getBoolean(
UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS);
gd = (GridData) warningAdditionalRefNotActive.getLayoutData();
gd.exclude = !showActivateAdditionalRefs;
warningAdditionalRefNotActive.setVisible(showActivateAdditionalRefs);
warningAdditionalRefNotActive.getParent().layout(true);
setErrorMessage(null);
try {
if (refText.getText().length() > 0) {
Change change = Change.fromRef(refText.getText());
if (change == null) {
setErrorMessage(UIText.FetchGerritChangePage_MissingChangeMessage);
return;
}
} else {
setErrorMessage(UIText.FetchGerritChangePage_MissingChangeMessage);
return;
}
if (createBranchSelected)
setErrorMessage(branchValidator.isValid(branchText.getText()));
else if (createTagSelected)
setErrorMessage(tagValidator.isValid(tagText.getText()));
} finally {
setPageComplete(getErrorMessage() == null);
}
}
private List<Change> getRefsForContentAssist()
throws InvocationTargetException, InterruptedException {
String uriText = uriCombo.getText();
if (!changeRefs.containsKey(uriText)) {
changeRefs.put(uriText, new ChangeList(repository, uriText));
}
ChangeList list = changeRefs.get(uriText);
if (!list.isDone()) {
IWizardContainer container = getContainer();
IRunnableWithProgress operation = monitor -> {
monitor.beginTask(MessageFormat.format(
UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
uriText), IProgressMonitor.UNKNOWN);
List<Change> result = list.get();
if (monitor.isCanceled()) {
return;
}
// If we get here, the ChangeList future is done.
if (result == null || result.isEmpty()) {
// Don't bother if we didn't get any results
return;
}
// If we do have results now, open the proposals.
Job showProposals = new WorkbenchJob(
UIText.FetchGerritChangePage_ShowingProposalsJobName) {
@Override
public IStatus runInUIThread(IProgressMonitor uiMonitor) {
// But only if we're not disposed, the focus is still
// (or again) in the Change field, and the uri is still
// the same
try {
if (container instanceof NonBlockingWizardDialog) {
// Otherwise the dialog was blocked anyway, and
// focus will be restored
if (refText != refText.getDisplay()
.getFocusControl()) {
return Status.CANCEL_STATUS;
}
String uriNow = uriCombo.getText();
if (!uriNow.equals(uriText)) {
return Status.CANCEL_STATUS;
}
}
contentProposer.openProposalPopup();
} catch (SWTException e) {
// Disposed already
return Status.CANCEL_STATUS;
} finally {
uiMonitor.done();
}
return Status.OK_STATUS;
}
};
showProposals.schedule();
};
if (container instanceof NonBlockingWizardDialog) {
NonBlockingWizardDialog dialog = (NonBlockingWizardDialog) container;
dialog.run(operation,
() -> list.cancel(ChangeList.CancelMode.ABANDON));
} else {
container.run(true, true, operation);
}
return null;
}
return list.get();
}
boolean doFetch() {
final RefSpec spec = new RefSpec().setSource(refText.getText())
.setDestination(Constants.FETCH_HEAD);
final String uri = uriCombo.getText();
final CheckoutMode mode = getCheckoutMode();
final boolean doCheckoutNewBranch = (mode == CheckoutMode.CREATE_BRANCH)
&& branchCheckoutButton.getSelection();
final boolean doActivateAdditionalRefs = showAdditionalRefs();
final String textForTag = tagText.getText();
final String textForBranch = branchText.getText();
Job job = new WorkspaceJob(
UIText.FetchGerritChangePage_GetChangeTaskName) {
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) {
try {
SubMonitor progress = SubMonitor.convert(monitor,
UIText.FetchGerritChangePage_GetChangeTaskName,
getTotalWork(mode));
RevCommit commit = fetchChange(uri, spec,
progress.newChild(1));
switch (mode) {
case CHECKOUT_FETCH_HEAD:
checkout(commit.name(), progress.newChild(1));
break;
case CREATE_TAG:
createTag(spec, textForTag, commit,
progress.newChild(1));
checkout(commit.name(), progress.newChild(1));
break;
case CREATE_BRANCH:
createBranch(textForBranch, doCheckoutNewBranch, commit,
progress.newChild(1));
break;
default:
break;
}
if (doActivateAdditionalRefs) {
activateAdditionalRefs();
}
if (mode == CheckoutMode.NOCHECKOUT) {
// Tell the world that FETCH_HEAD only changed. In other
// cases, JGit will have sent a RefsChangeEvent
// already.
repository.fireEvent(new FetchHeadChangedEvent());
}
storeLastUsedUri(uri);
} catch (CoreException ce) {
return ce.getStatus();
} catch (Exception e) {
return Activator.createErrorStatus(e.getLocalizedMessage(),
e);
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
private int getTotalWork(final CheckoutMode m) {
switch (m) {
case CHECKOUT_FETCH_HEAD:
case CREATE_BRANCH:
return 2;
case CREATE_TAG:
return 3;
default:
return 1;
}
}
@Override
public boolean belongsTo(Object family) {
if (JobFamilies.FETCH.equals(family))
return true;
return super.belongsTo(family);
}
};
job.setUser(true);
job.schedule();
return true;
}
private boolean showAdditionalRefs() {
return (checkoutFetchHead.getSelection()
|| updateFetchHead.getSelection())
&& activateAdditionalRefs.getSelection();
}
private CheckoutMode getCheckoutMode() {
if (createBranch.getSelection()) {
return CheckoutMode.CREATE_BRANCH;
} else if (createTag.getSelection()) {
return CheckoutMode.CREATE_TAG;
} else if (checkoutFetchHead.getSelection()) {
return CheckoutMode.CHECKOUT_FETCH_HEAD;
} else {
return CheckoutMode.NOCHECKOUT;
}
}
private RevCommit fetchChange(String uri, RefSpec spec,
IProgressMonitor monitor) throws CoreException, URISyntaxException,
IOException {
int timeout = Activator.getDefault().getPreferenceStore()
.getInt(UIPreferences.REMOTE_CONNECTION_TIMEOUT);
List<RefSpec> specs = new ArrayList<>(1);
specs.add(spec);
String taskName = NLS
.bind(UIText.FetchGerritChangePage_FetchingTaskName,
spec.getSource());
monitor.subTask(taskName);
FetchResult fetchRes = new FetchOperationUI(repository,
new URIish(uri), specs, timeout, false).execute(monitor);
monitor.worked(1);
try (RevWalk rw = new RevWalk(repository)) {
return rw.parseCommit(
fetchRes.getAdvertisedRef(spec.getSource()).getObjectId());
}
}
private void createTag(final RefSpec spec, final String textForTag,
RevCommit commit, IProgressMonitor monitor) throws CoreException {
monitor.subTask(UIText.FetchGerritChangePage_CreatingTagTaskName);
final TagBuilder tag = new TagBuilder();
PersonIdent personIdent = new PersonIdent(repository);
tag.setTag(textForTag);
tag.setTagger(personIdent);
tag.setMessage(NLS.bind(
UIText.FetchGerritChangePage_GeneratedTagMessage,
spec.getSource()));
tag.setObjectId(commit);
new TagOperation(repository, tag, false).execute(monitor);
monitor.worked(1);
}
private void createBranch(final String textForBranch, boolean doCheckout,
RevCommit commit, IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, doCheckout ? 10 : 2);
progress.subTask(UIText.FetchGerritChangePage_CreatingBranchTaskName);
CreateLocalBranchOperation bop = new CreateLocalBranchOperation(
repository, textForBranch, commit);
bop.execute(progress.newChild(2));
if (doCheckout) {
checkout(textForBranch, progress.newChild(8));
}
}
private void checkout(String targetName, IProgressMonitor monitor)
throws CoreException {
monitor.subTask(UIText.FetchGerritChangePage_CheckingOutTaskName);
BranchOperationUI.checkout(repository, targetName).run(monitor);
monitor.worked(1);
}
private void activateAdditionalRefs() {
// do this in the UI thread as it results in a
// refresh() on the history page
PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
Activator
.getDefault()
.getPreferenceStore()
.setValue(
UIPreferences.RESOURCEHISTORY_SHOW_ADDITIONAL_REFS,
true);
}
});
}
private ExplicitContentProposalAdapter addRefContentProposalToText(
final Text textField) {
KeyStroke stroke = UIUtils
.getKeystrokeOfBestActiveBindingFor(IWorkbenchCommandConstants.EDIT_CONTENT_ASSIST);
if (stroke != null) {
UIUtils.addBulbDecorator(textField, NLS.bind(
UIText.FetchGerritChangePage_ContentAssistTooltip,
stroke.format()));
}
IContentProposalProvider cp = new IContentProposalProvider() {
@Override
public IContentProposal[] getProposals(String contents, int position) {
List<Change> proposals;
try {
proposals = getRefsForContentAssist();
} catch (InvocationTargetException e) {
Activator.handleError(e.getMessage(), e, true);
return null;
} catch (InterruptedException e) {
return null;
}
if (proposals == null) {
return null;
}
List<IContentProposal> resultList = new ArrayList<>();
Pattern pattern = UIUtils.createProposalPattern(contents);
for (final Change ref : proposals) {
if (pattern != null && !pattern
.matcher(ref.getChangeNumber().toString())
.matches()) {
continue;
}
resultList.add(new ChangeContentProposal(ref));
}
return resultList
.toArray(new IContentProposal[resultList.size()]);
}
};
ExplicitContentProposalAdapter adapter = new ExplicitContentProposalAdapter(
textField, cp, stroke);
// set the acceptance style to always replace the complete content
adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
return adapter;
}
private static class ExplicitContentProposalAdapter
extends ContentProposalAdapter {
public ExplicitContentProposalAdapter(Control control,
IContentProposalProvider proposalProvider,
KeyStroke keyStroke) {
super(control, new TextContentAdapter(), proposalProvider,
keyStroke, null);
}
@Override
public void openProposalPopup() {
// Make this method accessible
super.openProposalPopup();
}
}
private final static class Change implements Comparable<Change> {
private final String refName;
private final Integer changeNumber;
private final Integer patchSetNumber;
static Change fromRef(String refName) {
try {
if (refName == null || !refName.startsWith("refs/changes/")) //$NON-NLS-1$
return null;
String[] tokens = refName.substring(13).split("/"); //$NON-NLS-1$
if (tokens.length != 3)
return null;
Integer changeNumber = Integer.valueOf(tokens[1]);
Integer patchSetNumber = Integer.valueOf(tokens[2]);
return new Change(refName, changeNumber, patchSetNumber);
} catch (NumberFormatException e) {
// if we can't parse this, just return null
return null;
} catch (IndexOutOfBoundsException e) {
// if we can't parse this, just return null
return null;
}
}
private Change(String refName, Integer changeNumber,
Integer patchSetNumber) {
this.refName = refName;
this.changeNumber = changeNumber;
this.patchSetNumber = patchSetNumber;
}
public String getRefName() {
return refName;
}
public Integer getChangeNumber() {
return changeNumber;
}
public Integer getPatchSetNumber() {
return patchSetNumber;
}
@Override
public String toString() {
return refName;
}
@Override
public int compareTo(Change o) {
int changeDiff = this.changeNumber.compareTo(o.changeNumber);
if (changeDiff == 0) {
changeDiff = this.getPatchSetNumber()
.compareTo(o.getPatchSetNumber());
}
return changeDiff;
}
}
private final static class ChangeContentProposal implements
IContentProposal {
private final Change myChange;
ChangeContentProposal(Change change) {
myChange = change;
}
@Override
public String getContent() {
return myChange.getRefName();
}
@Override
public int getCursorPosition() {
return 0;
}
@Override
public String getDescription() {
return NLS.bind(
UIText.FetchGerritChangePage_ContentAssistDescription,
myChange.getPatchSetNumber(), myChange.getChangeNumber());
}
@Override
public String getLabel() {
return NLS
.bind("{0} - {1}", myChange.getChangeNumber(), myChange.getPatchSetNumber()); //$NON-NLS-1$
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getContent();
}
}
/**
* A {@code ChangeList} is a "Future", loading the list of change refs
* asynchronously from the remote repository. The {@link ChangeList#get()
* get()} method blocks until the result is available or the future is
* canceled. Pre-fetching is possible by calling {@link ChangeList#fetch()}
* directly.
*/
private static class ChangeList {
/**
* Determines how to cancel a not-yet-completed future. Irrespective of
* the mechanism, the job may actually terminate normally, and
* subsequent calls to get() may return a result.
*/
public static enum CancelMode {
/**
* Tries to cancel the job, which may decide to ignore the request.
* Callers to get() will remain blocked until the job terminates.
*/
CANCEL,
/**
* Tries to cancel the job, which may decide to ignore the request.
* Outstanding get() calls will be woken up and may throw
* InterruptedException or return a result if the job terminated in
* the meantime.
*/
ABANDON,
/**
* Tries to cancel the job, and if that doesn't succeed immediately,
* interrupts the job's thread. Outstanding calls to get() will be
* woken up and may throw InterruptedException or return a result if
* the job terminated in the meantime.
*/
INTERRUPT
}
private static enum State {
PRISTINE, SCHEDULED, CANCELING, INTERRUPT, CANCELED, DONE
}
private final Repository repository;
private final String uriText;
private State state = State.PRISTINE;
private List<Change> result;
private InterruptibleJob job;
public ChangeList(Repository repository, String uriText) {
this.repository = repository;
this.uriText = uriText;
}
/**
* Tries to cancel the future. {@code cancel(false)} tries a normal job
* cancellation, which may or may not terminated the job (it may decide
* not to react to cancellation requests).
*
* @param cancellation
* {@link CancelMode} defining how to cancel
*
* @return {@code true} if the future was canceled (its job is not
* running anymore), {@code false} otherwise.
*/
public synchronized boolean cancel(CancelMode cancellation) {
CancelMode mode = cancellation == null ? CancelMode.CANCEL
: cancellation;
switch (state) {
case PRISTINE:
finish(false);
return true;
case SCHEDULED:
state = State.CANCELING;
boolean canceled = job.cancel();
if (canceled) {
state = State.CANCELED;
} else if (mode == CancelMode.INTERRUPT) {
interrupt();
} else if (mode == CancelMode.ABANDON) {
notifyAll();
}
return canceled;
case CANCELING:
// cancel(CANCEL|ABANDON) was called before.
if (mode == CancelMode.INTERRUPT) {
interrupt();
} else if (mode == CancelMode.ABANDON) {
notifyAll();
}
return false;
case INTERRUPT:
if (mode != CancelMode.CANCEL) {
notifyAll();
}
return false;
case CANCELED:
return true;
default:
return false;
}
}
public synchronized boolean isDone() {
return state == State.CANCELED || state == State.DONE;
}
/**
* Retrieves the result. If the result is not yet available, the method
* blocks until it is or {@link #cancel(CancelMode)} is called with
* {@link CancelMode#ABANDON} or {@link CancelMode#INTERRUPT}.
*
* @return the result, which may be {@code null} if the future was
* canceled
* @throws InterruptedException
* if waiting was interrupted
* @throws InvocationTargetException
* if the future's job cannot be created
*/
public synchronized List<Change> get()
throws InterruptedException, InvocationTargetException {
switch (state) {
case DONE:
case CANCELED:
return result;
case PRISTINE:
fetch();
return get();
default:
wait();
if (state == State.CANCELING || state == State.INTERRUPT) {
// canceled with ABANDON or INTERRUPT
throw new InterruptedException();
}
return get();
}
}
private synchronized void finish(boolean done) {
state = done ? State.DONE : State.CANCELED;
job = null;
notifyAll(); // We're done, wake up all outstanding get() calls
}
private synchronized void interrupt() {
state = State.INTERRUPT;
job.interrupt();
notifyAll(); // Abandon outstanding get() calls
}
/**
* On the first call, starts a background job to fetch the result.
* Subsequent calls do nothing and return immediately.
*
* @throws InvocationTargetException
* if starting the job fails
*/
public synchronized void fetch() throws InvocationTargetException {
if (job != null || state != State.PRISTINE) {
return;
}
ListRemoteOperation listOp;
try {
listOp = new ListRemoteOperation(repository,
new URIish(uriText),
Activator.getDefault().getPreferenceStore().getInt(
UIPreferences.REMOTE_CONNECTION_TIMEOUT));
} catch (URISyntaxException e) {
finish(false);
throw new InvocationTargetException(e);
}
job = new InterruptibleJob(MessageFormat.format(
UIText.FetchGerritChangePage_FetchingRemoteRefsMessage,
uriText)) {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
listOp.run(monitor);
} catch (InterruptedException e) {
return Status.CANCEL_STATUS;
} catch (InvocationTargetException e) {
synchronized (ChangeList.this) {
if (state == State.CANCELING
|| state == State.INTERRUPT) {
// JGit may report a TransportException when the
// thread is interrupted. Let's just pretend we
// canceled before. Also, if the user canceled
// already, he's not interested in errors
// anymore.
return Status.CANCEL_STATUS;
}
}
return Activator
.createErrorStatus(e.getLocalizedMessage(), e);
}
List<Change> changes = new ArrayList<>();
for (Ref ref : listOp.getRemoteRefs()) {
Change change = Change.fromRef(ref.getName());
if (change != null) {
changes.add(change);
}
}
Collections.sort(changes, Collections.reverseOrder());
result = changes;
return Status.OK_STATUS;
}
};
job.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
IStatus status = event.getResult();
finish(status != null && status.isOK());
}
});
job.setUser(false);
job.setSystem(true);
state = State.SCHEDULED;
job.schedule();
}
private static abstract class InterruptibleJob extends Job {
public InterruptibleJob(String name) {
super(name);
}
public void interrupt() {
Thread thread = getThread();
if (thread != null) {
thread.interrupt();
}
}
}
}
}