| … | … | @@ -53,7 +53,7 @@ function timeAgo (ms) { |
| 53 | 53 | return Math.floor(mo / 12) + 'y ago' |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | export async function renderRepoReleases (svc, ctx, repoName) { | |
| 56 | export async function renderRepoReleases (svc, ctx, repoName, opts = {}) { | |
| 57 | 57 | const prefix = (ctx && ctx.prefix) || '' |
| 58 | 58 | if (!svc._releases) { |
| 59 | 59 | return errorPage('Release manager not initialised', ctx, repoName) |
| … | … | @@ -90,7 +90,12 @@ export async function renderRepoReleases (svc, ctx, repoName) { |
| 90 | 90 | .map(e => ({ ...e, _ms: toMs(e.publishedAt) })) |
| 91 | 91 | .sort((a, b) => b._ms - a._ms) |
| 92 | 92 | |
| 93 | // Entries this forge cannot serve are OMITTED by default, not just dimmed. | |
| 94 | // On hyper-tps 377 of 384 rows were unservable: they buried the 7 usable ones | |
| 95 | // and shipped ~280KB of markup to say so. `?all=1` renders them, marked. | |
| 96 | const showAll = !!opts.showAll | |
| 93 | 97 | const absentCount = treeEntries.filter(e => e.heldLocally === false).length |
| 98 | const shownEntries = showAll ? treeEntries : treeEntries.filter(e => e.heldLocally !== false) | |
| 94 | 99 | |
| 95 | 100 | const { versions, channels } = await svc._releases.list(repoName) |
| 96 | 101 | const pinnedVersions = new Set(Object.values(channels || {}).map(c => c.version)) |
| … | … | @@ -123,19 +128,23 @@ ${renderRepoTabs(repoName, 'releases', {}, prefix)} |
| 123 | 128 | </div> |
| 124 | 129 | ${absentCount > 0 |
| 125 | 130 | ? `<div class="meta" style="margin-bottom:0.6rem"> |
| 126 | <strong>${absentCount}</strong> of ${treeEntries.length} not stored on this forge. | |
| 131 | <strong>${absentCount}</strong> of ${treeEntries.length} entries are not stored on this | |
| 132 | forge and are ${showAll ? 'shown below, marked' : 'hidden'}. | |
| 127 | 133 | The registry is append-only; the tree store is garbage-collected, so older |
| 128 | 134 | entries outlive their bytes. Another peer may still hold them. |
| 135 | ${showAll | |
| 136 | ? `<a href="${base}/releases">show only what this forge serves</a>` | |
| 137 | : `<a href="${base}/releases?all=1">show all ${treeEntries.length}</a>`} | |
| 129 | 138 | </div>` |
| 130 | 139 | : ''} |
| 131 | 140 | ${treeRows.unavailable |
| 132 | 141 | ? `<div class="empty">Registry unavailable — ${escapeHtml(treeRows.unavailable)}</div>` |
| 133 | : treeEntries.length === 0 | |
| 142 | : shownEntries.length === 0 | |
| 134 | 143 | ? `<div class="empty">Nothing published to pgh under the name <code>${escapeHtml(repoName)}</code>.</div>` |
| 135 | 144 | : `<table class="version-table"> |
| 136 | 145 | <thead><tr><th>Version</th><th>Published</th><th>Deps</th><th>Tree</th><th>Install</th></tr></thead> |
| 137 | 146 | <tbody> |
| 138 | ${treeEntries.map(e => `<tr${e.heldLocally === false ? ' class="tree-absent"' : ''}> | |
| 147 | ${shownEntries.map(e => `<tr${e.heldLocally === false ? ' class="tree-absent"' : ''}> | |
| 139 | 148 | <td><strong>${escapeHtml(String(e.version || ''))}</strong>${e.runtime ? ` <span class="meta">${escapeHtml(e.runtime)}</span>` : ''}${ |
| 140 | 149 | e.heldLocally === false |
| 141 | 150 | ? ' <span class="badge badge-closed" title="This forge does not hold this tree. The registry is append-only while the tree store is garbage-collected, so older entries routinely outlive their bytes. Another peer may still hold it.">not stored here</span>' |
| … | … | @@ -1091,7 +1091,7 @@ export function registerRoutes (svc) { |
| 1091 | 1091 | const repoReleasesMatch = url.match(/^\/repos\/([^/]+)\/releases$/) |
| 1092 | 1092 | if (repoReleasesMatch) { |
| 1093 | 1093 | const repoName = decodeURIComponent(repoReleasesMatch[1]) |
| 1094 | return sendHtml(res, await renderRepoReleases(svc, ctx, repoName)) | |
| 1094 | return sendHtml(res, await renderRepoReleases(svc, ctx, repoName, { showAll: query.all === '1' })) | |
| 1095 | 1095 | } |
| 1096 | 1096 | |
| 1097 | 1097 | // /repos/:name/docs — markdown docs index (U38) |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "pear-git-v3", |
| 3 | "version": "0.1.244", | |
| 3 | "version": "0.1.245", | |
| 4 | 4 | "description": "P2P git forge — multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -83,14 +83,15 @@ function svcWith (entries) { |
| 83 | 83 | } |
| 84 | 84 | |
| 85 | 85 | test('the page marks absent trees and refuses to print an install for them', async (t) => { |
| 86 | // (svc, ctx, repoName, opts) -- the order the router actually uses. | |
| 86 | 87 | const html = await renderRepoReleases(svcWith([ |
| 87 | 88 | { name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true }, |
| 88 | 89 | { name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false } |
| 89 | ]), 'demo', {}) | |
| 90 | ]), {}, 'demo', { showAll: true }) | |
| 90 | 91 | |
| 91 | 92 | t.ok(html.includes('not stored here'), 'the absent row is labelled') |
| 92 | 93 | t.ok(html.includes('tree-absent'), 'and dimmed via its own class') |
| 93 | t.ok(html.includes('1</strong> of 2 not stored on this forge'), 'summary counts it') | |
| 94 | t.ok(html.includes('<strong>1</strong> of 2'), 'summary counts it (copy is not the contract)') | |
| 94 | 95 | |
| 95 | 96 | // The held version keeps a usable command; the absent one must not have one. |
| 96 | 97 | t.ok(html.includes('pgh app install pgh://' + 'k'.repeat(64) + '/demo@2.0.0'), |
| … | … | @@ -103,9 +104,47 @@ test('the page marks absent trees and refuses to print an install for them', asy |
| 103 | 104 | test('a fully-held list shows no summary and no markers', async (t) => { |
| 104 | 105 | const html = await renderRepoReleases(svcWith([ |
| 105 | 106 | { name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true } |
| 106 | ]), 'demo', {}) | |
| 107 | ]), {}, 'demo', {}) | |
| 107 | 108 | |
| 108 | 109 | t.absent(html.includes('not stored here'), 'no marker') |
| 109 | 110 | t.absent(html.includes('not stored on this forge'), 'no summary noise when everything resolves') |
| 110 | 111 | t.ok(html.includes('pgh app install'), 'install still offered') |
| 111 | 112 | }) |
| 113 | ||
| 114 | // ── Hidden by default ──────────────────────────────────────── | |
| 115 | // | |
| 116 | // 377 of 384 hyper-tps rows were entries this forge cannot serve. Rendering | |
| 117 | // them by default buried the 7 usable ones and shipped ~280KB of markup to say | |
| 118 | // so. They are OMITTED now, not merely dimmed, with a link to see them. | |
| 119 | ||
| 120 | test('not-stored entries are omitted by default', async (t) => { | |
| 121 | const html = await renderRepoReleases(svcWith([ | |
| 122 | { name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true }, | |
| 123 | { name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false }, | |
| 124 | { name: 'demo', version: '0.9.0', treeHash: 'cc'.repeat(32), publishedAt: 0, heldLocally: false } | |
| 125 | ]), {}, 'demo', {}) | |
| 126 | ||
| 127 | t.ok(html.includes('demo@2.0.0'), 'the servable version is shown') | |
| 128 | t.absent(html.includes('0.9.0'), 'a not-stored version is not rendered at all') | |
| 129 | t.absent(html.includes('1.0.0'), 'nor the other one') | |
| 130 | t.ok(html.includes('?all=1'), 'but there is a way to see them') | |
| 131 | t.ok(/2 (older )?entr/i.test(html) || html.includes('>2<'), 'and the count is stated') | |
| 132 | }) | |
| 133 | ||
| 134 | test('showAll renders them, marked', async (t) => { | |
| 135 | const html = await renderRepoReleases(svcWith([ | |
| 136 | { name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true }, | |
| 137 | { name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false } | |
| 138 | ]), {}, 'demo', { showAll: true }) | |
| 139 | ||
| 140 | t.ok(html.includes('1.0.0'), 'the not-stored version appears') | |
| 141 | t.ok(html.includes('not stored here'), 'still marked') | |
| 142 | t.absent(html.includes('/demo@1.0.0'), 'and still offers no install') | |
| 143 | }) | |
| 144 | ||
| 145 | test('when everything is servable there is no hidden-count line', async (t) => { | |
| 146 | const html = await renderRepoReleases(svcWith([ | |
| 147 | { name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true } | |
| 148 | ]), {}, 'demo', {}) | |
| 149 | t.absent(html.includes('?all=1'), 'no toggle offered when nothing is hidden') | |
| 150 | }) |