From c8e178cc3eff1851ce223c8e3546bf0cdec23a69 Mon Sep 17 00:00:00 2001 From: Ian Pun Date: Wed, 8 Mar 2017 10:46:29 -0500 Subject: [PATCH] Open the clone wizard when a git URL is dropped anywhere on Eclipse Created new GitUrlChecker class that allows for checking strings to see if it is a proper git URL. You are now able to drag and drop a git URL into a running workspace to spawn a clone wizard (implemented by GitCloneDropAdapter). GitCloneDropAdapter is copied from MarketplaceDropAdapter with minor modifications (run clone wizard, resolve compiler warnings, improve a misleading exception handler). Bug: 513247 Change-Id: I609d9847eff4de70f198c8d6ce1289e1a2155e98 Signed-off-by: Ian Pun Signed-off-by: Thomas Wolf --- org.eclipse.egit.ui/plugin.xml | 6 + .../ui/internal/clone/GitCloneDropAdapter.java | 445 +++++++++++++++++++++ .../egit/ui/internal/clone/GitUrlChecker.java | 92 +++++ .../components/RepositorySelectionPage.java | 49 +-- 4 files changed, 550 insertions(+), 42 deletions(-) create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitCloneDropAdapter.java create mode 100644 org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitUrlChecker.java diff --git a/org.eclipse.egit.ui/plugin.xml b/org.eclipse.egit.ui/plugin.xml index 02a168c1a..737c9f553 100644 --- a/org.eclipse.egit.ui/plugin.xml +++ b/org.eclipse.egit.ui/plugin.xml @@ -6369,4 +6369,10 @@ + + + + diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitCloneDropAdapter.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitCloneDropAdapter.java new file mode 100644 index 000000000..a9287efa4 --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitCloneDropAdapter.java @@ -0,0 +1,445 @@ +/******************************************************************************* + * Copyright (c) 2011, 2017 The Eclipse Foundation 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: + * The Eclipse Foundation - initial API and implementation + * Ian Pun - reimplemented to work with Git Cloning DND using MarketplaceDropAdapter + *******************************************************************************/ +package org.eclipse.egit.ui.internal.clone; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.egit.ui.Activator; +import org.eclipse.egit.ui.internal.repository.tree.command.CloneCommand; +import org.eclipse.jface.util.Util; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DropTarget; +import org.eclipse.swt.dnd.DropTargetAdapter; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.eclipse.swt.dnd.DropTargetListener; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.dnd.URLTransfer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IPageListener; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IPartService; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.IPerspectiveListener; +import org.eclipse.ui.IStartup; +import org.eclipse.ui.IWindowListener; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.progress.UIJob; + +/** + * Adapter to listen for any Drag and Drop operations that transfer a valid git + * URL. If it goes through the URL parser correctly, a Clone Git Repo wizard + * will appear and be populated. + */ +public class GitCloneDropAdapter implements IStartup { + + private static final int[] PREFERRED_DROP_OPERATIONS = { DND.DROP_DEFAULT, + DND.DROP_COPY, DND.DROP_MOVE, DND.DROP_LINK }; + + private static final int DROP_OPERATIONS = DND.DROP_MOVE | DND.DROP_COPY + | DND.DROP_LINK | DND.DROP_DEFAULT; + + private final DropTargetAdapter dropListener = new GitDropTargetListener(); + + private final WorkbenchListener workbenchListener = new WorkbenchListener(); + + private Transfer[] transferAgents; + + @Override + public void earlyStartup() { + UIJob registerJob = new UIJob(Display.getDefault(), + "Git Clone DND Initialization") { //$NON-NLS-1$ + { + setPriority(Job.SHORT); + setSystem(true); + } + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + IWorkbench workbench = PlatformUI.getWorkbench(); + workbench.addWindowListener(workbenchListener); + IWorkbenchWindow[] workbenchWindows = workbench + .getWorkbenchWindows(); + for (IWorkbenchWindow window : workbenchWindows) { + workbenchListener.hookWindow(window); + } + return Status.OK_STATUS; + } + + }; + registerJob.schedule(); + } + + private void installDropTarget(final Shell shell) { + hookUrlTransfer(shell, dropListener); + } + + private DropTarget hookUrlTransfer(final Shell shell, + DropTargetAdapter dropAdapter) { + DropTarget target = findDropTarget(shell); + if (target != null) { + // target exists, get it and check proper registration + registerWithExistingTarget(target); + } else { + target = new DropTarget(shell, DROP_OPERATIONS); + if (transferAgents == null) { + transferAgents = new Transfer[] { URLTransfer.getInstance() }; + } + target.setTransfer(transferAgents); + } + registerDropListener(target, dropAdapter); + + Control[] children = shell.getChildren(); + for (Control child : children) { + hookRecursive(child, dropAdapter); + } + return target; + } + + private void registerDropListener(DropTarget target, + DropTargetListener dropAdapter) { + target.removeDropListener(dropAdapter); + target.addDropListener(dropAdapter); + } + + private void hookRecursive(Control child, DropTargetListener dropAdapter) { + DropTarget childTarget = findDropTarget(child); + if (childTarget != null) { + registerWithExistingTarget(childTarget); + registerDropListener(childTarget, dropAdapter); + } + if (child instanceof Composite) { + Composite composite = (Composite) child; + Control[] children = composite.getChildren(); + for (Control control : children) { + hookRecursive(control, dropAdapter); + } + } + } + + private void registerWithExistingTarget(DropTarget target) { + Transfer[] transfers = target.getTransfer(); + if (transfers != null) { + for (Transfer transfer : transfers) { + if (transfer instanceof URLTransfer) { + return; + } + } + Transfer[] newTransfers = new Transfer[transfers.length + 1]; + System.arraycopy(transfers, 0, newTransfers, 0, transfers.length); + newTransfers[transfers.length] = URLTransfer.getInstance(); + target.setTransfer(newTransfers); + } + } + + private DropTarget findDropTarget(Control control) { + Object object = control.getData(DND.DROP_TARGET_KEY); + if (object instanceof DropTarget) { + return (DropTarget) object; + } + return null; + } + + /** + * @param url + */ + protected void proceedClone(String url) { + CloneCommand command = new CloneCommand(url); + try { + command.execute(new ExecutionEvent()); + } catch (ExecutionException e) { + Activator.logError(e.getLocalizedMessage(), e); + } + } + + private class GitDropTargetListener extends DropTargetAdapter { + + @Override + public void dragEnter(DropTargetEvent e) { + updateDragDetails(e); + } + + @Override + public void dragOver(DropTargetEvent e) { + updateDragDetails(e); + } + + @Override + public void dragLeave(DropTargetEvent e) { + if (e.detail == DND.DROP_NONE) { + setDropOperation(e); + } + } + + @Override + public void dropAccept(DropTargetEvent e) { + updateDragDetails(e); + } + + @Override + public void dragOperationChanged(DropTargetEvent e) { + updateDragDetails(e); + } + + private void setDropOperation(DropTargetEvent e) { + int allowedOperations = e.operations; + for (int op : PREFERRED_DROP_OPERATIONS) { + if ((allowedOperations & op) != 0) { + e.detail = op; + return; + } + } + e.detail = allowedOperations; + } + + private void updateDragDetails(DropTargetEvent e) { + if (dropTargetIsValid(e, false)) { + setDropOperation(e); + } + } + + private boolean dropTargetIsValid(DropTargetEvent e, boolean isDrop) { + if (URLTransfer.getInstance().isSupportedType(e.currentDataType)) { + // on Windows, we get the URL already during drag operations... + // FIXME find a way to check the URL early on other platforms, + // too... + if (isDrop || Util.isWindows()) { + if (e.data == null && !extractEventData(e)) { + // ... but if we don't, it's no problem, unless this is + // already the final drop event + return !isDrop; + } + final String url = getUrl(e.data); + if (!GitUrlChecker.isValidGitUrl(url)) { + return false; + } + } + return true; + } + return false; + } + + private boolean extractEventData(DropTargetEvent e) { + TransferData transferData = e.currentDataType; + if (transferData != null) { + Object data = URLTransfer.getInstance() + .nativeToJava(transferData); + if (data != null && getUrl(data) != null) { + e.data = data; + return true; + } + } + return false; + } + + @Override + public void drop(DropTargetEvent event) { + if (!URLTransfer.getInstance() + .isSupportedType(event.currentDataType)) { + // ignore + return; + } + if (event.data == null) { + // reject + event.detail = DND.DROP_NONE; + return; + } + if (!dropTargetIsValid(event, true)) { + // reject + event.detail = DND.DROP_NONE; + return; + } + final String url = getUrl(event.data); + DropTarget source = (DropTarget) event.getSource(); + Display display = source.getDisplay(); + display.asyncExec(new Runnable() { + @Override + public void run() { + proceedClone(url); + } + }); + + } + + private String getUrl(Object eventData) { + if (!(eventData instanceof String)) { + return null; + } + // Depending on the form the link and browser/os, + // we get the url twice in the data separated by new lines + String[] dataLines = ((String) eventData) + .split(System.getProperty("line.separator")); //$NON-NLS-1$ + String url = dataLines[0]; + return url; + } + } + + private class WorkbenchListener implements IPartListener2, IPageListener, + IPerspectiveListener, IWindowListener { + + @Override + public void perspectiveActivated(IWorkbenchPage page, + IPerspectiveDescriptor perspective) { + pageChanged(page); + } + + @Override + public void perspectiveChanged(IWorkbenchPage page, + IPerspectiveDescriptor perspective, String changeId) { + // Nothing to do + } + + @Override + public void pageActivated(IWorkbenchPage page) { + pageChanged(page); + } + + @Override + public void pageClosed(IWorkbenchPage page) { + // Nothing to do + } + + @Override + public void pageOpened(IWorkbenchPage page) { + pageChanged(page); + } + + private void pageChanged(IWorkbenchPage page) { + if (page == null) { + return; + } + IWorkbenchWindow workbenchWindow = page.getWorkbenchWindow(); + windowChanged(workbenchWindow); + } + + @Override + public void windowActivated(IWorkbenchWindow window) { + windowChanged(window); + } + + private void windowChanged(IWorkbenchWindow window) { + if (window == null) { + return; + } + Shell shell = window.getShell(); + runUpdate(shell); + } + + @Override + public void windowDeactivated(IWorkbenchWindow window) { + // Nothing to do + } + + @Override + public void windowClosed(IWorkbenchWindow window) { + // Nothing to do + } + + @Override + public void windowOpened(IWorkbenchWindow window) { + hookWindow(window); + } + + public void hookWindow(IWorkbenchWindow window) { + if (window == null) { + return; + } + window.addPageListener(this); + window.addPerspectiveListener(this); + IPartService partService = window.getService(IPartService.class); + partService.addPartListener(this); + windowChanged(window); + } + + @Override + public void partOpened(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partActivated(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partBroughtToTop(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partVisible(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partClosed(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partDeactivated(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partHidden(IWorkbenchPartReference partRef) { + partUpdate(partRef); + } + + @Override + public void partInputChanged(IWorkbenchPartReference partRef) { + // Nothing to do + } + + private void partUpdate(IWorkbenchPartReference partRef) { + if (partRef == null) { + return; + } + IWorkbenchPage page = partRef.getPage(); + pageChanged(page); + } + + private void runUpdate(final Shell shell) { + if (shell == null || shell.isDisposed()) { + return; + } + Display display = shell.getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + try { + display.asyncExec(new Runnable() { + + @Override + public void run() { + if (!shell.isDisposed()) { + installDropTarget(shell); + } + } + }); + } catch (RuntimeException ex) { + // Swallow + } + } + } +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitUrlChecker.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitUrlChecker.java new file mode 100644 index 000000000..9927dddef --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/clone/GitUrlChecker.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2011, 2017 The Eclipse Foundation 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: + * The Eclipse Foundation - initial API and implementation + * Ian Pun - factored out of RepositorySelectionPage + *******************************************************************************/ +package org.eclipse.egit.ui.internal.clone; + +import java.net.URISyntaxException; + +import org.eclipse.egit.ui.internal.KnownHosts; +import org.eclipse.egit.ui.internal.components.RepositorySelectionPage.Protocol; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.TransportProtocol; +import org.eclipse.jgit.transport.URIish; + +/** + * Utility class for checking strings for being valid git URLs, and for + * sanitizing arbitrary string input. + */ +public abstract class GitUrlChecker { + + private static final String GIT_CLONE_COMMAND_PREFIX = "git clone "; //$NON-NLS-1$ + + /** + * Checks if the incoming string is a valid git URL. It is recommended to + * {@link #sanitizeAsGitUrl(String) sanitize} the String first if coming + * from an untrustworthy source. + * + * @param url + * to check + * @return {@code true} if the {@code url} is a valid git URL, {@code false} + * otherwise + */ + public static boolean isValidGitUrl(String url) { + try { + if (url != null) { + URIish u = new URIish(url); + if (canHandleProtocol(u)) { + if (Protocol.GIT.handles(u) || Protocol.SSH.handles(u) + || (Protocol.HTTP.handles(u) + || Protocol.HTTPS.handles(u)) + && KnownHosts.isKnownHost(u.getHost()) + || url.endsWith(Constants.DOT_GIT_EXT)) { + return true; + } + } + } + } catch (URISyntaxException e) { + // Ignore. This is used to check arbitrary input for being a + // possibly valid git URL; we don't want to flood the log here. + } + return false; + } + + /** + * Sanitize a string for use as a git URL. Strips the Git Clone command if + * needed and reduces remaining the input to anything before the first + * whitespace. + * + * @param input + * String to be sanitized + * @return sanitized string; if the input came from an untrustworthy source, + * is should still be checked using {@link #isValidGitUrl(String)} + * before being used for real as a git URL + */ + public static String sanitizeAsGitUrl(String input) { + String sanitized = input.trim(); + if (sanitized.startsWith(GIT_CLONE_COMMAND_PREFIX)) { + sanitized = sanitized.substring(GIT_CLONE_COMMAND_PREFIX.length()) + .trim(); + } + // Take only the part up to the first whitespace character + return sanitized.split("[\\h|\\v]", 2)[0]; //$NON-NLS-1$ + } + + private static boolean canHandleProtocol(URIish u) { + for (TransportProtocol proto : Transport.getTransportProtocols()) { + if (proto.canHandle(u)) { + return true; + } + } + return false; + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java index e0da64257..7e2ba5b30 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RepositorySelectionPage.java @@ -26,9 +26,9 @@ import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.UIPreferences; import org.eclipse.egit.ui.UIUtils; import org.eclipse.egit.ui.UIUtils.IPreviousValueProposalHandler; -import org.eclipse.egit.ui.internal.KnownHosts; import org.eclipse.egit.ui.internal.SecureStoreUtils; import org.eclipse.egit.ui.internal.UIText; +import org.eclipse.egit.ui.internal.clone.GitUrlChecker; import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.IRemoteSelectionListener; import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.SelectionType; import org.eclipse.egit.ui.internal.provisional.wizards.GitRepositoryInfo; @@ -39,8 +39,6 @@ import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.Transport; -import org.eclipse.jgit.transport.TransportProtocol; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FS; import org.eclipse.osgi.util.NLS; @@ -73,8 +71,6 @@ import org.eclipse.ui.PlatformUI; */ public class RepositorySelectionPage extends WizardPage implements IRepositorySearchResult { - private static final String GIT_CLONE_COMMAND_PREFIX = "git clone "; //$NON-NLS-1$ - private static final String EMPTY_STRING = ""; //$NON-NLS-1$ private final static String USED_URIS_PREF = "RepositorySelectionPage.UsedUris"; //$NON-NLS-1$ @@ -329,26 +325,11 @@ public class RepositorySelectionPage extends WizardPage implements IRepositorySe Clipboard clipboard = new Clipboard(Display.getCurrent()); String text = (String) clipboard .getContents(TextTransfer.getInstance()); - try { - if (text != null) { - text = stripGitCloneCommand(text); - // Split on any whitespace character - text = text.split( - "[ \\f\\n\\r\\x0B\\t\\xA0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000]", //$NON-NLS-1$ - 2)[0]; - URIish u = new URIish(text); - if (canHandleProtocol(u)) { - if (Protocol.GIT.handles(u) || Protocol.SSH.handles(u) - || (Protocol.HTTP.handles(u) - || Protocol.HTTPS.handles(u)) - && KnownHosts.isKnownHost(u.getHost()) - || text.endsWith(Constants.DOT_GIT_EXT)) { - preset = text; - } - } + if (text != null) { + text = GitUrlChecker.sanitizeAsGitUrl(text); + if (GitUrlChecker.isValidGitUrl(text)) { + preset = text; } - } catch (URISyntaxException e) { - // ignore, preset is null } clipboard.dispose(); } @@ -435,14 +416,6 @@ public class RepositorySelectionPage extends WizardPage implements IRepositorySe checkPage(); } - private boolean canHandleProtocol(URIish u) { - for (TransportProtocol proto : Transport.getTransportProtocols()) - if (proto.canHandle(u)) - return true; - - return false; - } - private void createRemotePanel(final Composite parent) { remoteButton = new Button(parent, SWT.RADIO); remoteButton @@ -782,7 +755,7 @@ public class RepositorySelectionPage extends WizardPage implements IRepositorySe try { final URIish finalURI = new URIish( - stripGitCloneCommand(uriText.getText())); + GitUrlChecker.sanitizeAsGitUrl(uriText.getText())); String proto = finalURI.getScheme(); if (proto == null && scheme.getSelectionIndex() >= 0) proto = scheme.getItem(scheme.getSelectionIndex()); @@ -884,14 +857,6 @@ public class RepositorySelectionPage extends WizardPage implements IRepositorySe } } - private String stripGitCloneCommand(String input) { - input = input.trim(); - if (input.startsWith(GIT_CLONE_COMMAND_PREFIX)) { - return input.substring(GIT_CLONE_COMMAND_PREFIX.length()).trim(); - } - return input; - } - private boolean setSafePassword(String p) { if ((password == null || password.length() == 0) && p != null && p.length() != 0) { @@ -1027,7 +992,7 @@ public class RepositorySelectionPage extends WizardPage implements IRepositorySe if (eventDepth != 1) return; - String strippedText = stripGitCloneCommand(text); + String strippedText = GitUrlChecker.sanitizeAsGitUrl(text); final URIish u = new URIish(strippedText); if (!text.equals(strippedText)) { uriText.setText(strippedText); -- 2.11.4.GIT