Skip to content

Commit 00eecd7

Browse files
committed
autodetect pasted URL
1 parent 46f5d0e commit 00eecd7

File tree

7 files changed

+164
-17
lines changed

7 files changed

+164
-17
lines changed

binderhub/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ def generate_config(self):
1010
"repo_providers"
1111
].items():
1212
config[repo_provider_class_alias] = repo_provider_class.labels
13+
config[repo_provider_class_alias][
14+
"display_name"
15+
] = repo_provider_class.display_name
16+
config[repo_provider_class_alias][
17+
"regex_detect"
18+
] = repo_provider_class.regex_detect
1319
return config
1420

1521
async def get(self):

binderhub/repoproviders.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class RepoProvider(LoggingConfigurable):
9999
config=True,
100100
)
101101

102+
# Not a traitlet because the class property is serialised in
103+
# config.ConfigHandler.generate_config()
104+
regex_detect = None
105+
102106
unresolved_ref = Unicode()
103107

104108
git_credentials = Unicode(
@@ -192,6 +196,15 @@ def is_valid_sha1(sha1):
192196
class FakeProvider(RepoProvider):
193197
"""Fake provider for local testing of the UI"""
194198

199+
name = Unicode("Fake")
200+
201+
display_name = "Fake GitHub"
202+
203+
regex_detect = [
204+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
205+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
206+
]
207+
195208
labels = {
196209
"text": "Fake Provider",
197210
"tag_text": "Fake Ref",
@@ -627,6 +640,13 @@ def _default_git_credentials(self):
627640
return rf"username=binderhub\npassword={self.private_token}"
628641
return ""
629642

643+
# Gitlab repos can be nested under projects
644+
_regex_detect_base = r"^https://gitlab\.com/(?<repo>[^/]+/[^/]+(/[^/-][^/]+)*)"
645+
regex_detect = [
646+
_regex_detect_base + r"(/-/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
647+
_regex_detect_base + r"(/-/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
648+
]
649+
630650
labels = {
631651
"text": "GitLab.com repository or URL",
632652
"tag_text": "Git ref (branch, tag, or commit)",
@@ -780,6 +800,11 @@ def _default_git_credentials(self):
780800
return rf"username={self.access_token}\npassword=x-oauth-basic"
781801
return ""
782802

803+
regex_detect = [
804+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
805+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
806+
]
807+
783808
labels = {
784809
"text": "GitHub repository name or URL",
785810
"tag_text": "Git ref (branch, tag, or commit)",
@@ -973,6 +998,10 @@ class GistRepoProvider(GitHubRepoProvider):
973998
help="Flag for allowing usages of secret Gists. The default behavior is to disallow secret gists.",
974999
)
9751000

1001+
regex_detect = [
1002+
r"^https://gist\.github\.com/(?<repo>[^/]+/[^/]+)(/(?<ref>[^/]+))?$"
1003+
]
1004+
9761005
labels = {
9771006
"text": "Gist ID (username/gistId) or URL",
9781007
"tag_text": "Git commit SHA",

binderhub/static/js/index.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import "../index.css";
1919
import { setUpLog } from "./src/log";
2020
import { updateUrls } from "./src/urls";
2121
import { getBuildFormValues } from "./src/form";
22-
import { updateRepoText } from "./src/repo";
22+
import { detectPastedRepo, updateRepoText } from "./src/repo";
2323

2424
/**
2525
* @type {URL}
@@ -166,7 +166,19 @@ function indexMain() {
166166
updatePathText();
167167
updateRepoText(BASE_URL);
168168

169-
$("#repository").on("keyup paste change", function () {
169+
// If the user pastes a URL into the repository field try to autodetect
170+
// In all other cases don't do anything to avoid overwriting the user's input
171+
// We need to wait for the paste to complete before we can read the input field
172+
// https://stackoverflow.com/questions/10972954/javascript-onpaste/10972973#10972973
173+
$("#repository").on("paste", () => {
174+
setTimeout(() => {
175+
detectPastedRepo(BASE_URL).then(() => {
176+
updateUrls(BADGE_BASE_URL);
177+
});
178+
}, 0);
179+
});
180+
181+
$("#repository").on("keyup change", function () {
170182
updateUrls(BADGE_BASE_URL);
171183
});
172184

binderhub/static/js/src/repo.js

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { detect, getRepoProviders } from "@jupyterhub/binderhub-client";
2+
import { updatePathText } from "./path";
3+
14
/**
2-
* Dict holding cached values of API request to _config endpoint
5+
* @param {Object} configDict Dict holding cached values of API request to _config endpoint
36
*/
4-
let configDict = {};
5-
6-
function setLabels() {
7+
function setLabels(configDict) {
78
const provider = $("#provider_prefix").val();
89
const text = configDict[provider]["text"];
910
const tagText = configDict[provider]["tag_text"];
@@ -23,15 +24,42 @@ function setLabels() {
2324
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
2425
*/
2526
export function updateRepoText(baseUrl) {
26-
if (Object.keys(configDict).length === 0) {
27-
const configUrl = new URL("_config", baseUrl);
28-
fetch(configUrl).then((resp) => {
29-
resp.json().then((data) => {
30-
configDict = data;
31-
setLabels();
32-
});
33-
});
34-
} else {
35-
setLabels();
27+
getRepoProviders(baseUrl).then(setLabels);
28+
}
29+
30+
/**
31+
* Attempt to fill in all fields by parsing a pasted repository URL
32+
*
33+
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
34+
*/
35+
export async function detectPastedRepo(baseUrl) {
36+
const repoField = $("#repository").val().trim();
37+
const fields = await detect(baseUrl, repoField);
38+
// Special case: The BinderHub UI supports https://git{hub,lab}.com/ in the
39+
// repository (it's stripped out later in the UI).
40+
// To keep the UI consistent insert it back if it was originally included.
41+
console.log(fields);
42+
if (fields) {
43+
let repo = fields.repository;
44+
if (repoField.startsWith("https://github.com/")) {
45+
repo = "https://github.com/" + repo;
46+
}
47+
if (repoField.startsWith("https://gitlab.com/")) {
48+
repo = "https://gitlab.com/" + repo;
49+
}
50+
$("#provider_prefix-selected").text(fields.providerName);
51+
$("#provider_prefix").val(fields.providerPrefix);
52+
$("#repository").val(repo);
53+
if (fields.ref) {
54+
$("#ref").val(fields.ref);
55+
}
56+
if (fields.path) {
57+
$("#filepath").val(fields.path);
58+
$("#url-or-file-selected").text(
59+
fields.pathType === "filepath" ? "File" : "URL",
60+
);
61+
}
62+
updatePathText();
63+
updateRepoText(baseUrl);
3664
}
3765
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { fetch as fetchPolyfill } from "whatwg-fetch";
2+
3+
// Use native browser fetch if available, and use the polyfill if not available
4+
// (e.g. in tests https://github.com/jestjs/jest/issues/13834#issuecomment-1407375787)
5+
// @todo: this is only a problem in the jest tests, so get rid of this and mock fetch instead
6+
const fetch = window.fetch || fetchPolyfill;
7+
8+
/**
9+
* Dict holding cached values of API request to _config endpoint for base URL
10+
*/
11+
let repoProviders = {};
12+
13+
/**
14+
* Get the repo provider configurations supported by the BinderHub instance
15+
*
16+
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
17+
*/
18+
export async function getRepoProviders(baseUrl) {
19+
if (!repoProviders[baseUrl]) {
20+
const configUrl = new URL("_config", baseUrl);
21+
const resp = await fetch(configUrl);
22+
repoProviders[baseUrl] = resp.json();
23+
}
24+
return repoProviders[baseUrl];
25+
}
26+
27+
/**
28+
* Attempt to parse a string (typically a repository URL) into a BinderHub
29+
* provider/repository/reference/path
30+
*
31+
* @param {URL} baseUrl Base URL to use for constructing path to _config endpoint
32+
* @param {string} text Repository URL or similar to parse
33+
* @returns {Object} An object if the repository could be parsed with fields
34+
* - providerPrefix Prefix denoting what provider was selected
35+
* - repository Repository to build
36+
* - ref Ref in this repo to build (optional)
37+
* - path Path to launch after this repo has been built (optional)
38+
* - pathType Type of thing to open path with (raw url, notebook file) (optional)
39+
* - providerName User friendly display name of the provider (optional)
40+
* null otherwise
41+
*/
42+
export async function detect(baseUrl, text) {
43+
const config = await getRepoProviders(baseUrl);
44+
45+
for (const provider in config) {
46+
const regex_detect = config[provider].regex_detect || [];
47+
for (const regex of regex_detect) {
48+
const m = text.match(regex);
49+
if (m?.groups.repo) {
50+
return {
51+
providerPrefix: provider,
52+
repository: m.groups.repo,
53+
ref: m.groups.ref,
54+
path: m.groups.filepath || m.groups.urlpath || null,
55+
pathType: m.groups.filepath
56+
? "filepath"
57+
: m.groups.urlpath
58+
? "urlpath"
59+
: null,
60+
providerName: config[provider].display_name,
61+
};
62+
}
63+
}
64+
}
65+
66+
return null;
67+
}

js/packages/binderhub-client/lib/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { NativeEventSource, EventSourcePolyfill } from "event-source-polyfill";
22
import { EventIterator } from "event-iterator";
33

4+
import { detect, getRepoProviders } from "./autodetect";
5+
46
// Use native browser EventSource if available, and use the polyfill if not available
57
const EventSource = NativeEventSource || EventSourcePolyfill;
68

@@ -211,3 +213,5 @@ export function makeBadgeMarkup(publicBaseUrl, url, syntax) {
211213
);
212214
}
213215
}
216+
217+
export { detect, getRepoProviders };

js/packages/binderhub-client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
},
1515
"homepage": "https://github.com/jupyterhub/binderhub#readme",
1616
"dependencies": {
17+
"event-iterator": "^2.0.0",
1718
"event-source-polyfill": "^1.0.31",
18-
"event-iterator": "^2.0.0"
19+
"whatwg-fetch": "^3.6.19"
1920
}
2021
}

0 commit comments

Comments
 (0)