Skip to content

Commit

Permalink
autodetect pasted URL
Browse files Browse the repository at this point in the history
  • Loading branch information
manics committed Dec 15, 2023
1 parent d32fb48 commit d7dd88b
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 17 deletions.
6 changes: 6 additions & 0 deletions binderhub/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
29 changes: 29 additions & 0 deletions binderhub/repoproviders.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class RepoProvider(LoggingConfigurable):
config=True,
)

# Not a traitlet because the class property is serialised in
# config.ConfigHandler.generate_config()
regex_detect = None

unresolved_ref = Unicode()

git_credentials = Unicode(
Expand Down Expand Up @@ -192,6 +196,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/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
]

labels = {
"text": "Fake Provider",
"tag_text": "Fake Ref",
Expand Down Expand Up @@ -627,6 +640,13 @@ def _default_git_credentials(self):
return rf"username=binderhub\npassword={self.private_token}"
return ""

# Gitlab repos can be nested under projects
_regex_detect_base = r"^https://gitlab\.com/(?<repo>[^/]+/[^/]+(/[^/-][^/]+)*)"
regex_detect = [
_regex_detect_base + r"(/-/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
_regex_detect_base + r"(/-/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
]

labels = {
"text": "GitLab.com repository or URL",
"tag_text": "Git ref (branch, tag, or commit)",
Expand Down Expand Up @@ -780,6 +800,11 @@ def _default_git_credentials(self):
return rf"username={self.access_token}\npassword=x-oauth-basic"
return ""

regex_detect = [
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
]

labels = {
"text": "GitHub repository name or URL",
"tag_text": "Git ref (branch, tag, or commit)",
Expand Down Expand Up @@ -973,6 +998,10 @@ 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/(?<repo>[^/]+/[^/]+)(/(?<ref>[^/]+))?$"
]

labels = {
"text": "Gist ID (username/gistId) or URL",
"tag_text": "Git commit SHA",
Expand Down
16 changes: 14 additions & 2 deletions binderhub/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import "../index.css";
import { setUpLog } from "./src/log";
import { updateUrls } from "./src/urls";
import { getBuildFormValues } from "./src/form";
import { updateRepoText } from "./src/repo";
import { detectPastedRepo, updateRepoText } from "./src/repo";

/**
* @type {URL}
Expand Down Expand Up @@ -166,7 +166,19 @@ 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
// We need to wait for the paste to complete before we can read the input field
// https://stackoverflow.com/questions/10972954/javascript-onpaste/10972973#10972973
$("#repository").on("paste", () => {
setTimeout(() => {
detectPastedRepo(BASE_URL).then(() => {
updateUrls(BADGE_BASE_URL);
});
}, 0);
});

$("#repository").on("keyup change", function () {
updateUrls(BADGE_BASE_URL);
});

Expand Down
56 changes: 42 additions & 14 deletions binderhub/static/js/src/repo.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { detect, getRepoProviders } from "@jupyterhub/binderhub-client";
import { updatePathText } from "./path";

/**
* Dict holding cached values of API request to _config endpoint
* @param {Object} configDict Dict holding cached values of API request to _config endpoint
*/
let configDict = {};

function setLabels() {
function setLabels(configDict) {
const provider = $("#provider_prefix").val();
const text = configDict[provider]["text"];
const tagText = configDict[provider]["tag_text"];
Expand All @@ -23,15 +24,42 @@ function setLabels() {
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
*/
export function updateRepoText(baseUrl) {
if (Object.keys(configDict).length === 0) {
const configUrl = new URL("_config", baseUrl);
fetch(configUrl).then((resp) => {
resp.json().then((data) => {
configDict = data;
setLabels();
});
});
} else {
setLabels();
getRepoProviders(baseUrl).then(setLabels);
}

/**
* Attempt to fill in all fields by parsing a pasted repository URL
*
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
*/
export async function detectPastedRepo(baseUrl) {
const repoField = $("#repository").val().trim();
const fields = await detect(baseUrl, repoField);
// Special case: The BinderHub UI supports https://git{hub,lab}.com/ in the
// repository (it's stripped out later in the UI).
// To keep the UI consistent insert it back if it was originally included.
console.log(fields);
if (fields) {
let repo = fields.repository;
if (repoField.startsWith("https://github.com/")) {
repo = "https://github.com/" + repo;
}
if (repoField.startsWith("https://gitlab.com/")) {
repo = "https://gitlab.com/" + repo;
}
$("#provider_prefix-selected").text(fields.providerName);
$("#provider_prefix").val(fields.providerPrefix);
$("#repository").val(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();
updateRepoText(baseUrl);
}
}
67 changes: 67 additions & 0 deletions js/packages/binderhub-client/lib/autodetect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { fetch as fetchPolyfill } from "whatwg-fetch";

// Use native browser fetch if available, and use the polyfill if not available
// (e.g. in tests https://github.com/jestjs/jest/issues/13834#issuecomment-1407375787)
// @todo: this is only a problem in the jest tests, so get rid of this and mock fetch instead
const fetch = window.fetch || fetchPolyfill;

/**
* Dict holding cached values of API request to _config endpoint for base URL
*/
let repoProviders = {};

/**
* Get the repo provider configurations supported by the BinderHub instance
*
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
*/
export async function getRepoProviders(baseUrl) {
if (!repoProviders[baseUrl]) {
const configUrl = new URL("_config", baseUrl);
const resp = await fetch(configUrl);
repoProviders[baseUrl] = resp.json();
}
return repoProviders[baseUrl];
}

/**
* Attempt to parse a string (typically a repository URL) into a BinderHub
* provider/repository/reference/path
*
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
* @param {string} text Repository URL or similar to parse
* @returns {Object} An object if the repository could be parsed with fields
* - providerPrefix Prefix denoting what provider was selected
* - repository Repository to build
* - ref Ref in this repo to build (optional)
* - path Path to launch after this repo has been built (optional)
* - pathType Type of thing to open path with (raw url, notebook file) (optional)
* - providerName User friendly display name of the provider (optional)
* null otherwise
*/
export async function detect(baseUrl, text) {
const config = await getRepoProviders(baseUrl);

for (const provider in config) {
const regex_detect = config[provider].regex_detect || [];
for (const regex of regex_detect) {
const m = text.match(regex);
if (m?.groups.repo) {
return {
providerPrefix: provider,
repository: 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,
providerName: config[provider].display_name,
};
}
}
}

return null;
}
4 changes: 4 additions & 0 deletions js/packages/binderhub-client/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NativeEventSource, EventSourcePolyfill } from "event-source-polyfill";
import { EventIterator } from "event-iterator";

import { detect, getRepoProviders } from "./autodetect";

// Use native browser EventSource if available, and use the polyfill if not available
const EventSource = NativeEventSource || EventSourcePolyfill;

Expand Down Expand Up @@ -211,3 +213,5 @@ export function makeBadgeMarkup(publicBaseUrl, url, syntax) {
);
}
}

export { detect, getRepoProviders };
3 changes: 2 additions & 1 deletion js/packages/binderhub-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"homepage": "https://github.com/jupyterhub/binderhub#readme",
"dependencies": {
"event-iterator": "^2.0.0",
"event-source-polyfill": "^1.0.31",
"event-iterator": "^2.0.0"
"whatwg-fetch": "^3.6.19"
}
}

0 comments on commit d7dd88b

Please sign in to comment.