From f2e90f62ce23052602e86a877eb5ddef7638a6dc Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 27 Dec 2017 14:45:03 -0800 Subject: [PATCH] Reconciling for github repo uris in concourse pipelines --- .../vscode/concourse/PipelineYmlSchema.java | 2 + .../concourse/PipelineYmlSchemaProblems.java | 3 +- .../github/DefaultGithubInfoProvider.java | 73 ++++++-- .../concourse/github/GithubInfoProvider.java | 16 +- .../github/GithubRepoContentAssistant.java | 16 +- .../concourse/github/GithubValueParsers.java | 103 ++++++++++++ .../vscode/concourse/ConcourseEditorTest.java | 158 ++++++++++++++++-- 7 files changed, 329 insertions(+), 42 deletions(-) create mode 100644 headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubValueParsers.java diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 8b722e897..5757d21a9 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -49,6 +49,7 @@ import org.springframework.ide.vscode.concourse.ConcourseModel.ResourceModel; import org.springframework.ide.vscode.concourse.ConcourseModel.StepModel; import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; import org.springframework.ide.vscode.concourse.github.GithubRepoContentAssistant; +import org.springframework.ide.vscode.concourse.github.GithubValueParsers; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; @@ -424,6 +425,7 @@ public class PipelineYmlSchema implements YamlSchema { { AbstractType t_git_repo_uri = f.yatomic("GitRepoUri"); t_git_repo_uri.setCustomContentAssistant(new GithubRepoContentAssistant(github)); + t_git_repo_uri.parseWith(GithubValueParsers.uri(github)); AbstractType source = f.ybean("GitSource"); addProp(source, "uri", t_git_repo_uri).isPrimary(true); diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchemaProblems.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchemaProblems.java index f4e3569dc..1deff641f 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchemaProblems.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchemaProblems.java @@ -16,5 +16,6 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.*; public class PipelineYmlSchemaProblems { - protected static final ProblemType UNUSED_RESOURCE = problemType("PipelineYamlUnusedResource", ProblemSeverity.ERROR); + public static final ProblemType UNUSED_RESOURCE = problemType("PipelineYamlUnusedResource", ProblemSeverity.ERROR); + public static final ProblemType UNKNOWN_GITHUB_ENTITY = problemType("UnknownGithubEntitity", ProblemSeverity.WARNING); } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java index 67f122405..e27068c1a 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java @@ -13,12 +13,15 @@ package org.springframework.ide.vscode.concourse.github; import java.io.File; import java.io.IOException; import java.util.Collection; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import org.kohsuke.github.GHFileNotFoundException; import org.kohsuke.github.GHPerson; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.springframework.ide.vscode.commons.util.CollectorUtil; +import org.springframework.ide.vscode.commons.util.ExceptionUtil; import org.springframework.ide.vscode.commons.util.Log; import com.google.common.cache.Cache; @@ -36,11 +39,39 @@ public class DefaultGithubInfoProvider implements GithubInfoProvider { //especially when connecting fails. So that user may try to address the //issue and try again. + private static class Result { + private Object result; + + public Result(Object valueOrTrowable) { + this.result = valueOrTrowable; + } + + @SuppressWarnings("unchecked") + public T get() throws Exception { + if (result instanceof Throwable) { + throw ExceptionUtil.exception((Throwable) result); + } + return (T)result; + } + } + + private Callable> loader(Callable callable) { + return () -> load(callable); + } + + private static Result load(Callable callable) { + try { + return new Result<>(callable.call()); + } catch (Throwable e) { + return new Result<>(e); + } + } + private GitHub github; private IOException connectionError; private Collection owners; - private Cache> reposByOwner = CacheBuilder.newBuilder() + private Cache>> reposByOwner = CacheBuilder.newBuilder() .expireAfterAccess(10, TimeUnit.MINUTES) .build(); @@ -94,17 +125,23 @@ public class DefaultGithubInfoProvider implements GithubInfoProvider { } try { if (github!=null) { - return reposByOwner.get(ownerName, () -> { + return reposByOwner.get(ownerName, loader(() -> { GHPerson owner = getOwner(ownerName); - return Flux.fromIterable(owner.listRepositories()) - .filter(repo -> repo.getOwnerName().equals(ownerName)) - .map(GHRepository::getName) - .collect(CollectorUtil.toImmutableSet()) - .block(); - }); + if (owner!=null) { + return Flux.fromIterable(owner.listRepositories()) + .filter(repo -> repo.getOwnerName().equals(ownerName)) + .map(GHRepository::getName) + .collect(CollectorUtil.toImmutableSet()) + .block(); + } + return null; + })) + .get(); } } catch (Exception e) { - Log.log(e); + if (!isMissingOwnerException(e)) { + Log.log(e); + } } return ImmutableList.of(); } @@ -112,9 +149,23 @@ public class DefaultGithubInfoProvider implements GithubInfoProvider { private GHPerson getOwner(String ownerName) throws IOException { try { return github.getUser(ownerName); - } catch (IOException e) { - return github.getOrganization(ownerName); + } catch (IOException e1) { + if (isMissingOwnerException(e1)) { + try { + return github.getOrganization(ownerName); + } catch (IOException e2) { + if (isMissingOwnerException(e2)) { + return null; + } + throw e2; + } + } + throw e1; } } + protected boolean isMissingOwnerException(Throwable e) { + return e instanceof GHFileNotFoundException; + } + } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java index b23f3dac4..80683f665 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java @@ -17,12 +17,20 @@ public interface GithubInfoProvider { /** * Retrieves a list of owners that can be suggested as hints in completions of github repo urls. *

- * Note: since github has millions of users and fetching all of them isn't really an option, the owners - * are expected to be somehow limited based on the user credentials (i.e. returning a list that is - * deemed relevant to the current logged in user, rather than an exhaustive list of every org and user name - * on github. + * Note: This list can not be used to determine whether a given owner exists. Github has millions of users + * and fetching all of them isn't really an option, therefore the owners are expected to be somehow limited + * based on the user credentials (i.e. returning a list that is deemed relevant to the current logged in user, + * rather than an exhaustive list of every org and user name on github. */ Collection getOwners() throws Exception; + + /** + * Fetch information about the repos owned by a given user or org. + *

+ * Should return null rather than throw an exception for the case + * where the owner did not exist. The caller can make use of this + * to determine implicitly whether a given owner is valid. + */ Collection getReposForOwner(String owner) throws Exception; } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java index 606f30181..bbc64b107 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java @@ -13,25 +13,21 @@ package org.springframework.ide.vscode.concourse.github; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.completion.SimpleCompletionFactory; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; -import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.ExceptionUtil; import org.springframework.ide.vscode.commons.util.FuzzyMatcher; import org.springframework.ide.vscode.commons.yaml.completion.CompletionFactory; import org.springframework.ide.vscode.commons.yaml.schema.ISubCompletionEngine; -import org.eclipse.lsp4j.CompletionItemKind; - import com.google.common.collect.ImmutableList; public class GithubRepoContentAssistant implements ISubCompletionEngine { - private String[] uriPrefixes = { + public static final String[] URI_PREFIXES = { "git@github.com:", "https://github.com/" }; @@ -45,14 +41,14 @@ public class GithubRepoContentAssistant implements ISubCompletionEngine { public List getCompletions(CompletionFactory f, DocumentRegion region, int offset) { DocumentRegion query = region.subSequence(0, offset); //If uri prefix is already there, we provide CA for owner / repo - for (String uriPrefix : uriPrefixes) { + for (String uriPrefix : URI_PREFIXES) { if (query.startsWith(uriPrefix)) { return getOwnerOrRepoCompletions(f, query.subSequence(uriPrefix.length())); } } //If uri prefix is not yet there, maybe we can suggest it (if it matches the query) - List proposals = new ArrayList<>(uriPrefixes.length); - for (String uriPrefix : uriPrefixes) { + List proposals = new ArrayList<>(URI_PREFIXES.length); + for (String uriPrefix : URI_PREFIXES) { if (FuzzyMatcher.matchScore(query, uriPrefix)!=0.0) { proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, uriPrefix, null, null)); } @@ -93,7 +89,7 @@ public class GithubRepoContentAssistant implements ISubCompletionEngine { List proposals = new ArrayList<>(repos.size()); for (String repo : repos) { if (FuzzyMatcher.matchScore(query, repo)!=0.0) { - proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, repo, null, null)); + proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, repo+".git", null, null)); } } return proposals; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubValueParsers.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubValueParsers.java new file mode 100644 index 000000000..063306929 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubValueParsers.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * 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: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.concourse.github; + +import java.util.Collection; + +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException; +import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; +import org.springframework.ide.vscode.commons.util.ValueParseException; +import org.springframework.ide.vscode.commons.util.ValueParser; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.concourse.PipelineYmlSchemaProblems; + +public class GithubValueParsers { + + public static class GithubRepoReference { + public final DocumentRegion owner; + public final DocumentRegion name; + public GithubRepoReference(DocumentRegion owner, DocumentRegion name) { + super(); + this.owner = owner; + this.name = name; + } + } + + private static ReconcileException unknownEntity(String msg, DocumentRegion highlight) { + return new ReconcileException(msg, PipelineYmlSchemaProblems.UNKNOWN_GITHUB_ENTITY, highlight.getStart(), highlight.getEnd()); + } + + public static ValueParser uri(GithubInfoProvider github) { + + return new ValueParser() { + + @Override public GithubRepoReference parse(String _str) throws Exception { + TextDocument doc = new TextDocument(null, LanguageId.PLAINTEXT); + doc.setText(_str); + DocumentRegion str = new DocumentRegion(doc); + GithubRepoReference repo = parseFormat(str); + if (repo!=null) { + Collection knownRepos; + try { + knownRepos = github.getReposForOwner(repo.owner.toString()); + } catch (Exception e) { + //Couldn't read info from github. Ignore this in reconciler context. + return repo; + } + if (knownRepos==null) { + throw unknownEntity("User or Organization not found: '"+repo.owner+"'", repo.owner); + } else { + if (!knownRepos.contains(repo.name.toString())) { + throw unknownEntity("Repo not found: '"+repo.name+"'", repo.name); + } + } + } + return repo; + } + + private GithubRepoReference parseFormat(DocumentRegion str) throws Exception { + String prefix = checkPrefix(str); + if (prefix!=null) { + DocumentRegion ownerAndName = str.subSequence(prefix.length()); + //Should end with '.git' + if (ownerAndName.endsWith(".git")) { + ownerAndName = ownerAndName.subSequence(0, ownerAndName.length()-4); + } else { + DocumentRegion highlight = ownerAndName.textAtEnd(1); + throw new ValueParseException("GitHub repo uri should end with '.git'", highlight.getStart(), highlight.getEnd()); + } + int slash = ownerAndName.indexOf('/'); + if (slash>=0) { + return new GithubRepoReference(ownerAndName.subSequence(0, slash), ownerAndName.subSequence(slash+1)); + } else { + throw new ValueParseException("Expecting something of the form '${owner}/${repo}'.", ownerAndName.getStart(), ownerAndName.getEnd()); + } + } + return null; + } + + private String checkPrefix(DocumentRegion str) throws ValueParseException { + for (String expectedPrefix : GithubRepoContentAssistant.URI_PREFIXES) { + int lastChar = expectedPrefix.length()-1; + if (str.startsWith(expectedPrefix.substring(0, lastChar))) { + if (str.charAt(lastChar)==expectedPrefix.charAt(lastChar)) { + return expectedPrefix; + } + throw new ValueParseException("Expecting a '"+expectedPrefix.charAt(lastChar)+"'", lastChar, lastChar+1); + } + } + return null; + } + }; + } + +} diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 9eb954967..9305d0f90 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.concourse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import static org.springframework.ide.vscode.languageserver.testharness.Editor.INDENTED_COMPLETION; import static org.springframework.ide.vscode.languageserver.testharness.Editor.PLAIN_COMPLETION; @@ -30,18 +31,21 @@ import org.eclipse.lsp4j.DiagnosticSeverity; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity; import org.springframework.ide.vscode.commons.util.IOUtil; import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; +import org.springframework.ide.vscode.concourse.github.GithubRepoContentAssistant; import org.springframework.ide.vscode.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; import org.springframework.ide.vscode.languageserver.testharness.SynchronizationPoint; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import org.mockito.Mockito; @@ -220,6 +224,7 @@ public class ConcourseEditorTest { public void reconcileAcceptsSensiblePipelineFile() throws Exception { Editor editor; + when(github.getReposForOwner("spring-projects")).thenReturn(ImmutableSet.of("sts4")); editor = harness.newEditor( getClasspathResourceText("workspace/pipeline.yml") ); @@ -643,7 +648,7 @@ public class ConcourseEditorTest { "- name: sts4\n" + " type: git\n" + " source:\n" + - " uri: https://github.com/kdvolder/somestuff\n" + + " uri: https://someplace.com/kdvolder/somestuff\n" + " branch: master\n" + "jobs:\n" + "- name: job1\n" + @@ -848,14 +853,14 @@ public class ConcourseEditorTest { " type: git\n" + " source:\n" + " branch: master\n" + - " uri: https://github.com/kdvolder/my-repo\n" + + " uri: https://someplace.com/kdvolder/my-repo\n" + "resources:\n" + "- name: your-repo\n" + " type: git\n" + " type: git\n" + " source:\n" + " branch: master\n" + - " uri: https://github.com/kdvolder/forked-repo\n" + " uri: https://someplace.com/kdvolder/forked-repo\n" ); editor.assertProblems( @@ -1049,7 +1054,7 @@ public class ConcourseEditorTest { "- name: sts4-out\n" + " type: git\n" + " source:\n" + - " uri: git@github.com:spring-projects/sts4.git\n" + + " uri: git@someplace.com:spring-projects/sts4.git\n" + " bogus: bad\n" + " branch: {{branch}}\n" + " private_key: {{rsa_id}}\n" + @@ -1564,7 +1569,7 @@ public class ConcourseEditorTest { "- name: repo\n" + " type: git\n" + " source:\n" + - " uri: git@github.com/johny-coder/test-repo\n" + + " uri: git@someplace.com:johny-coder/test-repo.git\n" + "jobs:\n" + "- name: do-stuff\n" + " plan:\n" + @@ -1580,7 +1585,7 @@ public class ConcourseEditorTest { "- name: repo\n" + " type: git\n" + " source:\n" + - " uri: git@github.com/johny-coder/test-repo\n" + + " uri: git@github.com:johny-coder/test-repo\n" + "jobs:\n" + "- name: do-stuff\n" + " plan:\n" + @@ -3929,7 +3934,7 @@ public class ConcourseEditorTest { " type: git\n" + " source:\n" + " branch: master\n" + - " uri: git@github.com/blah\n" + + " uri: git@someplace.com:blah/blah.git\n" + "jobs:\n" + "- name: build-it\n" + " plan:\n" + @@ -3949,7 +3954,7 @@ public class ConcourseEditorTest { " - name: cf-networking-dev\n" + " type: git\n" + " source:\n" + - " uri: git@github.com:cloudfoundry-incubator/cf-networking-release.git\n" + + " uri: git@someplace.com:cloudfoundry-incubator/cf-networking-release.git\n" + " branch: develop\n" + " ignore_paths:\n" + " - docs\n" + @@ -4443,23 +4448,23 @@ public class ConcourseEditorTest { " uri: <*>" ); editor.assertContextualCompletions("https://github.com/the-owner/<*>", - "https://github.com/the-owner/cool-project<*>", - "https://github.com/the-owner/good-stuff<*>", - "https://github.com/the-owner/nice-repo<*>" + "https://github.com/the-owner/cool-project.git<*>", + "https://github.com/the-owner/good-stuff.git<*>", + "https://github.com/the-owner/nice-repo.git<*>" ); editor.assertContextualCompletions("https://github.com/the-owner/proj<*>", - "https://github.com/the-owner/cool-project<*>" + "https://github.com/the-owner/cool-project.git<*>" ); editor.assertContextualCompletions("git@github.com:the-owner/<*>", - "git@github.com:the-owner/cool-project<*>", - "git@github.com:the-owner/good-stuff<*>", - "git@github.com:the-owner/nice-repo<*>" + "git@github.com:the-owner/cool-project.git<*>", + "git@github.com:the-owner/good-stuff.git<*>", + "git@github.com:the-owner/nice-repo.git<*>" ); editor.assertContextualCompletions("git@github.com:the-owner/proj<*>", - "git@github.com:the-owner/cool-project<*>" + "git@github.com:the-owner/cool-project.git<*>" ); } @@ -4485,6 +4490,127 @@ public class ConcourseEditorTest { editor.assertCompletionLabels("Explain some stuff"); } + + @Test public void noNetworkGithubUriReconciling() throws Exception { + //When not plugged into the network (i.e github api returns errors) + // we consider the repos unknowable and will not warn about anything. + when(github.getReposForOwner("the-owner")).thenThrow(new Exception("Some problem talking to github")); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:the-owner/bad-project.git\n" + + "- name: other-repo\n" + + " type: git\n" + + " source:\n" + + " uri: https://github.com/the-owner/wrong-project.git\n" + ); + editor.assertProblems( + "my-repo|Unused", + "other-repo|Unused" + ); + } + + @Test public void nonGithubUriReconciling() throws Exception { + //We only care about gihub uris. So, ignore anything else for reconciler. + when(github.getReposForOwner("the-owner")).thenReturn(ImmutableList.of( + "nice-repo", "cool-project", "good-stuff" + )); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@somehost.somewhere:the-owner/bad-project\n" + + "- name: other-repo\n" + + " type: git\n" + + " source:\n" + + " uri: https://somehost.somewhere/the-owner/wrong-project\n" + ); + editor.assertProblems( + "my-repo|Unused", + "other-repo|Unused" + ); + } + + @Test public void githubWrongFormatReconciling() throws Exception { + when(github.getReposForOwner("the-owner")).thenReturn(ImmutableList.of( + "nice-repo", "cool-project", "good-stuff" + )); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:ssh-just-owner\n" + + "- name: my-repo2\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:ssh-just-owner.git\n" + + "- name: other-repo\n" + + " type: git\n" + + " source:\n" + + " uri: https://github.com/https-just-owner\n" + + "- name: other-repo2\n" + + " type: git\n" + + " source:\n" + + " uri: https://github.com/https-just-owner.git\n" + + "- name: different-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com/ssh/wrong-separator.git\n" + + "- name: one-more-repo\n" + + " type: git\n" + + " source:\n" + + " uri: https://github.com:https/wrong-separator.git\n" + ); + editor.ignoreProblem(PipelineYmlSchemaProblems.UNUSED_RESOURCE); + editor.assertProblems( + "uri: git@github.com:ssh-just-owne^r^\n|should end with '.git'", + "ssh-just-owner|Expecting something of the form '${owner}/${repo}'", + "uri: https://github.com/https-just-owne^r^\n|should end with '.git'", + "https-just-owner|Expecting something of the form '${owner}/${repo}'", + "git@github.com^/^ssh/wrong-separator|Expecting a ':'", + "https://github.com^:^https/wrong-separator|Expecting a '/'" + ); + } + + @Test public void githubUriReconciling() throws Exception { + when(github.getReposForOwner("the-owner")).thenReturn(ImmutableList.of( + "nice-repo", "cool-project", "good-stuff" + )); + when(github.getReposForOwner("owner-no-exist")).thenReturn(null); + + String editorText = + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: $$github$$the-owner/cool-project.git\n" + + "- name: other-repo\n" + + " type: git\n" + + " source:\n" + + " uri: $$github$$the-owner/repo-no-exist.git\n" + + "- name: different-repo\n" + + " type: git\n" + + " source:\n" + + " uri: $$github$$owner-no-exist/who-cares.git\n" + ; + + for (String githubUriPrefix : GithubRepoContentAssistant.URI_PREFIXES) { + Editor editor = harness.newEditor(editorText.replace("$$github$$", githubUriPrefix)); + editor.ignoreProblem(PipelineYmlSchemaProblems.UNUSED_RESOURCE); + List problems = editor.assertProblems( + "repo-no-exist|Repo not found: 'repo-no-exist'", + "owner-no-exist|User or Organization not found: 'owner-no-exist'" + ); + for (Diagnostic d : problems) { + assertEquals(DiagnosticSeverity.Warning, d.getSeverity()); + } + } + } + ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {