@magland/mochi 0.3.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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
@@ -0,0 +1,461 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerPulls = registerPulls;
37
+ const avatar_1 = require("./avatar");
38
+ const trigger_1 = require("./ci/trigger");
39
+ const diff_1 = require("./diff");
40
+ const git_1 = require("./git");
41
+ const html_1 = require("./html");
42
+ const icons_1 = require("./icons");
43
+ const markdown_1 = require("./markdown");
44
+ const ops_1 = require("./ops");
45
+ const pulls = __importStar(require("./pulls"));
46
+ const render_1 = require("./render");
47
+ const session_1 = require("./session");
48
+ const views_1 = require("./views");
49
+ const web_1 = require("./web");
50
+ // Pull requests: the pages over the store in pulls.ts, and the merge button
51
+ // over ops.mergeBranch.
52
+ //
53
+ // A pull request here is one repository's branch proposed into another of its
54
+ // branches. Across repositories comes with forking, and nothing in the stored
55
+ // shape forbids it later; what it needs is a way to name a head somewhere
56
+ // else, which a vault does not have yet.
57
+ //
58
+ // Who may do what follows the vault's model, as issues do: reading is
59
+ // anonymous, opening one and commenting need a session, closing needs push
60
+ // role or authorship, and merging needs the write role on the repository,
61
+ // because a merge is a write to a branch like any other.
62
+ // Descriptions and comments are the long fields here, as they are on an issue.
63
+ const form = (0, web_1.urlencodedForm)('3mb');
64
+ function pullsUrl(ctx) {
65
+ return `${(0, views_1.repoUrl)(ctx)}/pulls`;
66
+ }
67
+ /** Descriptions and comments are markdown, through the same sanitizing path
68
+ * as a README, with #12 and commit ids linked as they are in an issue. */
69
+ function body(ctx, text) {
70
+ const base = (0, views_1.repoUrl)(ctx);
71
+ const ref = (0, views_1.encPath)(ctx.defaultBranch || ctx.ref || 'HEAD');
72
+ return (0, html_1.raw)((0, markdown_1.renderMarkdown)(text, {
73
+ rawBase: `${base}/raw/${ref}`,
74
+ blobBase: `${base}/blob/${ref}`,
75
+ issueBase: `${base}/issues`,
76
+ commitBase: `${base}/commit`,
77
+ }));
78
+ }
79
+ function stateBadge(state) {
80
+ if (state === 'merged')
81
+ return (0, html_1.html) `<span class="state-badge merged">${(0, icons_1.icon)('git-merge')}<span>Merged</span></span>`;
82
+ if (state === 'closed')
83
+ return (0, html_1.html) `<span class="state-badge closed">${(0, icons_1.icon)('git-pull-request-closed')}<span>Closed</span></span>`;
84
+ return (0, html_1.html) `<span class="state-badge open">${(0, icons_1.icon)('git-pull-request')}<span>Open</span></span>`;
85
+ }
86
+ function stateIcon(state) {
87
+ if (state === 'merged')
88
+ return (0, icons_1.icon)('git-merge', 'pull-merged');
89
+ if (state === 'closed')
90
+ return (0, icons_1.icon)('git-pull-request-closed', 'issue-closed');
91
+ return (0, icons_1.icon)('git-pull-request', 'issue-open');
92
+ }
93
+ function branchPair(ctx, pull) {
94
+ const base = (0, views_1.repoUrl)(ctx);
95
+ const chip = (ref) => (0, html_1.html) `<a class="chip mono" href="${base}/tree/${(0, views_1.encPath)(ref)}">${ref}</a>`;
96
+ return (0, html_1.html) `${chip(pull.base)} <span class="muted">&larr;</span> ${chip(pull.head)}`;
97
+ }
98
+ function listPage(ctx, list, state, counts) {
99
+ const base = pullsUrl(ctx);
100
+ const tab = (id, label, n, glyph) => (0, html_1.html) `<a class="state-tab${state === id ? ' current' : ''}" href="${base}?state=${id}">${(0, icons_1.icon)(glyph)}<span>${n} ${label}</span></a>`;
101
+ const rows = list.map((p) => (0, html_1.html) `<tr>
102
+ <td class="issue-cell">${stateIcon(p.state)}<span>
103
+ <a class="issue-link" href="${base}/${p.number}">${p.title}</a>
104
+ <div class="muted small">#${p.number} opened ${(0, render_1.timeTag)(p.created)} by ${p.author} &middot; ${branchPair(ctx, p)}</div></span></td>
105
+ <td class="right muted small">${p.comments > 0
106
+ ? (0, html_1.html) `<a class="issue-comments" href="${base}/${p.number}" title="${p.comments} comment${p.comments === 1 ? '' : 's'}">${(0, icons_1.icon)('comment')}<span>${p.comments}</span></a>`
107
+ : ''}</td>
108
+ </tr>`);
109
+ const newBtn = ctx.viewer
110
+ ? (0, html_1.html) `<a class="btn btn-primary" href="${base}/new">${(0, icons_1.icon)('git-pull-request')}<span>New pull request</span></a>`
111
+ : (0, html_1.html) `<a class="btn" href="/login?next=${encodeURIComponent(`${base}/new`)}">${(0, icons_1.icon)('git-pull-request')}<span>New pull request</span></a>`;
112
+ const empty = state === 'closed'
113
+ ? 'Nothing closed or merged yet.'
114
+ : state === 'open'
115
+ ? 'No open pull requests. Push a branch and propose it here.'
116
+ : 'No pull requests yet.';
117
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'pulls')}
118
+ <div class="page-head">
119
+ <span class="state-filter">${tab('open', 'Open', counts.open, 'git-pull-request')}${tab('closed', 'Closed', counts.closed, 'git-merge')}</span>
120
+ ${newBtn}
121
+ </div>
122
+ ${rows.length
123
+ ? (0, html_1.html) `<table class="listing issues"><tbody>${rows}</tbody></table>`
124
+ : (0, html_1.html) `<div class="empty-state">${empty}</div>`}`;
125
+ return (0, views_1.layout)(`Pull requests - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, base));
126
+ }
127
+ function commentCard(author, when, rendered, note = '') {
128
+ return (0, html_1.html) `<article class="issue-comment">
129
+ <div class="issue-comment-head">${(0, avatar_1.avatar)(author, 24)}<b>${author}</b><span class="muted small">${note || 'commented'} ${(0, render_1.timeTag)(when)}</span></div>
130
+ <div class="issue-comment-body markdown-body">${rendered}</div>
131
+ </article>`;
132
+ }
133
+ /** The box that says whether this can be merged, and offers to do it. */
134
+ function mergeBox(ctx, pull, preview, canMerge) {
135
+ const viewer = ctx.viewer;
136
+ if (pull.state === 'merged') {
137
+ // A merged branch has done its work; GitHub offers to sweep it away here
138
+ // and so does this, for anyone who may push and for any branch but the
139
+ // one it was merged into.
140
+ const stale = ctx.branches.some((b) => b.name === pull.head) && pull.head !== ctx.defaultBranch;
141
+ const sweep = stale && ctx.canPush && viewer
142
+ ? (0, html_1.html) `<form method="post" action="${pullsUrl(ctx)}/${pull.number}/delete-branch" data-confirm="Delete the branch ${pull.head}?">${(0, views_1.csrfField)(viewer)}<button class="btn" type="submit">${(0, icons_1.icon)('trash')}<span>Delete branch ${pull.head}</span></button></form>`
143
+ : '';
144
+ return (0, html_1.html) `<div class="merge-box merged">${(0, icons_1.icon)('git-merge')}<div><b>Merged</b><div class="muted small">${pull.mergedBy ?? 'someone'} merged this ${pull.mergedAt ? (0, render_1.timeTag)(pull.mergedAt) : ''}${pull.mergeSha
145
+ ? (0, html_1.html) ` as <a class="sha" href="${(0, views_1.repoUrl)(ctx)}/commit/${pull.mergeSha}">${pull.mergeSha.slice(0, 7)}</a>`
146
+ : ''}</div></div>${sweep}</div>`;
147
+ }
148
+ if (pull.state === 'closed') {
149
+ return (0, html_1.html) `<div class="merge-box closed">${(0, icons_1.icon)('git-pull-request-closed')}<div><b>Closed without merging</b><div class="muted small">${pull.closedBy ?? 'someone'} closed this ${pull.closedAt ? (0, render_1.timeTag)(pull.closedAt) : ''}</div></div></div>`;
150
+ }
151
+ if (!preview) {
152
+ return (0, html_1.html) `<div class="merge-box unknown">${(0, icons_1.icon)('alert')}<div><b>This branch cannot be compared</b><div class="muted small">One of ${pull.base} and ${pull.head} no longer exists.</div></div></div>`;
153
+ }
154
+ if (preview.status === 'up-to-date') {
155
+ return (0, html_1.html) `<div class="merge-box unknown">${(0, icons_1.icon)('check')}<div><b>Nothing to merge</b><div class="muted small">${pull.base} already contains everything on ${pull.head}.</div></div></div>`;
156
+ }
157
+ if (preview.status === 'conflict') {
158
+ const paths = preview.paths.slice(0, 20).map((p) => (0, html_1.html) `<li class="mono">${p}</li>`);
159
+ return (0, html_1.html) `<div class="merge-box conflict">${(0, icons_1.icon)('x')}<div><b>This branch has conflicts that must be resolved</b>
160
+ <div class="muted small">Merge ${pull.base} into ${pull.head} where you work, resolve them, and push.</div>
161
+ <ul class="merge-conflicts">${paths}</ul></div></div>`;
162
+ }
163
+ // How to merge is a choice, as it is on GitHub: keep both parents and the
164
+ // shape of the branch, or land it as one commit.
165
+ const button = canMerge && viewer
166
+ ? (0, html_1.html) `<form class="merge-do" method="post" action="${pullsUrl(ctx)}/${pull.number}/merge">${(0, views_1.csrfField)(viewer)}
167
+ <label class="merge-method"><select name="method"><option value="merge">Merge commit</option><option value="squash">Squash and merge</option></select></label>
168
+ <button class="btn btn-primary" type="submit">${(0, icons_1.icon)('git-merge')}<span>Merge pull request</span></button></form>`
169
+ : (0, html_1.html) `<span class="muted small">${viewer ? 'Merging needs push access to this repository.' : 'Sign in with push access to merge.'}</span>`;
170
+ return (0, html_1.html) `<div class="merge-box clean">${(0, icons_1.icon)('check')}<div><b>This branch has no conflicts with ${pull.base}</b><div class="muted small">${preview.fastForward
171
+ ? 'It can be merged by moving the branch forward.'
172
+ : 'Merging creates a merge commit on the base branch.'}</div></div>${button}</div>`;
173
+ }
174
+ function pullPage(ctx, pull, view) {
175
+ const base = pullsUrl(ctx);
176
+ const repo = (0, views_1.repoUrl)(ctx);
177
+ const viewer = ctx.viewer;
178
+ const mine = viewer !== null && viewer.auth.username === pull.author;
179
+ const canClose = ctx.canPush || mine;
180
+ const comments = pull.commentList.map((c) => commentCard(c.author, c.created, body(ctx, c.body)));
181
+ let replyBox;
182
+ if (viewer) {
183
+ const toggle = canClose && pull.state !== 'merged'
184
+ ? (0, html_1.html) `<button class="btn" type="submit" name="state" value="${pull.state === 'open' ? 'closed' : 'open'}" formaction="${base}/${pull.number}/state">${(0, icons_1.icon)(pull.state === 'open' ? 'git-pull-request-closed' : 'git-pull-request')}<span>${pull.state === 'open' ? 'Close pull request' : 'Reopen'}</span></button>`
185
+ : '';
186
+ replyBox = (0, html_1.html) `<form class="issue-reply" method="post" action="${base}/${pull.number}/comment">${(0, views_1.csrfField)(viewer)}
187
+ <div class="issue-comment-head">${(0, avatar_1.avatar)(viewer.auth.username, 24)}<b>${viewer.auth.username}</b></div>
188
+ <textarea class="code-editor" name="body" rows="6" placeholder="Leave a comment"></textarea>
189
+ <div class="actions">${toggle}<button class="btn btn-primary" type="submit">Comment</button></div>
190
+ </form>`;
191
+ }
192
+ else {
193
+ replyBox = (0, html_1.html) `<p class="muted"><a href="/login?next=${encodeURIComponent(`${base}/${pull.number}`)}">Sign in</a> to comment.</p>`;
194
+ }
195
+ const commitRows = view.commits.map((c) => (0, html_1.html) `<div class="commit-row"><span class="commit-main"><a class="title" href="${repo}/commit/${c.sha}">${c.subject}</a><div class="muted small">${c.author} committed ${(0, render_1.timeTag)(c.date)}</div></span><a class="sha" href="${repo}/commit/${c.sha}">${c.sha.slice(0, 7)}</a></div>`);
196
+ const description = body(ctx, pull.body);
197
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'pulls')}
198
+ <div class="issue-head">
199
+ <h1 class="issue-title">${pull.title} <span class="muted">#${pull.number}</span></h1>
200
+ <span class="right-group"><a class="btn" href="${base}">Back to pull requests</a></span>
201
+ </div>
202
+ <div class="issue-sub">${stateBadge(pull.state)}<span class="muted"><b>${pull.author}</b> wants to merge ${view.ahead} commit${view.ahead === 1 ? '' : 's'} into </span>${branchPair(ctx, pull)}</div>
203
+ ${mergeBox(ctx, pull, view.preview, ctx.canPush)}
204
+ <div class="issue-thread">
205
+ ${commentCard(pull.author, pull.created, description.text ? description : (0, html_1.raw)('<p class="muted">No description.</p>'), 'opened this')}
206
+ ${comments}
207
+ ${replyBox}
208
+ </div>
209
+ <div class="box"><div class="box-header">${(0, icons_1.icon)('git-commit')}<span>${view.commits.length} commit${view.commits.length === 1 ? '' : 's'}</span></div><div class="box-body pull-commits">${commitRows.length ? commitRows : (0, html_1.raw)('<p class="muted">Nothing to merge.</p>')}</div></div>
210
+ <h2 class="pull-files">Files changed</h2>
211
+ ${(0, html_1.raw)((0, diff_1.renderDiff)(view.patch, { blobBase: `${repo}/blob/${(0, views_1.encPath)(pull.head)}` }))}`;
212
+ return (0, views_1.layout)(`${pull.title} · #${pull.number} - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, `${base}/${pull.number}`));
213
+ }
214
+ function newPage(ctx, viewer, values, info, error) {
215
+ const base = pullsUrl(ctx);
216
+ const options = (selected) => ctx.branches.map((b) => (0, html_1.html) `<option value="${b.name}"${b.name === selected ? (0, html_1.raw)(' selected') : ''}>${b.name}</option>`);
217
+ const content = (0, html_1.html) `${(0, views_1.repoHeader)(ctx, 'pulls')}
218
+ <div class="form-box wide">
219
+ <h1>New pull request</h1>
220
+ ${error ? (0, html_1.html) `<div class="form-error">${error}</div>` : ''}
221
+ <form method="get" action="${base}/new" class="cmp-form">
222
+ ${(0, icons_1.icon)('git-compare')}<label class="cmp-picker">base <select name="base">${options(values.base)}</select></label>
223
+ <span class="muted">&larr;</span>
224
+ <label class="cmp-picker">compare <select name="head">${options(values.head)}</select></label>
225
+ <button class="btn" type="submit">Change branches</button>
226
+ </form>
227
+ ${info
228
+ ? (0, html_1.html) `<p class="muted">${info.ahead} commit${info.ahead === 1 ? '' : 's'} would be merged into <b>${values.base}</b> from <b>${values.head}</b>.</p>`
229
+ : ''}
230
+ <form method="post" action="${base}/new">${(0, views_1.csrfField)(viewer)}
231
+ <input type="hidden" name="base" value="${values.base}">
232
+ <input type="hidden" name="head" value="${values.head}">
233
+ <div class="field"><label for="title">Title</label>
234
+ <input type="text" id="title" name="title" value="${values.title}" maxlength="${pulls.MAX_TITLE}" required autofocus></div>
235
+ <div class="field"><label for="body">Description</label>
236
+ <textarea class="code-editor" id="body" name="body" rows="10" placeholder="What this changes, and why. Markdown is welcome.">${values.body}</textarea></div>
237
+ <div class="actions"><button class="btn btn-primary" type="submit">${(0, icons_1.icon)('git-pull-request')}<span>Create pull request</span></button><a class="btn" href="${base}">Cancel</a></div>
238
+ </form>
239
+ </div>`;
240
+ return (0, views_1.layout)(`New pull request - ${ctx.collection}/${ctx.repo}`, content, (0, views_1.repoOpts)(ctx, `${base}/new`));
241
+ }
242
+ function registerPulls(app, root, engine) {
243
+ /** The repository context plus the pull request named in the URL. */
244
+ async function withPull(req, res, viewer) {
245
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
246
+ if (!loaded)
247
+ return null;
248
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
249
+ if (!/^[1-9][0-9]*$/.test(req.params.n ?? '')) {
250
+ (0, web_1.send404)(res, 'No such pull request', viewer);
251
+ return null;
252
+ }
253
+ const n = parseInt(req.params.n, 10);
254
+ const pull = pulls.readPull(root, ctx.collection, ctx.repo, n);
255
+ if (!pull) {
256
+ (0, web_1.send404)(res, `Pull request #${n} does not exist in ${ctx.collection}/${ctx.repo}`, viewer);
257
+ return null;
258
+ }
259
+ return { ctx, pull, loaded };
260
+ }
261
+ app.get('/:collection/:repo/pulls', (0, web_1.ah)(async (req, res) => {
262
+ const viewer = (0, session_1.getViewer)(req, root);
263
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
264
+ if (!loaded)
265
+ return;
266
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
267
+ const asked = String(req.query.state ?? 'open');
268
+ const state = asked === 'closed' ? 'closed' : asked === 'all' ? 'all' : 'open';
269
+ const all = pulls.listPulls(root, ctx.collection, ctx.repo);
270
+ const counts = {
271
+ open: all.filter((p) => p.state === 'open').length,
272
+ closed: all.filter((p) => p.state !== 'open').length,
273
+ };
274
+ const list = state === 'all' ? all : all.filter((p) => (state === 'open' ? p.state === 'open' : p.state !== 'open'));
275
+ res.type('html').send(listPage(ctx, list, state, counts));
276
+ }));
277
+ app.get('/:collection/:repo/pulls/new', (0, web_1.ah)(async (req, res) => {
278
+ const viewer = (0, web_1.requireViewerPage)(root, req, res);
279
+ if (!viewer)
280
+ return;
281
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
282
+ if (!loaded)
283
+ return;
284
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
285
+ const names = ctx.branches.map((b) => b.name);
286
+ const asked = (name, fallback) => {
287
+ const value = String(req.query[name] ?? '');
288
+ return names.includes(value) ? value : fallback;
289
+ };
290
+ const baseRef = asked('base', ctx.defaultBranch || names[0] || '');
291
+ const headRef = asked('head', names.find((n) => n !== baseRef) ?? baseRef);
292
+ if (names.length < 2) {
293
+ (0, web_1.fail)(res, 400, 'A pull request needs two branches; this repository has fewer.', viewer, (0, views_1.repoUrl)(ctx));
294
+ return;
295
+ }
296
+ const cmp = await loaded.repo.compare(baseRef, headRef);
297
+ res.type('html').send(newPage(ctx, viewer, {
298
+ base: baseRef,
299
+ head: headRef,
300
+ // The first commit's subject is what people would type anyway.
301
+ title: cmp.commits.length === 1 ? cmp.commits[0].subject : '',
302
+ body: '',
303
+ }, { ahead: cmp.ahead, commits: cmp.commits }));
304
+ }));
305
+ app.post('/:collection/:repo/pulls/new', form, (0, web_1.ah)(async (req, res) => {
306
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
307
+ if (!viewer)
308
+ return;
309
+ const loaded = await (0, web_1.loadRepo)(root, req, res, viewer);
310
+ if (!loaded)
311
+ return;
312
+ const ctx = await (0, web_1.makeCtx)(root, req, loaded, loaded.defaultBranch ?? '', viewer);
313
+ const baseRef = (0, web_1.field)(req, 'base');
314
+ const headRef = (0, web_1.field)(req, 'head');
315
+ const title = (0, web_1.field)(req, 'title');
316
+ const text = (0, web_1.field)(req, 'body');
317
+ try {
318
+ const created = pulls.openPull(root, ctx.collection, ctx.repo, { title, body: text, author: viewer.auth.username, base: baseRef, head: headRef }, ctx.branches.map((b) => b.name));
319
+ res.redirect(`${pullsUrl(ctx)}/${created.number}`);
320
+ }
321
+ catch (e) {
322
+ const message = e instanceof ops_1.OpError ? e.message : 'Could not open the pull request.';
323
+ res
324
+ .status(400)
325
+ .type('html')
326
+ .send(newPage(ctx, viewer, { base: baseRef, head: headRef, title, body: text }, null, message));
327
+ }
328
+ }));
329
+ app.get('/:collection/:repo/pulls/:n', (0, web_1.ah)(async (req, res) => {
330
+ const viewer = (0, session_1.getViewer)(req, root);
331
+ const found = await withPull(req, res, viewer);
332
+ if (!found)
333
+ return;
334
+ const { ctx, pull, loaded } = found;
335
+ const known = (ref) => ctx.branches.some((b) => b.name === ref);
336
+ let view = { commits: [], patch: '', ahead: 0, preview: null };
337
+ if ((0, git_1.isValidRefName)(pull.base) && (0, git_1.isValidRefName)(pull.head) && known(pull.base) && known(pull.head)) {
338
+ const cmp = await loaded.repo.compare(pull.base, pull.head);
339
+ // A merged or closed pull request is history: what it did is on the
340
+ // base branch already, so no merge is planned for it.
341
+ const preview = pull.state === 'open' ? await (0, ops_1.previewMerge)(loaded.repo.dir, pull.base, pull.head).catch(() => null) : null;
342
+ view = { commits: cmp.commits, patch: cmp.patch, ahead: cmp.ahead, preview };
343
+ }
344
+ res.type('html').send(pullPage(ctx, pull, view));
345
+ }));
346
+ app.post('/:collection/:repo/pulls/:n/comment', form, (0, web_1.ah)(async (req, res) => {
347
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
348
+ if (!viewer)
349
+ return;
350
+ const found = await withPull(req, res, viewer);
351
+ if (!found)
352
+ return;
353
+ const { ctx, pull } = found;
354
+ try {
355
+ pulls.addComment(root, ctx.collection, ctx.repo, pull.number, {
356
+ author: viewer.auth.username,
357
+ body: (0, web_1.field)(req, 'body'),
358
+ });
359
+ }
360
+ catch (e) {
361
+ (0, web_1.fail)(res, 400, e instanceof ops_1.OpError ? e.message : 'Could not add the comment.', viewer, `${pullsUrl(ctx)}/${pull.number}`);
362
+ return;
363
+ }
364
+ res.redirect(`${pullsUrl(ctx)}/${pull.number}`);
365
+ }));
366
+ app.post('/:collection/:repo/pulls/:n/state', form, (0, web_1.ah)(async (req, res) => {
367
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
368
+ if (!viewer)
369
+ return;
370
+ const found = await withPull(req, res, viewer);
371
+ if (!found)
372
+ return;
373
+ const { ctx, pull } = found;
374
+ if (!ctx.canPush && viewer.auth.username !== pull.author) {
375
+ (0, web_1.fail)(res, 403, 'Only the author or someone with push access can close this pull request.', viewer, `${pullsUrl(ctx)}/${pull.number}`);
376
+ return;
377
+ }
378
+ const state = (0, web_1.field)(req, 'state') === 'closed' ? 'closed' : 'open';
379
+ const text = (0, web_1.field)(req, 'body').trim();
380
+ if (text !== '') {
381
+ try {
382
+ pulls.addComment(root, ctx.collection, ctx.repo, pull.number, {
383
+ author: viewer.auth.username,
384
+ body: text,
385
+ });
386
+ }
387
+ catch (e) {
388
+ // As in the issue route: a comment the writer got wrong must not stop
389
+ // the state change, but a failure to write at all should surface.
390
+ if (!(e instanceof ops_1.OpError))
391
+ throw e;
392
+ }
393
+ }
394
+ try {
395
+ pulls.setPullState(root, ctx.collection, ctx.repo, pull.number, state, viewer.auth.username);
396
+ }
397
+ catch (e) {
398
+ (0, web_1.fail)(res, 400, e instanceof ops_1.OpError ? e.message : 'Could not change the state.', viewer, `${pullsUrl(ctx)}/${pull.number}`);
399
+ return;
400
+ }
401
+ res.redirect(`${pullsUrl(ctx)}/${pull.number}`);
402
+ }));
403
+ app.post('/:collection/:repo/pulls/:n/delete-branch', form, (0, web_1.ah)(async (req, res) => {
404
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
405
+ if (!viewer)
406
+ return;
407
+ const found = await withPull(req, res, viewer);
408
+ if (!found)
409
+ return;
410
+ const { ctx, pull, loaded } = found;
411
+ const back = `${pullsUrl(ctx)}/${pull.number}`;
412
+ if (!ctx.canPush) {
413
+ (0, web_1.fail)(res, 403, `You do not have push access to ${ctx.collection}/${ctx.repo}.`, viewer, back);
414
+ return;
415
+ }
416
+ try {
417
+ await pulls.deletePullBranch(root, loaded.repo, pull.number, { defaultBranch: ctx.defaultBranch });
418
+ }
419
+ catch (e) {
420
+ (0, web_1.fail)(res, 400, e instanceof ops_1.OpError ? e.message : 'Could not delete the branch.', viewer, back);
421
+ return;
422
+ }
423
+ res.redirect(back);
424
+ }));
425
+ app.post('/:collection/:repo/pulls/:n/merge', form, (0, web_1.ah)(async (req, res) => {
426
+ const viewer = (0, web_1.requireViewerPost)(root, req, res);
427
+ if (!viewer)
428
+ return;
429
+ const found = await withPull(req, res, viewer);
430
+ if (!found)
431
+ return;
432
+ const { ctx, pull, loaded } = found;
433
+ const back = `${pullsUrl(ctx)}/${pull.number}`;
434
+ // Merging writes to a branch, so it takes the write role on the
435
+ // repository: authorship is not enough, as it is for closing.
436
+ if (!ctx.canPush) {
437
+ (0, web_1.fail)(res, 403, `You do not have push access to ${ctx.collection}/${ctx.repo}.`, viewer, back);
438
+ return;
439
+ }
440
+ if (pull.state !== 'open') {
441
+ (0, web_1.fail)(res, 400, 'This pull request is not open.', viewer, back);
442
+ return;
443
+ }
444
+ const method = (0, web_1.field)(req, 'method') === 'squash' ? 'squash' : 'merge';
445
+ const host = (req.get('host') ?? 'localhost').replace(/:\d+$/, '');
446
+ let result;
447
+ try {
448
+ result = await pulls.mergePull(root, loaded.repo, pull.number, { username: viewer.auth.username, email: `${viewer.auth.username}@noreply.${host}` }, { method }, { defaultBranch: ctx.defaultBranch });
449
+ }
450
+ catch (e) {
451
+ if (e instanceof pulls.MergeConflict) {
452
+ (0, web_1.fail)(res, 409, e.message, viewer, back);
453
+ return;
454
+ }
455
+ (0, web_1.fail)(res, 400, e instanceof ops_1.OpError ? e.message : 'The merge failed.', viewer, back);
456
+ return;
457
+ }
458
+ (0, trigger_1.firePush)(root, engine, { collection: ctx.collection, name: ctx.repo }, pull.base, result.before, result.sha, viewer.auth.username);
459
+ res.redirect(back);
460
+ }));
461
+ }