@magland/mochi 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/agefile.js +47 -0
- package/dist/agescript.js +343 -0
- package/dist/api/admin.js +66 -28
- package/dist/api/auth.js +3 -2
- package/dist/api/backup.js +227 -192
- package/dist/api/contents.js +46 -16
- package/dist/api/repos.js +60 -4
- package/dist/api/write.js +138 -8
- package/dist/api.js +34 -1
- package/dist/browse.js +171 -33
- package/dist/build-info.json +2 -2
- package/dist/ci/api.js +138 -4
- package/dist/ci/artifacts.js +82 -2
- package/dist/ci/engine.js +30 -17
- package/dist/ci/expr.js +14 -0
- package/dist/ci/runners.js +60 -1
- package/dist/ci/runs.js +21 -3
- package/dist/ci/trigger.js +6 -4
- package/dist/ci/views.js +24 -3
- package/dist/ci/web.js +48 -0
- package/dist/cli/admin-cmd.js +40 -3
- package/dist/cli/backup-cmd.js +166 -120
- package/dist/cli/gitrun.js +50 -0
- package/dist/cli/pr-cmd.js +155 -0
- package/dist/cli/repo-cmd.js +134 -6
- package/dist/cli/sync-cmd.js +128 -0
- package/dist/cli/target.js +7 -3
- package/dist/config.js +10 -0
- package/dist/credentials.js +9 -8
- package/dist/deploy-cli.js +214 -109
- package/dist/deploy-runner-cli.js +125 -49
- package/dist/domains.js +259 -0
- package/dist/egress.js +23 -6
- package/dist/forms.js +703 -32
- package/dist/git.js +124 -13
- package/dist/githttp.js +80 -23
- package/dist/githubauth.js +185 -0
- package/dist/icons.js +14 -0
- package/dist/import-cli.js +40 -25
- package/dist/index.js +110 -4
- package/dist/issueweb.js +10 -6
- package/dist/lfs.js +9 -2
- package/dist/limit.js +2 -2
- package/dist/logincodes.js +31 -0
- package/dist/maintenance.js +297 -0
- package/dist/markdown.js +193 -3
- package/dist/mdedit.js +29 -0
- package/dist/migrate.js +48 -0
- package/dist/naming.js +25 -0
- package/dist/onetime.js +112 -0
- package/dist/ops.js +293 -16
- package/dist/pagescript.js +355 -24
- package/dist/perms.js +1 -0
- package/dist/profile.js +2 -0
- package/dist/pulls.js +21 -1
- package/dist/pullweb.js +25 -6
- package/dist/releases.js +10 -1
- package/dist/runner/actions.js +12 -1
- package/dist/runner/client.js +40 -2
- package/dist/runner/docker.js +40 -28
- package/dist/runner/download.js +77 -0
- package/dist/runner/externals.js +12 -1
- package/dist/runner/job.js +26 -11
- package/dist/runner/steps.js +42 -7
- package/dist/runner-cli.js +89 -0
- package/dist/scan.js +58 -2
- package/dist/server.js +107 -26
- package/dist/session.js +82 -40
- package/dist/site.js +110 -27
- package/dist/sitepublish.js +177 -0
- package/dist/sitesettings.js +420 -0
- package/dist/siteshost.js +116 -28
- package/dist/source.js +20 -0
- package/dist/style.js +162 -3
- package/dist/topics.js +165 -0
- package/dist/vault.js +399 -6
- package/dist/vendor-age.js +15 -0
- package/dist/views.js +242 -57
- package/dist/web.js +11 -1
- package/dist/webauthn.js +347 -0
- package/dist/webops.js +1263 -33
- package/package.json +4 -4
package/dist/views.js
CHANGED
|
@@ -3,14 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.encPath = encPath;
|
|
4
4
|
exports.repoUrl = repoUrl;
|
|
5
5
|
exports.csrfField = csrfField;
|
|
6
|
+
exports.userLink = userLink;
|
|
7
|
+
exports.gitAuthor = gitAuthor;
|
|
6
8
|
exports.layout = layout;
|
|
7
9
|
exports.repoOpts = repoOpts;
|
|
8
10
|
exports.copyButton = copyButton;
|
|
9
11
|
exports.copyRow = copyRow;
|
|
12
|
+
exports.agePassInput = agePassInput;
|
|
10
13
|
exports.repoHeader = repoHeader;
|
|
14
|
+
exports.topicChips = topicChips;
|
|
11
15
|
exports.homePage = homePage;
|
|
12
16
|
exports.aboutPage = aboutPage;
|
|
13
17
|
exports.collectionPage = collectionPage;
|
|
18
|
+
exports.topicsIndexPage = topicsIndexPage;
|
|
19
|
+
exports.topicPage = topicPage;
|
|
14
20
|
exports.treePage = treePage;
|
|
15
21
|
exports.searchPage = searchPage;
|
|
16
22
|
exports.findFilePage = findFilePage;
|
|
@@ -26,6 +32,8 @@ const render_1 = require("./render");
|
|
|
26
32
|
const session_1 = require("./session");
|
|
27
33
|
const assets_1 = require("./assets");
|
|
28
34
|
const pagescript_1 = require("./pagescript");
|
|
35
|
+
const agescript_1 = require("./agescript");
|
|
36
|
+
const agefile_1 = require("./agefile");
|
|
29
37
|
const themes_1 = require("./themes");
|
|
30
38
|
const logo_1 = require("./logo");
|
|
31
39
|
const icons_1 = require("./icons");
|
|
@@ -43,9 +51,39 @@ function repoUrl(ctx) {
|
|
|
43
51
|
function csrfField(viewer) {
|
|
44
52
|
return (0, html_1.html) `<input type="hidden" name="csrf" value="${viewer.csrf}">`;
|
|
45
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A vault username as a link to their profile page at /<name>. The link keeps
|
|
56
|
+
* the weight and colour of the text around it, so a name reads the same
|
|
57
|
+
* whether or not it is one; only hover says it goes somewhere. `face` puts
|
|
58
|
+
* the identicon before the name at that size, and `bold` matches the places
|
|
59
|
+
* that already set the name in bold.
|
|
60
|
+
*/
|
|
61
|
+
function userLink(name, opts = {}) {
|
|
62
|
+
const label = opts.bold ? (0, html_1.html) `<b>${name}</b>` : (0, html_1.html) `${name}`;
|
|
63
|
+
return (0, html_1.html) `<a class="user-link" href="/${encodeURIComponent(name)}">${opts.face ? (0, avatar_1.avatar)(name, opts.face) : ''}${label}</a>`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A git author identity, linked to a profile when the email resolves to a
|
|
67
|
+
* vault user and shown exactly as git reported it when it does not. The
|
|
68
|
+
* identicon is drawn from the account where there is one, so the same person
|
|
69
|
+
* wears the same face whatever author emails their commits carry.
|
|
70
|
+
*/
|
|
71
|
+
function gitAuthor(ctx, name, email, opts = {}) {
|
|
72
|
+
const account = email ? (ctx.accountFor?.(email) ?? null) : null;
|
|
73
|
+
if (account) {
|
|
74
|
+
const label = opts.bold ? (0, html_1.html) `<b>${name}</b>` : (0, html_1.html) `${name}`;
|
|
75
|
+
return (0, html_1.html) `<a class="user-link" href="/${encodeURIComponent(account)}" title="${account}">${opts.face ? (0, avatar_1.avatar)(account, opts.face) : ''}${label}</a>`;
|
|
76
|
+
}
|
|
77
|
+
const label = opts.bold ? (0, html_1.html) `<b>${name}</b>` : (0, html_1.html) `${name}`;
|
|
78
|
+
return (0, html_1.html) `${opts.face ? (0, avatar_1.avatar)(email || name, opts.face) : ''}${label}`;
|
|
79
|
+
}
|
|
46
80
|
const FOLDER_ICON = (0, icons_1.icon)('folder', 'icon');
|
|
47
81
|
const FILE_ICON = (0, icons_1.icon)('file', 'icon file');
|
|
82
|
+
// An age-encrypted file wears the lock wherever files are listed, so what is
|
|
83
|
+
// encrypted can be seen without opening anything.
|
|
84
|
+
const AGE_ICON = (0, icons_1.icon)('lock', 'icon file');
|
|
48
85
|
const REPO_ICON = (0, icons_1.icon)('repo', 'icon');
|
|
86
|
+
const fileIcon = (name) => ((0, agefile_1.isAgeFile)(name) ? AGE_ICON : FILE_ICON);
|
|
49
87
|
function userBox(opts) {
|
|
50
88
|
const viewer = opts.viewer ?? null;
|
|
51
89
|
if (!viewer) {
|
|
@@ -63,6 +101,8 @@ function userBox(opts) {
|
|
|
63
101
|
<summary aria-label="Account menu">${(0, avatar_1.avatar)(name, 24)}${(0, icons_1.icon)('chevron-down', 'caret')}</summary>
|
|
64
102
|
<div class="dropdown-menu dd-right">
|
|
65
103
|
<div class="dd-section">Signed in as <b>${name}</b></div>
|
|
104
|
+
<a class="dd-item" href="/${encodeURIComponent(name)}">${(0, icons_1.icon)('person')}<span>Your profile</span></a>
|
|
105
|
+
<a class="dd-item" href="/account">${(0, icons_1.icon)('lock')}<span>Your account</span></a>
|
|
66
106
|
${admin}
|
|
67
107
|
<form method="post" action="/logout">${csrfField(viewer)}<button type="submit" class="dd-item">${(0, icons_1.icon)('sign-out')}<span>Sign out</span></button></form>
|
|
68
108
|
</div>
|
|
@@ -96,6 +136,14 @@ function themeMenu() {
|
|
|
96
136
|
</div>
|
|
97
137
|
</details>`;
|
|
98
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The way to /about from every page. A visitor can land anywhere in a vault,
|
|
141
|
+
* on a file or an issue rather than the front page, so what this site is has
|
|
142
|
+
* to be reachable from wherever they arrived rather than from one listing.
|
|
143
|
+
*/
|
|
144
|
+
function aboutLink() {
|
|
145
|
+
return (0, html_1.html) `<a class="topbar-icon" href="/about" aria-label="About" title="About this vault">${(0, icons_1.icon)('info')}</a>`;
|
|
146
|
+
}
|
|
99
147
|
function jumpDialog(jump) {
|
|
100
148
|
const data = jump
|
|
101
149
|
? // The payload is JSON in a script tag, where escaping entities would
|
|
@@ -140,6 +188,9 @@ function layout(title, content, opts = {}) {
|
|
|
140
188
|
const theme = (0, themes_1.activeTheme)().name;
|
|
141
189
|
const sheet = (0, assets_1.styleSheet)((0, themes_1.activeTheme)()).tag;
|
|
142
190
|
const script = (0, pagescript_1.pageScript)().tag;
|
|
191
|
+
// Deferred: nothing on it is needed before the DOM exists, and the page's
|
|
192
|
+
// own script must not wait behind 300 KB of cryptography.
|
|
193
|
+
const age = opts.ageScript ? (0, html_1.html) `<script src="/assets/age.js?v=${(0, agescript_1.ageScript)().tag}" defer></script>` : '';
|
|
143
194
|
// The theme pair rides on <html> rather than in the script, which is what
|
|
144
195
|
// lets /assets/page.js be one cacheable file for every vault: the script
|
|
145
196
|
// reads these two attributes instead of being generated around them.
|
|
@@ -153,10 +204,10 @@ function layout(title, content, opts = {}) {
|
|
|
153
204
|
<link id="hl-css" rel="stylesheet" href="/assets/hl.css?t=${encodeURIComponent(theme)}">
|
|
154
205
|
<link rel="stylesheet" href="/assets/katex/katex.css">
|
|
155
206
|
<link rel="icon" href="/favicon.svg?t=${encodeURIComponent(theme)}" type="image/svg+xml">
|
|
156
|
-
<script src="/assets/page.js?v=${script}"></script
|
|
207
|
+
<script src="/assets/page.js?v=${script}"></script>${age}
|
|
157
208
|
</head>
|
|
158
209
|
<body>
|
|
159
|
-
<header class="topbar"><div class="container"><a class="brand" href="/">${(0, html_1.raw)(logo_1.WORDMARK)}</a><span class="crumbs">${opts.crumbs}</span><div class="userbox">${jumpButton()}${themeMenu()}${userBox(opts)}</div></div></header>
|
|
210
|
+
<header class="topbar"><div class="container"><a class="brand" href="/">${(0, html_1.raw)(logo_1.WORDMARK)}</a><span class="crumbs">${opts.crumbs}</span><div class="userbox">${jumpButton()}${aboutLink()}${themeMenu()}${userBox(opts)}</div></div></header>
|
|
160
211
|
<main class="container">
|
|
161
212
|
${content}
|
|
162
213
|
</main>
|
|
@@ -219,6 +270,17 @@ function copyButton(label = '', text, title = '') {
|
|
|
219
270
|
function copyRow(cmd) {
|
|
220
271
|
return (0, html_1.html) `<div class="cmd-row"><code>${cmd}</code>${copyButton()}</div>`;
|
|
221
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* A passphrase input wrapped with a show/hide toggle. The input never carries
|
|
275
|
+
* a name, so no form can ever post its value; /assets/age.js wires the
|
|
276
|
+
* toggle, and both eye glyphs are drawn here so the script draws nothing.
|
|
277
|
+
* With an `id` the field is labelled by its <label>; without one it carries
|
|
278
|
+
* its placeholder as the accessible name.
|
|
279
|
+
*/
|
|
280
|
+
function agePassInput(opts) {
|
|
281
|
+
const label = opts.placeholder ?? 'Passphrase';
|
|
282
|
+
return (0, html_1.html) `<span class="age-pass-wrap"><input type="password"${opts.id ? (0, html_1.html) ` id="${opts.id}"` : ''}${opts.placeholder ? (0, html_1.html) ` placeholder="${opts.placeholder}"` : ''}${opts.id ? '' : (0, html_1.html) ` aria-label="${label}"`} autocomplete="${opts.autocomplete}" spellcheck="false"${opts.required ? (0, html_1.raw)(' required') : ''}><button type="button" class="age-eye" aria-label="Show the passphrase" aria-pressed="false">${(0, icons_1.icon)('eye', 'glyph-eye')}${(0, icons_1.icon)('eye-off', 'glyph-eye-off')}</button></span>`;
|
|
283
|
+
}
|
|
222
284
|
/**
|
|
223
285
|
* The branch and tag picker: a button carrying the current ref, opening a
|
|
224
286
|
* menu of every ref with a filter box, as on GitHub. It is a <details>
|
|
@@ -279,7 +341,11 @@ function repoHeader(ctx, active) {
|
|
|
279
341
|
const search = (0, html_1.html) `<form class="repo-search" method="get" action="${base}/search" role="search"><input type="hidden" name="ref" value="${ctx.ref}"><input class="search-input" type="search" name="q" placeholder="Search this repository" aria-label="Search this repository">${(0, icons_1.icon)('search', 'search-glyph')}</form>`;
|
|
280
342
|
const parent = ctx.forkedFrom
|
|
281
343
|
? (0, html_1.html) `<div class="fork-note muted small">${(0, icons_1.icon)('repo-forked')}<span>forked from <a href="/${encodeURIComponent(ctx.forkedFrom.collection)}/${encodeURIComponent(ctx.forkedFrom.repo)}">${ctx.forkedFrom.collection}/${ctx.forkedFrom.repo}</a></span></div>`
|
|
282
|
-
:
|
|
344
|
+
: ctx.upstream
|
|
345
|
+
? (0, html_1.html) `<div class="fork-note muted small">${(0, icons_1.icon)('repo-forked')}<span>forked from ${ctx.upstream.web
|
|
346
|
+
? (0, html_1.html) `<a href="${ctx.upstream.web}" rel="noopener">${ctx.upstream.label}</a>`
|
|
347
|
+
: ctx.upstream.label}${ctx.canPush ? (0, html_1.html) ` · <a href="${base}/sync">Sync</a>` : ''}</span></div>`
|
|
348
|
+
: '';
|
|
283
349
|
const badge = ctx.isPrivate ? (0, html_1.html) ` <span class="counter" title="Only collaborators, owners, and site admins can see this repository">Private</span>` : '';
|
|
284
350
|
return (0, html_1.html) `<div class="repo-title">${REPO_ICON}<a href="/${encodeURIComponent(ctx.collection)}">${ctx.collection}</a> <span class="muted">/</span> <a href="${base}"><b>${ctx.repo}</b></a>${badge}${search}</div>
|
|
285
351
|
${parent}
|
|
@@ -327,6 +393,17 @@ function listFilter(target, placeholder, rowCount) {
|
|
|
327
393
|
function noMatches(target) {
|
|
328
394
|
return (0, html_1.html) `<div class="empty-state" id="${target}-empty" hidden>No match.</div>`;
|
|
329
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Topic chips, each leading to the vault-wide page for that topic. One look
|
|
398
|
+
* everywhere on purpose: a topic is the same topic wherever it appears, so it
|
|
399
|
+
* wears one colour from the theme rather than a colour of its own the way an
|
|
400
|
+
* issue label does.
|
|
401
|
+
*/
|
|
402
|
+
function topicChips(topics) {
|
|
403
|
+
if (!topics || topics.length === 0)
|
|
404
|
+
return '';
|
|
405
|
+
return (0, html_1.html) `<span class="topic-chips">${(0, html_1.joinHtml)(topics.map((t) => (0, html_1.html) `<a class="chip topic" href="/topics/${encodeURIComponent(t)}">${t}</a>`))}</span>`;
|
|
406
|
+
}
|
|
330
407
|
const CI_MARKS = {
|
|
331
408
|
running: { glyph: 'dot', label: 'running' },
|
|
332
409
|
success: { glyph: 'check-circle', label: 'passed' },
|
|
@@ -353,12 +430,19 @@ function repoCard(r, showCollection) {
|
|
|
353
430
|
? (0, html_1.html) `<a class="site-link" href="${r.siteUrl}" title="Site" aria-label="Site for ${r.name}">${(0, icons_1.icon)('globe')}</a>`
|
|
354
431
|
: '';
|
|
355
432
|
const ci = r.ci ? ciMark(r.ci, r.name) : '';
|
|
356
|
-
|
|
433
|
+
// The card clips to two lines with CSS, but the clipping is visual only:
|
|
434
|
+
// the bytes still travel, and a listing repeats them per repository, so a
|
|
435
|
+
// description larger than the model now accepts (one written before the cap
|
|
436
|
+
// existed) is cut here rather than shipped whole to every reader.
|
|
437
|
+
const shown = r.description && r.description.length > 400 ? `${r.description.slice(0, 400)}…` : r.description;
|
|
438
|
+
const desc = shown ? (0, html_1.html) `<p class="rc-desc">${shown}</p>` : '';
|
|
439
|
+
const topics = topicChips(r.topics);
|
|
357
440
|
const when = r.updated ? (0, html_1.html) `<span class="rc-when">${(0, render_1.timeTag)(r.updated)}</span>` : '';
|
|
358
441
|
const badge = r.isPrivate ? (0, html_1.html) `<span class="counter">Private</span>` : '';
|
|
359
442
|
return (0, html_1.html) `<li class="repo-card">
|
|
360
443
|
<div class="rc-top"><a class="rc-name" href="${href}">${prefix}${r.name}</a>${badge}<span class="rc-marks">${site}${ci}</span></div>
|
|
361
444
|
${desc}
|
|
445
|
+
${topics ? (0, html_1.html) `<div class="rc-topics">${topics}</div>` : ''}
|
|
362
446
|
<div class="rc-meta">${when}</div>
|
|
363
447
|
</li>`;
|
|
364
448
|
}
|
|
@@ -366,7 +450,9 @@ ${desc}
|
|
|
366
450
|
* The listing itself. Sorting is a link rather than a script, so the order a
|
|
367
451
|
* reader chose is in the address they can keep, and the two orders answer the
|
|
368
452
|
* two questions a listing is asked: what has been happening lately, and where
|
|
369
|
-
* is the one I already know the name of.
|
|
453
|
+
* is the one I already know the name of. Narrowing by topic is a link for the
|
|
454
|
+
* same reason, and each link carries the rest of the choice, so picking a
|
|
455
|
+
* sort never loses the topic and picking a topic never loses the sort.
|
|
370
456
|
*/
|
|
371
457
|
function repoListing(repos, opts) {
|
|
372
458
|
const sorted = [...repos];
|
|
@@ -376,29 +462,54 @@ function repoListing(repos, opts) {
|
|
|
376
462
|
else {
|
|
377
463
|
sorted.sort((a, b) => (b.updated ?? '').localeCompare(a.updated ?? ''));
|
|
378
464
|
}
|
|
379
|
-
const
|
|
465
|
+
const topic = opts.topics?.current ?? '';
|
|
466
|
+
const listUrl = (sort, t) => {
|
|
467
|
+
const params = new URLSearchParams();
|
|
468
|
+
if (sort !== 'recent')
|
|
469
|
+
params.set('sort', sort);
|
|
470
|
+
if (t !== '')
|
|
471
|
+
params.set('topic', t);
|
|
472
|
+
const q = params.toString();
|
|
473
|
+
return q === '' ? opts.sortBase : `${opts.sortBase}?${q}`;
|
|
474
|
+
};
|
|
475
|
+
const sortLink = (id, label) => (0, html_1.html) `<a${opts.sort === id ? (0, html_1.raw)(' class="current" aria-current="true"') : ''} href="${listUrl(id, topic)}">${label}</a>`;
|
|
476
|
+
const inUse = opts.topics?.inUse ?? [];
|
|
477
|
+
const topicMenu = inUse.length !== 0 || topic !== ''
|
|
478
|
+
? (0, html_1.html) `<details class="dropdown topic-menu">
|
|
479
|
+
<summary class="btn">${(0, icons_1.icon)('tag')}<span>${topic || 'Topics'}</span>${(0, icons_1.icon)('chevron-down', 'caret')}</summary>
|
|
480
|
+
<div class="dropdown-menu"><div class="dd-scroll">${(0, html_1.joinHtml)([
|
|
481
|
+
(0, html_1.html) `<a class="dd-item${topic === '' ? ' current' : ''}" href="${listUrl(opts.sort, '')}"><span class="dd-check"></span><span class="dd-label">Any topic</span></a>`,
|
|
482
|
+
...inUse.map((t) => (0, html_1.html) `<a class="dd-item${topic === t.topic ? ' current' : ''}" href="${listUrl(opts.sort, t.topic)}">${topic === t.topic ? (0, icons_1.icon)('check', 'dd-check') : (0, html_1.raw)('<span class="dd-check"></span>')}<span class="dd-label">${t.topic}</span><span class="muted small">${t.count}</span></a>`),
|
|
483
|
+
])}</div></div>
|
|
484
|
+
</details>`
|
|
485
|
+
: '';
|
|
380
486
|
const controls = (0, html_1.html) `<div class="listing-controls">
|
|
381
487
|
<input class="list-filter" type="text" placeholder="Filter repositories" data-target="repo-list" data-filter="cards" aria-label="Filter repositories">
|
|
488
|
+
${topicMenu}
|
|
382
489
|
<span class="seg">${sortLink('recent', 'Recent')}${sortLink('name', 'A–Z')}</span>
|
|
383
490
|
</div>`;
|
|
384
|
-
|
|
491
|
+
// The controls stay once a topic is chosen, whatever the count: a narrowing
|
|
492
|
+
// that hides the way back out would be a trap.
|
|
493
|
+
return (0, html_1.html) `${repos.length > 5 || topic !== '' ? controls : ''}<ul class="repo-grid" id="repo-list">${(0, html_1.joinHtml)(sorted.map((r) => repoCard(r, opts.showCollection)), '\n')}</ul>${noMatches('repo-list')}`;
|
|
385
494
|
}
|
|
386
|
-
function homePage(rootLabel, collections, repos, sort, viewer) {
|
|
495
|
+
function homePage(rootLabel, collections, repos, sort, viewer, topics = { current: '', inUse: [] }) {
|
|
387
496
|
const total = repos.length;
|
|
388
497
|
const chips = collections.length
|
|
389
498
|
? (0, html_1.html) `<nav class="collection-chips" aria-label="Collections">${collections.map((c) => (0, html_1.html) `<a class="coll-chip" href="/${encodeURIComponent(c.name)}">${(0, avatar_1.avatar)(c.name, 18, 'square')}<span>${c.name}</span><span class="coll-count">${c.repoCount}</span></a>`)}</nav>`
|
|
390
499
|
: '';
|
|
391
500
|
const body = total === 0
|
|
392
|
-
?
|
|
393
|
-
|
|
501
|
+
? topics.current !== ''
|
|
502
|
+
? (0, html_1.html) `<div class="empty-state">No repositories carry the topic <b>${topics.current}</b>. <a href="/">Show all</a>.</div>`
|
|
503
|
+
: (0, html_1.html) `<div class="empty-state">No repositories yet.${viewer ? ' Create one with the buttons above, or push to a new path.' : ''}</div>`
|
|
504
|
+
: repoListing(repos, { showCollection: true, sort, sortBase: '/', topics });
|
|
394
505
|
const newBtn = viewer
|
|
395
506
|
? (0, html_1.html) `<a class="btn" href="/new/collection">${(0, icons_1.icon)('plus')}<span>New collection</span></a><a class="btn btn-primary" href="/new">${(0, icons_1.icon)('plus')}<span>New repository</span></a>`
|
|
396
507
|
: '';
|
|
397
|
-
// The about link is how a visitor who landed here cold finds out what they
|
|
398
|
-
// are looking at, so it rides on the first line they read.
|
|
399
508
|
const summary = total === 0
|
|
400
509
|
? ''
|
|
401
|
-
:
|
|
510
|
+
: topics.current !== ''
|
|
511
|
+
? (0, html_1.html) `<p class="lede">${total} ${total === 1 ? 'repository' : 'repositories'} with the topic <b>${topics.current}</b>.</p>`
|
|
512
|
+
: (0, html_1.html) `<p class="lede">${total} ${total === 1 ? 'repository' : 'repositories'} in ${collections.length} ${collections.length === 1 ? 'collection' : 'collections'}.</p>`;
|
|
402
513
|
const content = (0, html_1.html) `<div class="page-head"><h1>Repositories</h1><span class="right-group">${newBtn}</span></div>
|
|
403
514
|
${summary}
|
|
404
515
|
${chips}
|
|
@@ -410,39 +521,21 @@ ${
|
|
|
410
521
|
return layout('Mochi Forge', content, { viewer, path: '/' });
|
|
411
522
|
}
|
|
412
523
|
/**
|
|
413
|
-
* What this place is
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* docs a visitor cannot reach.
|
|
524
|
+
* What this place is, in as few lines as answer the question. A visitor who
|
|
525
|
+
* wants more detail than this has the vault itself and the project on GitHub;
|
|
526
|
+
* a visitor who landed here cold wants one paragraph, not a manual.
|
|
417
527
|
*/
|
|
418
528
|
function aboutPage(baseUrl, viewer) {
|
|
419
529
|
const content = (0, html_1.html) `<div class="about-page">
|
|
420
530
|
<h1>About this vault</h1>
|
|
421
|
-
<p class="lede">This site is a <i>vault</i>: a self-hosted git forge
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<h2>Reading</h2>
|
|
425
|
-
<p>Reading is anonymous. Browse files, history, and diffs; read issues and pull requests; search inside a repository
|
|
426
|
-
(press <span class="mono">/</span> anywhere); download any ref as an archive. Cloning needs no account either:</p>
|
|
531
|
+
<p class="lede">This site is a <i>vault</i>: a small self-hosted git forge, holding git repositories grouped into
|
|
532
|
+
collections.</p>
|
|
533
|
+
<p>Browsing and cloning are anonymous, apart from private repositories:</p>
|
|
427
534
|
${copyRow(`git clone ${baseUrl}/<collection>/<repository>`)}
|
|
428
|
-
|
|
429
|
-
<
|
|
430
|
-
<p>
|
|
431
|
-
|
|
432
|
-
when git asks for credentials, and to <a href="/login">sign in</a> on the web, which unlocks editing files, opening
|
|
433
|
-
issues and pull requests, and drafting releases in the browser.</p>
|
|
434
|
-
<p>Pushing to a repository that does not exist yet creates it, if your token's scope covers that path:</p>
|
|
435
|
-
${copyRow(`git push ${baseUrl}/<collection>/<new-repository> main`)}
|
|
436
|
-
|
|
437
|
-
<h2>What repositories carry</h2>
|
|
438
|
-
<p>Beside its files, a repository here can have issues, pull requests, releases tied to tags, GitHub-style workflows run
|
|
439
|
-
by the operator's own runners, and a static site served from the repository at
|
|
440
|
-
<span class="mono">/<collection>/<repository>/site/</span>. Workflows run without credentials: a vault holds
|
|
441
|
-
no secrets, so jobs are limited to work that needs none, such as building and publishing a site.</p>
|
|
442
|
-
|
|
443
|
-
<h2>The software</h2>
|
|
444
|
-
<p>This vault runs <a href="https://github.com/magland/mochi">Mochi Forge</a>, an open-source forge: one process, no
|
|
445
|
-
database, every repository an ordinary bare git repository on the operator's own disk.</p>
|
|
535
|
+
<p>Writing needs an account, which this vault's administrator creates; there is no sign-up. Your token is the password
|
|
536
|
+
git asks for, and it also <a href="/login">signs you in</a> here.</p>
|
|
537
|
+
<p>The software is <a href="https://github.com/magland/mochiforge">Mochi Forge</a>, open source under the Apache 2.0
|
|
538
|
+
license.</p>
|
|
446
539
|
</div>`;
|
|
447
540
|
return layout('About - Mochi Forge', content, { viewer, path: '/about' });
|
|
448
541
|
}
|
|
@@ -453,19 +546,25 @@ canAdminCollection,
|
|
|
453
546
|
// The collection's profile README, read from its .mochi repository; see
|
|
454
547
|
// src/profile.ts. Rendered above the listing, since it is what the page is
|
|
455
548
|
// for once a collection has one.
|
|
456
|
-
profile = null
|
|
549
|
+
profile = null,
|
|
550
|
+
// Present when a vault user bears the collection's name: the page is then
|
|
551
|
+
// their profile page too, and this is what the profile part shows.
|
|
552
|
+
owner = null, topics = { current: '', inUse: [] }) {
|
|
457
553
|
const body = repoList.length === 0
|
|
458
|
-
?
|
|
554
|
+
? topics.current !== ''
|
|
555
|
+
? (0, html_1.html) `<div class="empty-state">No repositories here carry the topic <b>${topics.current}</b>. <a href="/${encodeURIComponent(collection)}">Show all</a>.</div>`
|
|
556
|
+
: (0, html_1.html) `<div class="empty-state">No repositories in this collection yet.${viewer ? ' Create one with the buttons above, or push to a new path.' : ''}</div>`
|
|
459
557
|
: repoListing(repoList, {
|
|
460
558
|
showCollection: false,
|
|
461
559
|
sort,
|
|
462
560
|
sortBase: `/${encodeURIComponent(collection)}`,
|
|
561
|
+
topics,
|
|
463
562
|
});
|
|
464
563
|
const settingsBtn = canAdminCollection
|
|
465
564
|
? (0, html_1.html) `<a class="btn" href="/${encodeURIComponent(collection)}/settings">${(0, icons_1.icon)('sliders')}<span>Settings</span></a>`
|
|
466
565
|
: '';
|
|
467
566
|
const newBtn = viewer
|
|
468
|
-
? (0, html_1.html) `<a class="btn" href="/import?collection=${encodeURIComponent(collection)}">${(0, icons_1.icon)('download')}<span>Import</span></a><a class="btn btn-primary" href="/new?collection=${encodeURIComponent(collection)}">${(0, icons_1.icon)('plus')}<span>New repository</span></a>`
|
|
567
|
+
? (0, html_1.html) `<a class="btn" href="/import?collection=${encodeURIComponent(collection)}">${(0, icons_1.icon)('download')}<span>Import or fork</span></a><a class="btn btn-primary" href="/new?collection=${encodeURIComponent(collection)}">${(0, icons_1.icon)('plus')}<span>New repository</span></a>`
|
|
469
568
|
: '';
|
|
470
569
|
// The prompt to write a profile goes only to a viewer who could administer
|
|
471
570
|
// the collection, and only while there is none: a page with a profile says
|
|
@@ -475,13 +574,53 @@ profile = null) {
|
|
|
475
574
|
: profile && canAdminCollection
|
|
476
575
|
? (0, html_1.html) `<p class="muted small profile-hint">This collection has no profile README. <a href="${profile.addUrl}">Add one</a> at <span class="mono">.mochi/profile/README.md</span> to introduce it here.</p>`
|
|
477
576
|
: '';
|
|
478
|
-
|
|
577
|
+
// A user's namespace page opens with who they are; a plain collection's
|
|
578
|
+
// with what it is called. The avatar keeps the shape it has everywhere
|
|
579
|
+
// else: round for a person, square for a collection.
|
|
580
|
+
const editProfileBtn = owner?.isViewer
|
|
581
|
+
? (0, html_1.html) `<a class="btn" href="/settings/profile">${(0, icons_1.icon)('pencil')}<span>Edit profile</span></a>`
|
|
582
|
+
: '';
|
|
583
|
+
const head = owner
|
|
584
|
+
? (0, html_1.html) `<div class="page-head"><h1 class="with-avatar">${(0, avatar_1.avatar)(collection, 32)}${owner.displayName ?? collection}${owner.displayName ? (0, html_1.html) ` <span class="profile-username">${collection}</span>` : ''}${owner.siteAdmin ? (0, html_1.html) ` <span class="counter" title="Administers this vault">Admin</span>` : ''}</h1><span class="right-group">${editProfileBtn}${settingsBtn}${newBtn}</span></div>${owner.bio ? (0, html_1.html) `<p class="profile-bio">${owner.bio}</p>` : ''}${owner.links.length
|
|
585
|
+
? (0, html_1.html) `<div class="profile-links">${(0, html_1.joinHtml)(owner.links.map((l) => (0, html_1.html) `<a href="${l}" rel="nofollow noopener noreferrer">${(0, icons_1.icon)('link')}<span>${l.replace(/^https?:\/\//, '')}</span></a>`))}</div>`
|
|
586
|
+
: ''}`
|
|
587
|
+
: (0, html_1.html) `<div class="page-head"><h1 class="with-avatar">${(0, avatar_1.avatar)(collection, 28, 'square')}${collection}</h1><span class="right-group">${settingsBtn}${newBtn}</span></div>`;
|
|
588
|
+
const content = (0, html_1.html) `${head}${profileBox}${body}`;
|
|
479
589
|
return layout(collection, content, {
|
|
480
590
|
crumbs: (0, html_1.html) ` / <a href="/${encodeURIComponent(collection)}">${collection}</a>`,
|
|
481
591
|
viewer,
|
|
482
592
|
path: `/${encodeURIComponent(collection)}`,
|
|
483
593
|
});
|
|
484
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Every topic in use across the vault, as this viewer sees it: the same
|
|
597
|
+
* repositories the front page would list, counted rather than repeated. The
|
|
598
|
+
* page exists so a topic chip has somewhere vault-wide to lead, and so the
|
|
599
|
+
* set of topics can be surveyed without opening repositories to collect it.
|
|
600
|
+
*/
|
|
601
|
+
function topicsIndexPage(topics, viewer) {
|
|
602
|
+
const body = topics.length === 0
|
|
603
|
+
? (0, html_1.html) `<div class="empty-state">No repository carries a topic yet.${viewer ? ' Add some from a repository’s About panel or settings.' : ''}</div>`
|
|
604
|
+
: (0, html_1.html) `<ul class="topic-index">${(0, html_1.joinHtml)(topics.map((t) => (0, html_1.html) `<li><a class="chip topic" href="/topics/${encodeURIComponent(t.topic)}">${t.topic}</a><span class="muted small">${t.count} ${t.count === 1 ? 'repository' : 'repositories'}</span></li>`), '\n')}</ul>`;
|
|
605
|
+
const content = (0, html_1.html) `<div class="page-head"><h1>Topics</h1></div>
|
|
606
|
+
<p class="lede">What the repositories in this vault say they are about, across every collection.</p>
|
|
607
|
+
${body}`;
|
|
608
|
+
return layout('Topics', content, { viewer, path: '/topics' });
|
|
609
|
+
}
|
|
610
|
+
/** One topic's page: every visible repository carrying it, vault-wide. */
|
|
611
|
+
function topicPage(topic, repos, sort, viewer) {
|
|
612
|
+
const body = repos.length === 0
|
|
613
|
+
? (0, html_1.html) `<div class="empty-state">No repository carries this topic. <a href="/topics">All topics</a>.</div>`
|
|
614
|
+
: repoListing(repos, { showCollection: true, sort, sortBase: `/topics/${encodeURIComponent(topic)}` });
|
|
615
|
+
const content = (0, html_1.html) `<div class="page-head"><h1 class="topic-title">${(0, icons_1.icon)('tag')}${topic}</h1></div>
|
|
616
|
+
<p class="lede">${repos.length} ${repos.length === 1 ? 'repository' : 'repositories'} with this topic · <a href="/topics">all topics</a></p>
|
|
617
|
+
${body}`;
|
|
618
|
+
return layout(`${topic} - Topics`, content, {
|
|
619
|
+
crumbs: (0, html_1.html) ` / <a href="/topics">topics</a> / <a href="/topics/${encodeURIComponent(topic)}">${topic}</a>`,
|
|
620
|
+
viewer,
|
|
621
|
+
path: `/topics/${encodeURIComponent(topic)}`,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
485
624
|
/** "1,284" - counts in the interface are grouped, as they are on GitHub. */
|
|
486
625
|
function count(n) {
|
|
487
626
|
return n.toLocaleString('en-US');
|
|
@@ -516,9 +655,9 @@ function languagesBlock(languages) {
|
|
|
516
655
|
const SHOWN_CONTRIBUTORS = 12;
|
|
517
656
|
/**
|
|
518
657
|
* Who wrote this repository, by commit count, as GitHub lists in its About
|
|
519
|
-
* panel.
|
|
520
|
-
*
|
|
521
|
-
* them.
|
|
658
|
+
* panel. A face with an account behind it leads to that user's profile page;
|
|
659
|
+
* one without leads to that identity's commits here, which is all the vault
|
|
660
|
+
* knows about them.
|
|
522
661
|
*/
|
|
523
662
|
function contributorsBlock(ctx, people) {
|
|
524
663
|
if (people.length === 0)
|
|
@@ -529,7 +668,9 @@ function contributorsBlock(ctx, people) {
|
|
|
529
668
|
// git author emails their commits carry.
|
|
530
669
|
const faces = people
|
|
531
670
|
.slice(0, SHOWN_CONTRIBUTORS)
|
|
532
|
-
.map((p) => (0, html_1.html) `<a class="contributor" href="${
|
|
671
|
+
.map((p) => (0, html_1.html) `<a class="contributor" href="${p.account
|
|
672
|
+
? `/${encodeURIComponent(p.account)}`
|
|
673
|
+
: `${base}/commits/${encPath(ctx.ref)}?author=${encodeURIComponent(p.email || p.name)}`}" title="${p.account ?? p.name} - ${count(p.commits)} commit${p.commits === 1 ? '' : 's'}">${(0, avatar_1.avatar)(p.account ?? (p.email || p.name), 28)}</a>`);
|
|
533
674
|
const more = people.length > SHOWN_CONTRIBUTORS
|
|
534
675
|
? (0, html_1.html) `<span class="muted small">+${count(people.length - SHOWN_CONTRIBUTORS)} more</span>`
|
|
535
676
|
: '';
|
|
@@ -557,6 +698,24 @@ function aboutPanel(ctx, view) {
|
|
|
557
698
|
const description = view.description
|
|
558
699
|
? (0, html_1.html) `<p class="side-desc">${view.description}</p>`
|
|
559
700
|
: (0, html_1.html) `<p class="side-desc muted">No description provided.</p>`;
|
|
701
|
+
// Topics, and for a writer the place to change them without leaving the
|
|
702
|
+
// page: a details element holding the one-line form, so the editor costs no
|
|
703
|
+
// script and appears only when opened. The form posts the whole set, which
|
|
704
|
+
// is what the API takes too.
|
|
705
|
+
const topicEditor = ctx.canPush && ctx.viewer
|
|
706
|
+
? (0, html_1.html) `<details class="dropdown topic-edit"><summary class="chip topic-add" title="Edit topics" aria-label="Edit topics">${view.topics.length ? (0, icons_1.icon)('pencil') : (0, html_1.html) `${(0, icons_1.icon)('plus')}<span>Add topics</span>`}</summary>
|
|
707
|
+
<div class="dropdown-menu dd-right topic-edit-menu"><form method="post" action="${repoUrl(ctx)}/settings/topics">
|
|
708
|
+
${csrfField(ctx.viewer)}
|
|
709
|
+
<input type="hidden" name="next" value="repo">
|
|
710
|
+
<label class="muted small" for="side-topics">Topics, separated by spaces</label>
|
|
711
|
+
<input type="text" id="side-topics" name="topics" value="${view.topics.join(' ')}" placeholder="webgpu numbl mri">
|
|
712
|
+
<p class="muted small">Lowercase letters, digits, and hyphens.</p>
|
|
713
|
+
<button type="submit" class="btn btn-primary">${(0, icons_1.icon)('check')}<span>Save</span></button>
|
|
714
|
+
</form></div></details>`
|
|
715
|
+
: '';
|
|
716
|
+
const topicsRow = view.topics.length || topicEditor
|
|
717
|
+
? (0, html_1.html) `<div class="side-topics">${topicChips(view.topics)}${topicEditor}</div>`
|
|
718
|
+
: '';
|
|
560
719
|
const facts = [
|
|
561
720
|
(0, html_1.html) `<a href="${base}/commits/${encPath(ctx.ref)}">${(0, icons_1.icon)('history')}<span>${count(view.commitCount)} commit${view.commitCount === 1 ? '' : 's'}</span></a>`,
|
|
562
721
|
(0, html_1.html) `<a href="${base}/branches">${(0, icons_1.icon)('git-branch')}<span>${count(ctx.branches.length)} branch${ctx.branches.length === 1 ? '' : 'es'}</span></a>`,
|
|
@@ -569,6 +728,7 @@ function aboutPanel(ctx, view) {
|
|
|
569
728
|
<div class="side-block">
|
|
570
729
|
<h3>About${settings}</h3>
|
|
571
730
|
${description}
|
|
731
|
+
${topicsRow}
|
|
572
732
|
${links.length ? (0, html_1.html) `<div class="side-links">${links}</div><hr class="rule">` : ''}
|
|
573
733
|
<div class="side-links">${facts}</div>
|
|
574
734
|
</div>
|
|
@@ -594,7 +754,7 @@ function treePage(ctx, view) {
|
|
|
594
754
|
name = (0, html_1.html) `${FOLDER_ICON}<a href="${refBase}/${encPath(childPath)}">${e.name}</a>`;
|
|
595
755
|
}
|
|
596
756
|
else if (e.type === 'blob') {
|
|
597
|
-
name = (0, html_1.html) `${
|
|
757
|
+
name = (0, html_1.html) `${fileIcon(e.name)}<a href="${base}/blob/${encPath(ctx.ref)}/${encPath(childPath)}">${e.name}</a>`;
|
|
598
758
|
}
|
|
599
759
|
else {
|
|
600
760
|
name = (0, html_1.html) `${FOLDER_ICON}<span>${e.name}</span> <span class="muted small mono">@ ${e.sha.slice(0, 7)}</span>`;
|
|
@@ -611,7 +771,7 @@ function treePage(ctx, view) {
|
|
|
611
771
|
const latest = view.latest;
|
|
612
772
|
const latestBar = latest
|
|
613
773
|
? (0, html_1.html) `<div class="latest-commit">
|
|
614
|
-
<span class="lc-main"
|
|
774
|
+
<span class="lc-main">${gitAuthor(ctx, latest.author, latest.email, { bold: true })} <a href="${base}/commit/${latest.sha}">${latest.subject}</a></span>
|
|
615
775
|
<span class="lc-meta"><a class="sha" href="${base}/commit/${latest.sha}">${latest.sha.slice(0, 7)}</a> ${(0, render_1.timeTag)(latest.date)} <a class="lc-history" href="${base}/commits/${encPath(ctx.ref)}">${(0, icons_1.icon)('history')}<b>${count(view.commitCount)}</b> <span>Commits</span></a></span>
|
|
616
776
|
</div>`
|
|
617
777
|
: '';
|
|
@@ -640,6 +800,7 @@ function treePage(ctx, view) {
|
|
|
640
800
|
? (0, html_1.html) `<div class="box" id="readme"><div class="box-header">${(0, icons_1.icon)('book')}<a href="${base}/blob/${encPath(ctx.ref)}/${encPath(readmePath ?? 'README')}">${view.readmeName ?? 'README'}</a></div><div class="box-body markdown-body">${(0, html_1.raw)(view.readmeHtml)}</div></div>`
|
|
641
801
|
: '';
|
|
642
802
|
const content = (0, html_1.html) `${repoHeader(ctx, 'code')}
|
|
803
|
+
${view.msg ? (0, html_1.html) `<div class="flash">${view.msg}</div>` : ''}
|
|
643
804
|
<div class="toolbar">
|
|
644
805
|
<div class="left">${refPicker(ctx, (ref) => `${base}/tree/${encPath(ref)}`)}${breadcrumb(ctx, path)}</div>
|
|
645
806
|
<div class="right-group">${findButton(ctx)}${historyBtn}${forkBtn}${addFileBtn}${cloneMenu(ctx)}</div>
|
|
@@ -728,7 +889,7 @@ function findFilePage(ctx, paths, total) {
|
|
|
728
889
|
const items = paths.map((p) => {
|
|
729
890
|
const cut = p.lastIndexOf('/');
|
|
730
891
|
const dir = cut === -1 ? '' : (0, html_1.html) `<span class="muted">${p.slice(0, cut + 1)}</span>`;
|
|
731
|
-
return (0, html_1.html) `<a class="find-item" href="${base}/blob/${encPath(ctx.ref)}/${encPath(p)}">${
|
|
892
|
+
return (0, html_1.html) `<a class="find-item" href="${base}/blob/${encPath(ctx.ref)}/${encPath(p)}">${fileIcon(p)}<span>${dir}${p.slice(cut + 1)}</span></a>`;
|
|
732
893
|
});
|
|
733
894
|
const capped = total > paths.length
|
|
734
895
|
? (0, html_1.html) `<p class="muted small">Showing the first ${count(paths.length)} of ${count(total)} files.</p>`
|
|
@@ -747,7 +908,7 @@ function blobPage(ctx, path, view, isMarkdown = false) {
|
|
|
747
908
|
const base = repoUrl(ctx);
|
|
748
909
|
const blobUrl = `${base}/blob/${encPath(ctx.ref)}/${encPath(path)}`;
|
|
749
910
|
const rawUrl = `${base}/raw/${encPath(ctx.ref)}/${encPath(path)}`;
|
|
750
|
-
const editable = (view.kind === 'code' || view.kind === 'markdown') && view.editable;
|
|
911
|
+
const editable = (view.kind === 'code' || view.kind === 'markdown' || view.kind === 'age') && view.editable;
|
|
751
912
|
const editBtns = editable
|
|
752
913
|
? (0, html_1.html) `<a class="btn" href="${base}/edit/${encPath(ctx.ref)}/${encPath(path)}" title="Edit this file">${(0, icons_1.icon)('pencil')}<span>Edit</span></a><a class="btn btn-danger-outline" href="${base}/delete/${encPath(ctx.ref)}/${encPath(path)}" title="Delete this file">${(0, icons_1.icon)('trash')}<span>Delete</span></a>`
|
|
753
914
|
: '';
|
|
@@ -794,6 +955,29 @@ function blobPage(ctx, path, view, isMarkdown = false) {
|
|
|
794
955
|
<p>This file is ${(0, render_1.formatSize)(view.size)}; the repository holds a pointer to it.</p>
|
|
795
956
|
<p class="muted small mono">sha256:${view.oid}</p>
|
|
796
957
|
<p><a class="btn btn-primary" href="${rawUrl}">${(0, icons_1.icon)('download')}<span>Download</span></a></p>
|
|
958
|
+
</div>`;
|
|
959
|
+
}
|
|
960
|
+
else if (view.kind === 'age') {
|
|
961
|
+
// The card is inert markup; /assets/age.js (loaded by this page alone)
|
|
962
|
+
// wires the form, fetches the ciphertext from the raw URL, and decrypts
|
|
963
|
+
// in the page. The passphrase inputs never carry a name, so no form
|
|
964
|
+
// mishap can post one. On unlock the card gives way to a slim bar over
|
|
965
|
+
// the output: what happened, a copy of the exact plaintext, and the lock
|
|
966
|
+
// that puts the card back.
|
|
967
|
+
body = (0, html_1.html) `${meta((0, render_1.formatSize)(view.size))}
|
|
968
|
+
<div class="age-box" data-age-view data-age-raw="${rawUrl}" data-age-inner="${view.markdownInner ? 'markdown' : 'text'}">
|
|
969
|
+
<div class="blob-binary age-card">
|
|
970
|
+
<p class="age-head">${(0, icons_1.icon)('lock')}<b>Encrypted with age</b></p>
|
|
971
|
+
<p class="muted small">The vault stores this file as ciphertext it cannot read. The passphrase decrypts it here, in this page, and is sent nowhere.</p>
|
|
972
|
+
<form class="age-unlock">${agePassInput({ placeholder: 'Passphrase', autocomplete: 'off', required: true })}<button type="submit" class="btn btn-primary" data-busy-label="Decrypting…">Decrypt</button></form>
|
|
973
|
+
<p class="age-error form-error" hidden></p>
|
|
974
|
+
<noscript><p class="muted small">Decrypting in the browser needs JavaScript. Without it, take the raw file and the age CLI.</p></noscript>
|
|
975
|
+
</div>
|
|
976
|
+
<div class="age-bar" data-age-bar hidden>
|
|
977
|
+
<span class="age-bar-note">${(0, icons_1.icon)('lock')}<span>Decrypted in your browser; the vault still holds only the ciphertext.</span></span>
|
|
978
|
+
<span class="age-bar-actions"><button type="button" class="btn" data-age-copy title="Copy the decrypted text"><span class="copy-idle">${(0, icons_1.icon)('copy')}<span>Copy</span></span><span class="copy-done">${(0, icons_1.icon)('check')}<span>Copied</span></span></button><button type="button" class="btn" data-age-lock title="Clear the plaintext from the page">${(0, icons_1.icon)('lock')}<span>Lock</span></button></span>
|
|
979
|
+
</div>
|
|
980
|
+
<div class="age-output" hidden></div>
|
|
797
981
|
</div>`;
|
|
798
982
|
}
|
|
799
983
|
else {
|
|
@@ -804,7 +988,8 @@ function blobPage(ctx, path, view, isMarkdown = false) {
|
|
|
804
988
|
<div class="left">${refPicker(ctx, (ref) => `${base}/blob/${encPath(ref)}/${encPath(path)}`)}${breadcrumb(ctx, path)}</div>
|
|
805
989
|
</div>
|
|
806
990
|
${body}`;
|
|
807
|
-
|
|
991
|
+
const opts = view.kind === 'age' ? { ...repoOpts(ctx, blobUrl), ageScript: true } : repoOpts(ctx, blobUrl);
|
|
992
|
+
return layout(`${path} at ${ctx.ref} - ${ctx.collection}/${ctx.repo}`, content, opts);
|
|
808
993
|
}
|
|
809
994
|
/**
|
|
810
995
|
* The blame view: every line of a file beside the commit that last touched
|
|
@@ -824,7 +1009,7 @@ function blamePage(ctx, path, highlighted, lines, size) {
|
|
|
824
1009
|
? (0, html_1.html) `<a class="blame-prior" href="${base}/blame/${encPath(l.previous.sha)}/${encPath(l.previous.path)}" title="Blame this file before this change" aria-label="Blame this file before this change">${(0, icons_1.icon)('versions')}</a>`
|
|
825
1010
|
: '';
|
|
826
1011
|
const about = starts
|
|
827
|
-
? (0, html_1.html) `<a class="sha" href="${base}/commit/${l.sha}">${l.sha.slice(0, 7)}</a><a class="blame-subject" href="${base}/commit/${l.sha}" title="${l.summary}">${l.summary}</a><span class="blame-when small muted">${l.author} ${(0, render_1.timeTag)(l.date)}</span>${prior}`
|
|
1012
|
+
? (0, html_1.html) `<a class="sha" href="${base}/commit/${l.sha}">${l.sha.slice(0, 7)}</a><a class="blame-subject" href="${base}/commit/${l.sha}" title="${l.summary}">${l.summary}</a><span class="blame-when small muted">${gitAuthor(ctx, l.author, l.email)} ${(0, render_1.timeTag)(l.date)}</span>${prior}`
|
|
828
1013
|
: '';
|
|
829
1014
|
return (0, html_1.html) `<div class="blame-row${starts ? ' blame-start' : ''}" id="L${n}"><span class="blame-commit">${about}</span><a class="lnum" href="#L${n}" aria-label="Line ${n}">${n}</a><span class="ltext">${(0, html_1.raw)(texts[i] ?? '')}</span></div>`;
|
|
830
1015
|
});
|
|
@@ -843,7 +1028,7 @@ function commitsPage(ctx, path, commits, page, totalPages, totalCount, author) {
|
|
|
843
1028
|
const query = author ? `?author=${encodeURIComponent(author)}` : '';
|
|
844
1029
|
// Each row carries what a reader might want next from that commit: to read
|
|
845
1030
|
// it, to take its id, or to browse the tree as it stood then.
|
|
846
|
-
const row = (c) => (0, html_1.html) `<div class="commit-row"><span class="commit-main">${(0, avatar_1.avatar)(c.author, 20)}<span><a class="title" href="${base}/commit/${c.sha}">${c.subject}</a><div class="muted small">${c.author} committed ${(0, render_1.timeTag)(c.date)}</div></span></span><span class="commit-actions"><a class="sha" href="${base}/commit/${c.sha}">${c.sha.slice(0, 7)}</a>${copyButton('', c.sha, 'Copy the full commit id')}<a class="btn" href="${base}/tree/${c.sha}" title="Browse the repository at this commit" aria-label="Browse the repository at this commit">${(0, icons_1.icon)('code')}</a></span></div>`;
|
|
1031
|
+
const row = (c) => (0, html_1.html) `<div class="commit-row"><span class="commit-main">${(0, avatar_1.avatar)((c.email && ctx.accountFor?.(c.email)) || c.email || c.author, 20)}<span><a class="title" href="${base}/commit/${c.sha}">${c.subject}</a><div class="muted small">${gitAuthor(ctx, c.author, c.email)} committed ${(0, render_1.timeTag)(c.date)}</div></span></span><span class="commit-actions"><a class="sha" href="${base}/commit/${c.sha}">${c.sha.slice(0, 7)}</a>${copyButton('', c.sha, 'Copy the full commit id')}<a class="btn" href="${base}/tree/${c.sha}" title="Browse the repository at this commit" aria-label="Browse the repository at this commit">${(0, icons_1.icon)('code')}</a></span></div>`;
|
|
847
1032
|
// Commits are grouped under the day they landed, as on GitHub: a history
|
|
848
1033
|
// reads as a sequence of days, and the dates stop repeating on every row.
|
|
849
1034
|
const groups = [];
|
|
@@ -892,7 +1077,7 @@ function commitPage(ctx, detail, diffHtml) {
|
|
|
892
1077
|
<div class="subject">${subject}</div>
|
|
893
1078
|
${body ? (0, html_1.html) `<div class="body">${body}</div>` : ''}
|
|
894
1079
|
<div class="meta">
|
|
895
|
-
<span
|
|
1080
|
+
<span>${gitAuthor(ctx, detail.author, detail.email, { bold: true })} <${detail.email}></span>
|
|
896
1081
|
<span>committed ${(0, render_1.timeTag)(detail.date, '')}</span>
|
|
897
1082
|
<span>commit <span class="sha">${detail.sha.slice(0, 12)}</span></span>
|
|
898
1083
|
${detail.parents.length ? (0, html_1.html) `<span>parent${detail.parents.length > 1 ? 's' : ''} ${parents}</span>` : ''}
|
package/dist/web.js
CHANGED
|
@@ -58,7 +58,9 @@ const releases_1 = require("./releases");
|
|
|
58
58
|
const perms_1 = require("./perms");
|
|
59
59
|
const scan_1 = require("./scan");
|
|
60
60
|
const session_1 = require("./session");
|
|
61
|
+
const sitesettings_1 = require("./sitesettings");
|
|
61
62
|
const site_1 = require("./site");
|
|
63
|
+
const vault_1 = require("./vault");
|
|
62
64
|
const views = __importStar(require("./views"));
|
|
63
65
|
// Helpers shared by the HTML route modules.
|
|
64
66
|
function ah(fn) {
|
|
@@ -153,6 +155,11 @@ async function makeCtx(root, req, loaded, ref, viewer) {
|
|
|
153
155
|
// Where the Site tab points. A site with an origin of its own is linked there
|
|
154
156
|
// directly rather than through the forge path that would only redirect.
|
|
155
157
|
const siteOrigin = (0, site_1.siteHostUrl)(root, req, loaded.repo.collection, loaded.repo.name);
|
|
158
|
+
// Ties names on the page back to vault users: commit author emails resolve
|
|
159
|
+
// to accounts, and @mentions resolve to profile links. The vault is read
|
|
160
|
+
// once per page; a vault that failed to load just resolves nobody.
|
|
161
|
+
const state = (0, vault_1.loadVault)(root);
|
|
162
|
+
const vault = state.status === 'ok' ? state.vault : null;
|
|
156
163
|
return {
|
|
157
164
|
collection: loaded.repo.collection,
|
|
158
165
|
repo: loaded.repo.name,
|
|
@@ -162,7 +169,7 @@ async function makeCtx(root, req, loaded, ref, viewer) {
|
|
|
162
169
|
branches: loaded.branches,
|
|
163
170
|
tags: loaded.tags,
|
|
164
171
|
cloneUrl,
|
|
165
|
-
hasSite: (0, scan_1.siteDir)(root, loaded.repo.collection, loaded.repo.name) !== null,
|
|
172
|
+
hasSite: (0, sitesettings_1.siteSettings)(loaded.repo.dir).enabled && (0, scan_1.siteDir)(root, loaded.repo.collection, loaded.repo.name) !== null,
|
|
166
173
|
siteUrl: siteOrigin
|
|
167
174
|
? `${siteOrigin}/`
|
|
168
175
|
: `/${encodeURIComponent(loaded.repo.collection)}/${encodeURIComponent(loaded.repo.name)}/site/`,
|
|
@@ -171,10 +178,13 @@ async function makeCtx(root, req, loaded, ref, viewer) {
|
|
|
171
178
|
openIssues: (0, issues_1.issueCounts)(root, loaded.repo.collection, loaded.repo.name).open,
|
|
172
179
|
openPulls: (0, pulls_1.pullCounts)(root, loaded.repo.collection, loaded.repo.name).open,
|
|
173
180
|
forkedFrom: (0, scan_1.forkParent)(loaded.repo.dir),
|
|
181
|
+
upstream: (0, scan_1.upstreamOf)(loaded.repo.dir),
|
|
174
182
|
viewer,
|
|
175
183
|
isPrivate: (0, perms_1.repoIsPrivate)(loaded.repo.dir),
|
|
176
184
|
canPush: (0, perms_1.atLeast)(loaded.role, 'write'),
|
|
177
185
|
canAdmin: (0, perms_1.atLeast)(loaded.role, 'admin'),
|
|
186
|
+
accountFor: (email) => (vault ? (0, vault_1.accountForEmail)(vault, email) : null),
|
|
187
|
+
hasUser: (name) => (vault ? Object.prototype.hasOwnProperty.call(vault.users, name) : false),
|
|
178
188
|
};
|
|
179
189
|
}
|
|
180
190
|
function baseUrlOf(req) {
|