openvibers/openvibe-git Commits
Official mirrors: OpenVibers is a user account, list it as one (#10)
Changes
@@@ -103,15 +103,27 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>} catch { return null; }}-- /** Every public repository of an organisation, following GitHub's Link pagination. */++ /**++ * Every public repository of an organisation (or of a user account: OpenVibers is one, and GitHub answers 404 for++ * /orgs/<user>/repos, 11-09), following GitHub's Link pagination.++ */async function orgRepos(org, { perPage = 100, maxPages = 100 } = {}) {const apiOrigin = new URL(config.github.apiUrl).origin;let url = `${config.github.apiUrl}/orgs/${encodeURIComponent(org)}/repos?type=public&per_page=${perPage}`;++ let triedUser = false;const all = [];for (let page = 0; url && page < maxPages; page += 1) {const current = new URL(url);if (current.origin !== apiOrigin) throw new Error('GitHub pagination left the API origin');const res = await fetchImpl(current.toString(), { headers: headers(), signal: AbortSignal.timeout(15_000) });++ if (res.status === 404 && page === 0 && !triedUser) {++ // Not an organisation: the same name as a user account. type=owner lists what it owns; private ones++ // never appear without that account's token, and the seed keeps only public repositories anyway.++ triedUser = true;++ url = `${config.github.apiUrl}/users/${encodeURIComponent(org)}/repos?type=owner&per_page=${perPage}`;++ page -= 1;++ continue;++ }if (!res.ok) {const err = new Error(`GitHub ${res.status} for ${current.pathname}`);err.status = res.status;@@@ -119,6 +131,15 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>}const batch = await res.json();if (!Array.isArray(batch)) throw new Error('GitHub returned no repository list');++ // The listing carries what meta() would ask for: cache it, so mirroring 70 repositories costs a page or two++ // of the API, not one call each (the host has 60 an hour without a token).++ for (const repo of batch) {++ if (!repo || !repo.full_name) continue;++ metaCache.set(String(repo.full_name).toLowerCase(), { at: now(), meta: {++ full_name: repo.full_name, description: repo.description || '', default_branch: repo.default_branch || 'main',++ language: repo.language || null, stars: Number(repo.stargazers_count) || 0, private: !!repo.private,++ } });++ }all.push(...batch);url = nextLink(res.headers && typeof res.headers.get === 'function' ? res.headers.get('link') : null);}@@@ -36,6 +36,9 @@ const LISTING = {const link = page === 1 ? '<https://api.github.com/orgs/OpenVibers/repos?type=public&per_page=100&page=2>; rel="next"' : null;return response(LISTING[page] || [], link);}++ // A user account, not an organisation: GitHub answers 404 for /orgs/<name>/repos (OpenVibers is one, 11-09).++ if (target.pathname === '/orgs/Solo/repos') return { ok: false, status: 404, headers: { get: () => null }, json: async () => ({ message: 'Not Found' }) };++ if (target.pathname === '/users/Solo/repos') return response([{ name: 'solo-site', full_name: 'Solo/solo-site', fork: false, archived: false, private: false, clone_url: 'https://github.com/Solo/solo-site.git' }], null);const meta = /^\/repos\/OpenVibers\/([^/]+)$/.exec(target.pathname);if (meta) return response({ full_name: `OpenVibers/${meta[1]}`, description: '', default_branch: 'main', private: false, stargazers_count: 0 });throw new Error(`unexpected GitHub request ${url}`);@@@ -123,6 +126,14 @@ const LISTING = {assert.equal((await pending).ran, true);t.ctx.github.orgRepos = realOrgRepos;-- console.log('official: 30 checks passed');++ // A user account's repositories: the org listing 404s, the user listing answers.++ const solo = await t.ctx.github.orgRepos('Solo');++ assert.deepEqual(solo.map((r) => r.full_name), ['Solo/solo-site']);++ assert.ok(calls.some((c) => c.url.includes('/users/Solo/repos?type=owner')), 'falls back to /users/<name>/repos');++ const before = calls.length;++ assert.equal((await t.ctx.github.meta('Solo/solo-site')).default_branch, 'main');++ assert.equal(calls.length, before, 'the listing fills the metadata cache: no call per repository');++++ console.log('official: 34 checks passed');} finally { await t.close(); }})().catch((err) => { console.error(err); process.exit(1); });
@@ -103,15 +103,27 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>
@@ -103,15 +103,27 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>
} catch { return null; }
} catch { return null; }
}
}
- /** Every public repository of an organisation, following GitHub's Link pagination. */
+ /**
+ * Every public repository of an organisation (or of a user account: OpenVibers is one, and GitHub answers 404 for
+ * /orgs/<user>/repos, 11-09), following GitHub's Link pagination.
+ */
async function orgRepos(org, { perPage = 100, maxPages = 100 } = {}) {
async function orgRepos(org, { perPage = 100, maxPages = 100 } = {}) {
const apiOrigin = new URL(config.github.apiUrl).origin;
const apiOrigin = new URL(config.github.apiUrl).origin;
let url = `${config.github.apiUrl}/orgs/${encodeURIComponent(org)}/repos?type=public&per_page=${perPage}`;
let url = `${config.github.apiUrl}/orgs/${encodeURIComponent(org)}/repos?type=public&per_page=${perPage}`;
+ let triedUser = false;
const all = [];
const all = [];
for (let page = 0; url && page < maxPages; page += 1) {
for (let page = 0; url && page < maxPages; page += 1) {
const current = new URL(url);
const current = new URL(url);
if (current.origin !== apiOrigin) throw new Error('GitHub pagination left the API origin');
if (current.origin !== apiOrigin) throw new Error('GitHub pagination left the API origin');
const res = await fetchImpl(current.toString(), { headers: headers(), signal: AbortSignal.timeout(15_000) });
const res = await fetchImpl(current.toString(), { headers: headers(), signal: AbortSignal.timeout(15_000) });
+ if (res.status === 404 && page === 0 && !triedUser) {
+ // Not an organisation: the same name as a user account. type=owner lists what it owns; private ones
+ // never appear without that account's token, and the seed keeps only public repositories anyway.
+ triedUser = true;
+ url = `${config.github.apiUrl}/users/${encodeURIComponent(org)}/repos?type=owner&per_page=${perPage}`;
+ page -= 1;
+ continue;
+ }
if (!res.ok) {
if (!res.ok) {
const err = new Error(`GitHub ${res.status} for ${current.pathname}`);
const err = new Error(`GitHub ${res.status} for ${current.pathname}`);
err.status = res.status;
err.status = res.status;
@@ -119,6 +131,15 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>
@@ -119,6 +131,15 @@ function createGithubClient({ config, fetchImpl = globalThis.fetch, now = () =>
}
}
const batch = await res.json();
const batch = await res.json();
if (!Array.isArray(batch)) throw new Error('GitHub returned no repository list');
if (!Array.isArray(batch)) throw new Error('GitHub returned no repository list');
+ // The listing carries what meta() would ask for: cache it, so mirroring 70 repositories costs a page or two
+ // of the API, not one call each (the host has 60 an hour without a token).
+ for (const repo of batch) {
+ if (!repo || !repo.full_name) continue;
+ metaCache.set(String(repo.full_name).toLowerCase(), { at: now(), meta: {
+ full_name: repo.full_name, description: repo.description || '', default_branch: repo.default_branch || 'main',
+ language: repo.language || null, stars: Number(repo.stargazers_count) || 0, private: !!repo.private,
+ } });
+ }
all.push(...batch);
all.push(...batch);
url = nextLink(res.headers && typeof res.headers.get === 'function' ? res.headers.get('link') : null);
url = nextLink(res.headers && typeof res.headers.get === 'function' ? res.headers.get('link') : null);
}
}
@@ -36,6 +36,9 @@ const LISTING = {
@@ -36,6 +36,9 @@ const LISTING = {
const link = page === 1 ? '<https://api.github.com/orgs/OpenVibers/repos?type=public&per_page=100&page=2>; rel="next"' : null;
const link = page === 1 ? '<https://api.github.com/orgs/OpenVibers/repos?type=public&per_page=100&page=2>; rel="next"' : null;
return response(LISTING[page] || [], link);
return response(LISTING[page] || [], link);
}
}
+ // A user account, not an organisation: GitHub answers 404 for /orgs/<name>/repos (OpenVibers is one, 11-09).
+ if (target.pathname === '/orgs/Solo/repos') return { ok: false, status: 404, headers: { get: () => null }, json: async () => ({ message: 'Not Found' }) };
+ if (target.pathname === '/users/Solo/repos') return response([{ name: 'solo-site', full_name: 'Solo/solo-site', fork: false, archived: false, private: false, clone_url: 'https://github.com/Solo/solo-site.git' }], null);
const meta = /^\/repos\/OpenVibers\/([^/]+)$/.exec(target.pathname);
const meta = /^\/repos\/OpenVibers\/([^/]+)$/.exec(target.pathname);
if (meta) return response({ full_name: `OpenVibers/${meta[1]}`, description: '', default_branch: 'main', private: false, stargazers_count: 0 });
if (meta) return response({ full_name: `OpenVibers/${meta[1]}`, description: '', default_branch: 'main', private: false, stargazers_count: 0 });
throw new Error(`unexpected GitHub request ${url}`);
throw new Error(`unexpected GitHub request ${url}`);
@@ -123,6 +126,14 @@ const LISTING = {
@@ -123,6 +126,14 @@ const LISTING = {
assert.equal((await pending).ran, true);
assert.equal((await pending).ran, true);
t.ctx.github.orgRepos = realOrgRepos;
t.ctx.github.orgRepos = realOrgRepos;
- console.log('official: 30 checks passed');
+ // A user account's repositories: the org listing 404s, the user listing answers.
+ const solo = await t.ctx.github.orgRepos('Solo');
+ assert.deepEqual(solo.map((r) => r.full_name), ['Solo/solo-site']);
+ assert.ok(calls.some((c) => c.url.includes('/users/Solo/repos?type=owner')), 'falls back to /users/<name>/repos');
+ const before = calls.length;
+ assert.equal((await t.ctx.github.meta('Solo/solo-site')).default_branch, 'main');
+ assert.equal(calls.length, before, 'the listing fills the metadata cache: no call per repository');
+
+ console.log('official: 34 checks passed');
} finally { await t.close(); }
} finally { await t.close(); }
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });