diff --git a/org.sonatype.m2e.egit.feature/pom.xml b/org.sonatype.m2e.egit.feature/pom.xml index 407120f..dcb2d40 100644 --- a/org.sonatype.m2e.egit.feature/pom.xml +++ b/org.sonatype.m2e.egit.feature/pom.xml @@ -12,7 +12,7 @@ org.sonatype.m2e.egit org.sonatype.m2e.egit.parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT org.sonatype.m2e.egit.feature diff --git a/org.sonatype.m2e.egit.repository/pom.xml b/org.sonatype.m2e.egit.repository/pom.xml index c4b625a..ff7f781 100644 --- a/org.sonatype.m2e.egit.repository/pom.xml +++ b/org.sonatype.m2e.egit.repository/pom.xml @@ -6,7 +6,7 @@ org.sonatype.m2e.egit org.sonatype.m2e.egit.parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT org.sonatype.m2e.egit.repository diff --git a/org.sonatype.m2e.egit.tests/.classpath b/org.sonatype.m2e.egit.tests/.classpath index 798048d..46cec6e 100644 --- a/org.sonatype.m2e.egit.tests/.classpath +++ b/org.sonatype.m2e.egit.tests/.classpath @@ -1,6 +1,6 @@ - + diff --git a/org.sonatype.m2e.egit.tests/META-INF/MANIFEST.MF b/org.sonatype.m2e.egit.tests/META-INF/MANIFEST.MF index 2fb2652..390c6fc 100644 --- a/org.sonatype.m2e.egit.tests/META-INF/MANIFEST.MF +++ b/org.sonatype.m2e.egit.tests/META-INF/MANIFEST.MF @@ -2,13 +2,14 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Maven SCM Handler for EGit Tests Bundle-SymbolicName: org.sonatype.m2e.egit.tests;singleton:=true -Bundle-Version: 0.14.0.qualifier +Bundle-Version: 0.15.0.qualifier Bundle-Vendor: Sonatype -Bundle-RequiredExecutionEnvironment: J2SE-1.5 +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Require-Bundle: org.eclipse.m2e.core, org.eclipse.m2e.scm, org.eclipse.m2e.tests.common, org.eclipse.m2e.maven.runtime, + org.eclipse.jgit;bundle-version="[3.0.0,5.0.0)", org.sonatype.m2e.egit, org.eclipse.core.runtime, org.eclipse.core.resources, diff --git a/org.sonatype.m2e.egit.tests/pom.xml b/org.sonatype.m2e.egit.tests/pom.xml index 6dbfe64..e23d035 100644 --- a/org.sonatype.m2e.egit.tests/pom.xml +++ b/org.sonatype.m2e.egit.tests/pom.xml @@ -12,7 +12,7 @@ org.sonatype.m2e.egit org.sonatype.m2e.egit.parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT org.sonatype.m2e.egit.tests diff --git a/org.sonatype.m2e.egit.tests/src/org/sonatype/m2e/egit/tests/EgitScmHandlerTest.java b/org.sonatype.m2e.egit.tests/src/org/sonatype/m2e/egit/tests/EgitScmHandlerTest.java index f270298..3202b59 100644 --- a/org.sonatype.m2e.egit.tests/src/org/sonatype/m2e/egit/tests/EgitScmHandlerTest.java +++ b/org.sonatype.m2e.egit.tests/src/org/sonatype/m2e/egit/tests/EgitScmHandlerTest.java @@ -9,7 +9,16 @@ package org.sonatype.m2e.egit.tests; import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URISyntaxException; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.m2e.scm.MavenProjectScmInfo; import org.eclipse.ui.PlatformUI; import org.sonatype.m2e.egit.internal.EgitScmHandler; @@ -42,4 +51,118 @@ public void testCheckoutNoMaster() throws Exception { assertWorkspaceProject("git-test"); } + + public void testNormalizeURI() throws Exception { + EgitScmHandlerExt handler = new EgitScmHandlerExt(); + + String uri = EgitScmHandler.GIT_SCM_ID + "git@github.com:errai/errai.git/errai-common"; + + assertEquals("ssh://git@github.com/errai/errai.git", handler.normalizeUri(uri, false)); + assertEquals("git://github.com/errai/errai.git", handler.normalizeUri(uri, true)); + } + + /** + * Authentication success is mocked. + * Expected: + * - no exception, + * - EgitScmHandler.runCloneOperation() is called, + * - EgitScmHandler.onAuthFailed() is not called. + */ + public void testAuthFailed1() throws Exception { + EgitScmHandlerExt handler = new EgitScmHandlerExt(); + String url = EgitScmHandlerExt.GIT_SCM_ID + "git@github.com:errai/errai.git/errai-common"; + MavenProjectScmInfo scmInfo = new MavenProjectScmInfo(url, null, null, "HEAD", url, url); + CoreException exc = null; + try { + handler.checkoutProject(scmInfo, null, new NullProgressMonitor()); + } catch (CoreException e) { + exc = e; + } + assertNull(exc); + assertEquals(1, handler.callsOfrunCloneOperation); + assertEquals(0, handler.callsOfonAuthFailed); + } + + /** + * Authentication failure is mocked, user selects cancel to connect anonymously. + * Expected: + * - no exception, + * - EgitScmHandler.runCloneOperation() is called, + * - EgitScmHandler.onAuthFailed() is called. + */ + public void testAuthFailed2() throws Exception { + EgitScmHandlerExt handler = new EgitScmHandlerExt(true, false); + String url = EgitScmHandlerExt.GIT_SCM_ID + "git@github.com:errai/errai.git/errai-common"; + MavenProjectScmInfo scmInfo = new MavenProjectScmInfo(url, null, null, "HEAD", url, url); + CoreException exc = null; + try { + handler.checkoutProject(scmInfo, null, new NullProgressMonitor()); + } catch (CoreException e) { + exc = e; + } + assertNull(exc); + assertEquals(1, handler.callsOfrunCloneOperation); + assertEquals(1, handler.callsOfonAuthFailed); + } + + /** + * Authentication failure is mocked, user selects ok to connect anonymously. + * Expected: + * - no exception, + * - EgitScmHandler.runCloneOperation() is called twice, + * - EgitScmHandler.onAuthFailed() is called. + */ + public void testAuthFailed3() throws Exception { + EgitScmHandlerExt handler = new EgitScmHandlerExt(true, true); + String url = EgitScmHandlerExt.GIT_SCM_ID + "git@github.com:errai/errai.git/errai-common"; + MavenProjectScmInfo scmInfo = new MavenProjectScmInfo(url, null, null, "HEAD", url, url); + CoreException exc = null; + try { + handler.checkoutProject(scmInfo, null, new NullProgressMonitor()); + } catch (CoreException e) { + exc = e; + } + assertNull(exc); + assertEquals(2, handler.callsOfrunCloneOperation); + assertEquals(1, handler.callsOfonAuthFailed); + } + + class EgitScmHandlerExt extends EgitScmHandler { + boolean throwAuthFailed = false; + boolean accessAnonymously = false; + + int callsOfonAuthFailed = 0; + int callsOfrunCloneOperation = 0; + + EgitScmHandlerExt() {} + + EgitScmHandlerExt(boolean throwAuthFailed, boolean accessAnonymously) { + this.throwAuthFailed = throwAuthFailed; + this.accessAnonymously = accessAnonymously; + } + + //Make normalizeUri(String, boolean) accessible. + @Override + public String normalizeUri(String uri, boolean avoidSSH) throws URISyntaxException { + return super.normalizeUri(uri, avoidSSH); + } + + //Mock super + @Override + protected void runCloneOperation(URIish uri, File location, String refName, SubMonitor pm) + throws InvocationTargetException, IOException, InterruptedException { + callsOfrunCloneOperation++; + if(throwAuthFailed) { + throwAuthFailed = false; //next time run smoothly. + throw new InvocationTargetException(new TransportException("Auth failed")); + } + } + + //Mock super + @Override + protected boolean onAuthFailed() { + callsOfonAuthFailed++; + return accessAnonymously; + } + } } diff --git a/org.sonatype.m2e.egit/.classpath b/org.sonatype.m2e.egit/.classpath index 798048d..46cec6e 100644 --- a/org.sonatype.m2e.egit/.classpath +++ b/org.sonatype.m2e.egit/.classpath @@ -1,6 +1,6 @@ - + diff --git a/org.sonatype.m2e.egit/META-INF/MANIFEST.MF b/org.sonatype.m2e.egit/META-INF/MANIFEST.MF index bfe8475..c01abfa 100644 --- a/org.sonatype.m2e.egit/META-INF/MANIFEST.MF +++ b/org.sonatype.m2e.egit/META-INF/MANIFEST.MF @@ -2,9 +2,9 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Bundle-Name Bundle-SymbolicName: org.sonatype.m2e.egit;singleton:=true -Bundle-Version: 0.14.0.qualifier +Bundle-Version: 0.15.0.qualifier Bundle-Vendor: %Bundle-Vendor -Bundle-RequiredExecutionEnvironment: J2SE-1.5 +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Require-Bundle: org.eclipse.m2e.core;bundle-version="[1.0.0,2.0.0)", org.eclipse.m2e.scm;bundle-version="[1.0.0,2.0.0)", org.eclipse.m2e.core.ui;bundle-version="[1.0.0,2.0.0)", diff --git a/org.sonatype.m2e.egit/pom.xml b/org.sonatype.m2e.egit/pom.xml index 0504e55..623229f 100644 --- a/org.sonatype.m2e.egit/pom.xml +++ b/org.sonatype.m2e.egit/pom.xml @@ -12,7 +12,7 @@ org.sonatype.m2e.egit org.sonatype.m2e.egit.parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT org.sonatype.m2e.egit diff --git a/org.sonatype.m2e.egit/src/org/sonatype/m2e/egit/internal/EgitScmHandler.java b/org.sonatype.m2e.egit/src/org/sonatype/m2e/egit/internal/EgitScmHandler.java index b4a6b99..490e4bd 100644 --- a/org.sonatype.m2e.egit/src/org/sonatype/m2e/egit/internal/EgitScmHandler.java +++ b/org.sonatype.m2e.egit/src/org/sonatype/m2e/egit/internal/EgitScmHandler.java @@ -20,14 +20,19 @@ import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.egit.core.op.CloneOperation; +import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.transport.URIish; import org.eclipse.m2e.scm.MavenProjectScmInfo; import org.eclipse.m2e.scm.spi.ScmHandler; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,25 +53,41 @@ public void checkoutProject(MavenProjectScmInfo info, File location, IProgressMo log.debug("Checking out project from {} to {}", info, location); SubMonitor pm = SubMonitor.convert(monitor, 100); - try { - URIish uri = getUri(info); - - String refName = getRefName(info); - CloneOperation clone = new CloneOperation(uri, true /* allSelected */, new ArrayList(), location, refName, - "origin", getTimeout()); - clone.run(pm.newChild(99)); - - fixAutoCRLF(clone.getGitDir()); - } catch(InvocationTargetException e) { - Throwable cause = e.getTargetException(); - throw new CoreException(new Status(IStatus.ERROR, getClass().getName(), cause.getMessage(), cause)); - } catch(IOException e) { - throw new CoreException(new Status(IStatus.ERROR, getClass().getName(), e.getMessage(), e)); - } catch(URISyntaxException e) { - throw new CoreException(new Status(IStatus.ERROR, getClass().getName(), e.getMessage(), e)); - } catch(InterruptedException e) { - // The monitor was canceled + try { + boolean avoidSSH = false; + URIish uri = null; + boolean repeat = true; + //The cycle will run maximum twice, first time with avoidSSH = false, + //second, if user chooses it, with avoidSSH = true. + while(repeat) { + repeat = false; + try { + uri = getUri(info, avoidSSH); + String refName = getRefName(info); + runCloneOperation(uri, location, refName, pm); + // + break; + } catch(InvocationTargetException e) { + Throwable cause = e.getTargetException(); + if(!avoidSSH && uri != null && "ssh".equals(uri.getScheme()) && cause instanceof TransportException) { + boolean accessGitAnonimously = onAuthFailed(); + if(accessGitAnonimously) { + avoidSSH = true; + repeat = true; + continue; + } else { + pm.setCanceled(true); + break; + } + } + throw new CoreException(new Status(IStatus.ERROR, getClass().getName(), cause.getMessage(), cause)); + } catch(IOException | URISyntaxException e) { + throw new CoreException(new Status(IStatus.ERROR, getClass().getName(), e.getMessage(), e)); + } catch(InterruptedException e) { + // The monitor was canceled + } + } } finally { pm.done(); } @@ -76,9 +97,32 @@ protected int getTimeout() { return 30; } - protected URIish getUri(MavenProjectScmInfo info) throws URISyntaxException { + protected void runCloneOperation(URIish uri, File location, String refName, SubMonitor pm) + throws InvocationTargetException, IOException, InterruptedException { + CloneOperation clone = new CloneOperation(uri, true /* allSelected */, new ArrayList(), location, refName, + "origin", getTimeout()); + clone.run(pm.newChild(99)); + + fixAutoCRLF(clone.getGitDir()); + } + + protected boolean onAuthFailed() { + final boolean[] result = new boolean[]{false}; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + String title = "Authentication failed"; + String message = "The clone URL uses the SSH protocol. It seems you do not have a valid SSH key. Do you want to continue with the GIT protocol anonymously?"; + result[0] = MessageDialog.openConfirm(shell, title, message); + } + }); + return result[0]; + } + + protected URIish getUri(MavenProjectScmInfo info, boolean avoidSSH) throws URISyntaxException { String url = info.getRepositoryUrl(); - url = normalizeUri(url); + url = normalizeUri(url, avoidSSH); URIish uri = new URIish(url); @@ -91,7 +135,7 @@ protected URIish getUri(MavenProjectScmInfo info) throws URISyntaxException { return uri; } - protected String normalizeUri(String uri) throws URISyntaxException { + protected String normalizeUri(String uri, boolean avoidSSH) throws URISyntaxException { if(!uri.startsWith(GIT_SCM_ID)) { return uri; } @@ -101,6 +145,27 @@ protected String normalizeUri(String uri) throws URISyntaxException { throw new URISyntaxException(uri, "Invalid git URI"); } + if(avoidSSH) { + String gitPrefix = "git://"; + //Replace @ with :// + if(uri.startsWith("git@")) { + uri = gitPrefix + uri.substring(4); + } + //Replace ':' after host with '/' for git + if(uri.startsWith(gitPrefix)) { + int slash = uri.indexOf("/", gitPrefix.length()); + int colon = uri.indexOf(":", gitPrefix.length()); + if(colon > 0 && slash > colon) { + uri = uri.substring(0, colon) + "/" + uri.substring(colon + 1); + } + } + } + //3. Remove tail after .git + int dotGit = uri.indexOf(".git"); + if(dotGit >= 0 && uri.length() > dotGit + 4) { + uri = uri.substring(0, dotGit + 4); + } + URIish gitUri = new URIish(uri); if(gitUri.getScheme() == null) { if(gitUri.getHost() == null || "file".equals(gitUri.getHost())) { diff --git a/pom.xml b/pom.xml index 1529895..ee49b6b 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ org.sonatype.m2e.egit org.sonatype.m2e.egit.parent - 0.14.0-SNAPSHOT + 0.15.0-SNAPSHOT pom Maven SCM Handler for EGit Parent