diff --git a/binderhub/build.py b/binderhub/build.py index 0cbf82762..12f0221c5 100644 --- a/binderhub/build.py +++ b/binderhub/build.py @@ -455,15 +455,10 @@ def get_builder_volumes(self): return volumes, volume_mounts - def submit(self): + def get_env(self): """ - Submit a build pod to create the image for the repository. - - Progress of the build can be monitored by listening for items in - the Queue passed to the constructor as `q`. + Get the Kubernetes environment variables for the build pod. """ - volumes, volume_mounts = self.get_builder_volumes() - env = [ client.V1EnvVar(name=key, value=value) for key, value in self.extra_envs.items() @@ -472,7 +467,6 @@ def submit(self): env.append( client.V1EnvVar(name="GIT_CREDENTIAL_ENV", value=self.git_credentials) ) - if self.registry_credentials: env.append( client.V1EnvVar( @@ -480,8 +474,16 @@ def submit(self): value=json.dumps(self.registry_credentials), ) ) + return env + + def make_pod(self): + """ + Make a Kubernetes pod specification for the build pod. + """ + volumes, volume_mounts = self.get_builder_volumes() + env = self.get_env() - self.pod = client.V1Pod( + pod = client.V1Pod( metadata=client.V1ObjectMeta( name=self.name, labels={ @@ -528,6 +530,16 @@ def submit(self): affinity=self.get_affinity(), ), ) + return pod + + def submit(self): + """ + Submit a build pod to create the image for the repository. + + Progress of the build can be monitored by listening for items in + the Queue passed to the constructor as `q`. + """ + self.pod = self.make_pod() try: _ = self.api.create_namespaced_pod( diff --git a/binderhub/config.py b/binderhub/config.py index 3fd655839..df152c88d 100644 --- a/binderhub/config.py +++ b/binderhub/config.py @@ -10,6 +10,12 @@ def generate_config(self): "repo_providers" ].items(): config[repo_provider_class_alias] = repo_provider_class.labels + config[repo_provider_class_alias][ + "display_name" + ] = repo_provider_class.display_name + config[repo_provider_class_alias][ + "regex_detect" + ] = repo_provider_class.regex_detect return config async def get(self): diff --git a/binderhub/repoproviders.py b/binderhub/repoproviders.py index 38fb05de1..de66fd670 100644 --- a/binderhub/repoproviders.py +++ b/binderhub/repoproviders.py @@ -99,6 +99,13 @@ class RepoProvider(LoggingConfigurable): config=True, ) + regex_detect = List( + [], + help=""" + List of regexes to detect repository parameters from URLs. + """, + ) + unresolved_ref = Unicode() git_credentials = Unicode( @@ -192,6 +199,15 @@ def is_valid_sha1(sha1): class FakeProvider(RepoProvider): """Fake provider for local testing of the UI""" + name = Unicode("Fake") + + display_name = "Fake GitHub" + + regex_detect = [ + r"^https://github.com/(?[^/]+/[^/]+)(/blob/(?[^/]+)(/(?.+))?)?$", + r"^https://github.com/(?[^/]+/[^/]+)(/tree/(?[^/]+)(/(?.+))?)?$", + ] + labels = { "text": "Fake Provider", "tag_text": "Fake Ref", @@ -627,6 +643,11 @@ def _default_git_credentials(self): return rf"username=binderhub\npassword={self.private_token}" return "" + regex_detect = [ + r"^https://gitlab.com/(?[^/]+/[^/]+)(/-/blob/(?[^/]+)(/(?.+))?)?$", + r"^https://gitlab.com/(?[^/]+/[^/]+)(/-/tree/(?[^/]+)(/(?.+))?)?$", + ] + labels = { "text": "GitLab.com repository or URL", "tag_text": "Git ref (branch, tag, or commit)", @@ -780,6 +801,11 @@ def _default_git_credentials(self): return rf"username={self.access_token}\npassword=x-oauth-basic" return "" + regex_detect = [ + r"^https://github.com/(?[^/]+/[^/]+)(/blob/(?[^/]+)(/(?.+))?)?$", + r"^https://github.com/(?[^/]+/[^/]+)(/tree/(?[^/]+)(/(?.+))?)?$", + ] + labels = { "text": "GitHub repository name or URL", "tag_text": "Git ref (branch, tag, or commit)", @@ -973,6 +999,8 @@ class GistRepoProvider(GitHubRepoProvider): help="Flag for allowing usages of secret Gists. The default behavior is to disallow secret gists.", ) + regex_detect = [r"^https://gist.github.com/(?[^/]+/[^/]+)(/(?[^/]+))?$"] + labels = { "text": "Gist ID (username/gistId) or URL", "tag_text": "Git commit SHA", diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 536a4e278..40ceac73b 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -7,6 +7,7 @@ import { BinderRepository } from "@jupyterhub/binderhub-client"; import { updatePathText } from "./src/path"; import { nextHelpText } from "./src/loading"; import { updateFavicon } from "./src/favicon"; +import { detect } from "./src/autodetect"; import "xterm/css/xterm.css"; @@ -166,7 +167,39 @@ function indexMain() { updatePathText(); updateRepoText(BASE_URL); - $("#repository").on("keyup paste change", function () { + // If the user pastes a URL into the repository field try to autodetect + // In all other cases don't do anything to avoid overwriting the user's input + $("#repository").on("paste", function (ev) { + const event = ev.originalEvent; + const pastedText = (event.clipboardData || window.clipboardData) + ?.getData("text") + .trim(); + if (pastedText) { + const fields = detect(pastedText); + console.log(fields); + if (fields) { + $("#provider_prefix-selected").text(fields.display_name); + $("#provider_prefix").val(fields.provider); + $("#repository").val(fields.repo); + if (fields.ref) { + $("#ref").val(fields.ref); + } + if (fields.path) { + $("#filepath").val(fields.path); + $("#url-or-file-selected").text( + fields.pathType === "filepath" ? "File" : "URL", + ); + } + updatePathText(); + event.preventDefault(); + updateRepoText(BASE_URL); + } + } + // else autodetect failed, let the paste go through + updateUrls(BADGE_BASE_URL); + }); + + $("#repository").on("keyup change", function () { updateUrls(BADGE_BASE_URL); }); diff --git a/binderhub/static/js/src/autodetect.js b/binderhub/static/js/src/autodetect.js new file mode 100644 index 000000000..365f961e5 --- /dev/null +++ b/binderhub/static/js/src/autodetect.js @@ -0,0 +1,27 @@ +import { getConfigDict } from "./repo"; + +export function detect(text) { + // Assumes configDict was already loaded by another module + const configDict = getConfigDict(); + for (const provider in configDict) { + const regex_detect = configDict[provider].regex_detect || []; + for (const regex of regex_detect) { + const m = text.match(regex); + if (m?.groups.repo) { + return { + provider: provider, + display_name: configDict[provider].display_name, + repo: m.groups.repo, + ref: m.groups.ref, + path: m.groups.filepath || m.groups.urlpath || null, + pathType: m.groups.filepath + ? "filepath" + : m.groups.urlpath + ? "urlpath" + : null, + }; + } + } + } + return null; +} diff --git a/binderhub/static/js/src/repo.js b/binderhub/static/js/src/repo.js index e848b49ed..5bb701506 100644 --- a/binderhub/static/js/src/repo.js +++ b/binderhub/static/js/src/repo.js @@ -35,3 +35,7 @@ export function updateRepoText(baseUrl) { setLabels(); } } + +export function getConfigDict() { + return configDict; +}