@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,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerIssueApi = registerIssueApi;
4
+ const issues_1 = require("../issues");
5
+ const auth_1 = require("./auth");
6
+ // Issues. The domain functions in src/issues.ts do all of the work; this is a
7
+ // transport over them, and the response objects are the IssueSummary and Issue
8
+ // interfaces unchanged, so a field added to an issue's state appears here without
9
+ // a decision being made about it.
10
+ //
11
+ // Who the author is comes from the bearer token, never from the body, on any
12
+ // route, ever. Reading an issue takes any valid token; writing one takes push
13
+ // scope over the repository, which is the same rule the web applies.
14
+ const MAX_LISTED = 500;
15
+ function issueNumber(raw) {
16
+ return /^[1-9][0-9]*$/.test(raw ?? '') ? parseInt(raw, 10) : null;
17
+ }
18
+ function registerIssueApi(app, root, limiter) {
19
+ // The labels route is registered before :n so that /issues/labels is not read
20
+ // as issue number "labels".
21
+ app.get('/api/repos/:collection/:repo/issues/labels', (req, res) => {
22
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
23
+ if (!found)
24
+ return;
25
+ const all = (0, issues_1.listIssues)(root, found.repo.collection, found.repo.name);
26
+ res.json({ labels: (0, issues_1.labelsInUse)(all), authors: (0, issues_1.authorsInUse)(all) });
27
+ });
28
+ app.get('/api/repos/:collection/:repo/issues', (req, res) => {
29
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
30
+ if (!found)
31
+ return;
32
+ const stateAsked = String(req.query.state ?? 'open');
33
+ if (!['open', 'closed', 'all'].includes(stateAsked)) {
34
+ (0, auth_1.apiError)(res, 400, 'state must be open, closed, or all');
35
+ return;
36
+ }
37
+ const sortAsked = String(req.query.sort ?? 'newest');
38
+ if (!issues_1.ISSUE_SORTS.some((s) => s.key === sortAsked)) {
39
+ (0, auth_1.apiError)(res, 400, `sort must be one of: ${issues_1.ISSUE_SORTS.map((s) => s.key).join(', ')}`);
40
+ return;
41
+ }
42
+ const all = (0, issues_1.listIssues)(root, found.repo.collection, found.repo.name, {
43
+ match: String(req.query.q ?? '') || undefined,
44
+ });
45
+ const selected = (0, issues_1.selectIssues)(all, {
46
+ state: stateAsked,
47
+ label: String(req.query.label ?? '') || undefined,
48
+ author: String(req.query.author ?? '') || undefined,
49
+ sort: sortAsked,
50
+ });
51
+ const limit = (0, auth_1.limitParam)(req.query.limit, MAX_LISTED, MAX_LISTED);
52
+ res.json({ issues: selected.slice(0, limit), total: selected.length });
53
+ });
54
+ app.get('/api/repos/:collection/:repo/issues/:n', (req, res) => {
55
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
56
+ if (!found)
57
+ return;
58
+ const n = issueNumber(req.params.n);
59
+ const issue = n === null ? null : (0, issues_1.readIssue)(root, found.repo.collection, found.repo.name, n);
60
+ if (!issue) {
61
+ (0, auth_1.apiError)(res, 404, `no issue ${req.params.n} in ${found.repo.collection}/${found.repo.name}`);
62
+ return;
63
+ }
64
+ res.json(issue);
65
+ });
66
+ app.post('/api/repos/:collection/:repo/issues', (req, res) => {
67
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
68
+ if (!found)
69
+ return;
70
+ const body = (0, auth_1.bodyOf)(req);
71
+ const title = (0, auth_1.stringField)(body, 'title');
72
+ if (title === null) {
73
+ (0, auth_1.apiError)(res, 400, 'a "title" is required');
74
+ return;
75
+ }
76
+ const labels = (0, auth_1.stringsField)(body, 'labels');
77
+ if (labels === null) {
78
+ (0, auth_1.apiError)(res, 400, '"labels" must be a list of strings');
79
+ return;
80
+ }
81
+ try {
82
+ // The author is the token's user. No author field is read from the body.
83
+ const created = (0, issues_1.createIssue)(root, found.repo.collection, found.repo.name, {
84
+ title,
85
+ body: (0, auth_1.stringField)(body, 'body') ?? '',
86
+ author: found.actor.username,
87
+ labels,
88
+ });
89
+ res.status(201).json(created);
90
+ }
91
+ catch (e) {
92
+ (0, auth_1.sendOpError)(res, e, 'could not create the issue');
93
+ }
94
+ });
95
+ app.patch('/api/repos/:collection/:repo/issues/:n', (req, res) => {
96
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
97
+ if (!found)
98
+ return;
99
+ const n = issueNumber(req.params.n);
100
+ if (n === null) {
101
+ (0, auth_1.apiError)(res, 404, `no issue ${req.params.n}`);
102
+ return;
103
+ }
104
+ const body = (0, auth_1.bodyOf)(req);
105
+ const labels = (0, auth_1.stringsField)(body, 'labels');
106
+ if (labels === null) {
107
+ (0, auth_1.apiError)(res, 400, '"labels" must be a list of strings');
108
+ return;
109
+ }
110
+ try {
111
+ (0, issues_1.editIssue)(root, found.repo.collection, found.repo.name, n, {
112
+ title: (0, auth_1.stringField)(body, 'title') ?? undefined,
113
+ body: (0, auth_1.stringField)(body, 'body') ?? undefined,
114
+ labels,
115
+ });
116
+ res.json((0, issues_1.readIssue)(root, found.repo.collection, found.repo.name, n));
117
+ }
118
+ catch (e) {
119
+ (0, auth_1.sendOpError)(res, e, 'could not edit the issue');
120
+ }
121
+ });
122
+ app.post('/api/repos/:collection/:repo/issues/:n/comments', (req, res) => {
123
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
124
+ if (!found)
125
+ return;
126
+ const n = issueNumber(req.params.n);
127
+ const text = (0, auth_1.stringField)((0, auth_1.bodyOf)(req), 'body');
128
+ if (n === null) {
129
+ (0, auth_1.apiError)(res, 404, `no issue ${req.params.n}`);
130
+ return;
131
+ }
132
+ if (text === null) {
133
+ (0, auth_1.apiError)(res, 400, 'a "body" is required');
134
+ return;
135
+ }
136
+ try {
137
+ res.status(201).json((0, issues_1.addComment)(root, found.repo.collection, found.repo.name, n, { author: found.actor.username, body: text }));
138
+ }
139
+ catch (e) {
140
+ (0, auth_1.sendOpError)(res, e, 'could not add the comment');
141
+ }
142
+ });
143
+ app.post('/api/repos/:collection/:repo/issues/:n/state', (req, res) => {
144
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
145
+ if (!found)
146
+ return;
147
+ const n = issueNumber(req.params.n);
148
+ const state = (0, auth_1.stringField)((0, auth_1.bodyOf)(req), 'state');
149
+ if (n === null) {
150
+ (0, auth_1.apiError)(res, 404, `no issue ${req.params.n}`);
151
+ return;
152
+ }
153
+ if (state !== 'open' && state !== 'closed') {
154
+ (0, auth_1.apiError)(res, 400, '"state" must be "open" or "closed"');
155
+ return;
156
+ }
157
+ try {
158
+ (0, issues_1.setIssueState)(root, found.repo.collection, found.repo.name, n, state, found.actor.username);
159
+ res.json((0, issues_1.readIssue)(root, found.repo.collection, found.repo.name, n));
160
+ }
161
+ catch (e) {
162
+ (0, auth_1.sendOpError)(res, e, 'could not change the state');
163
+ }
164
+ });
165
+ }
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerPullApi = registerPullApi;
4
+ const trigger_1 = require("../ci/trigger");
5
+ const git_1 = require("../git");
6
+ const ops_1 = require("../ops");
7
+ const pulls_1 = require("../pulls");
8
+ const auth_1 = require("./auth");
9
+ // Pull requests. The interesting route here is GET .../merge, which answers "will
10
+ // this apply" without writing anything: an agent that reads, thinks, and proposes
11
+ // wants to know before it asks. POST .../merge goes through pulls.mergePull, the
12
+ // same sequence the web merge button performs.
13
+ const MAX_LISTED = 500;
14
+ function pullNumber(raw) {
15
+ return /^[1-9][0-9]*$/.test(raw ?? '') ? parseInt(raw, 10) : null;
16
+ }
17
+ function registerPullApi(app, root, limiter, engine) {
18
+ app.get('/api/repos/:collection/:repo/pulls', (req, res) => {
19
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
20
+ if (!found)
21
+ return;
22
+ const stateAsked = String(req.query.state ?? 'open');
23
+ if (!['open', 'closed', 'merged', 'all'].includes(stateAsked)) {
24
+ (0, auth_1.apiError)(res, 400, 'state must be open, closed, merged, or all');
25
+ return;
26
+ }
27
+ const all = (0, pulls_1.listPulls)(root, found.repo.collection, found.repo.name);
28
+ const selected = stateAsked === 'all' ? all : all.filter((p) => p.state === stateAsked);
29
+ const limit = (0, auth_1.limitParam)(req.query.limit, MAX_LISTED, MAX_LISTED);
30
+ res.json({ pulls: selected.slice(0, limit), total: selected.length });
31
+ });
32
+ app.get('/api/repos/:collection/:repo/pulls/:n', (req, res) => {
33
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
34
+ if (!found)
35
+ return;
36
+ const n = pullNumber(req.params.n);
37
+ const pull = n === null ? null : (0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n);
38
+ if (!pull) {
39
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n} in ${found.repo.collection}/${found.repo.name}`);
40
+ return;
41
+ }
42
+ res.json(pull);
43
+ });
44
+ app.post('/api/repos/:collection/:repo/pulls', async (req, res) => {
45
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
46
+ if (!found)
47
+ return;
48
+ const body = (0, auth_1.bodyOf)(req);
49
+ const title = (0, auth_1.stringField)(body, 'title');
50
+ const base = (0, auth_1.stringField)(body, 'base');
51
+ const head = (0, auth_1.stringField)(body, 'head');
52
+ if (title === null || base === null || head === null) {
53
+ (0, auth_1.apiError)(res, 400, '"title", "base", and "head" are required');
54
+ return;
55
+ }
56
+ const branches = (await found.repo.listRefs('heads')).map((b) => b.name);
57
+ try {
58
+ const created = (0, pulls_1.openPull)(root, found.repo.collection, found.repo.name, { title, body: (0, auth_1.stringField)(body, 'body') ?? '', author: found.actor.username, base, head }, branches);
59
+ res.status(201).json(created);
60
+ }
61
+ catch (e) {
62
+ (0, auth_1.sendOpError)(res, e, 'could not open the pull request');
63
+ }
64
+ });
65
+ app.patch('/api/repos/:collection/:repo/pulls/:n', (req, res) => {
66
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
67
+ if (!found)
68
+ return;
69
+ const n = pullNumber(req.params.n);
70
+ if (n === null) {
71
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
72
+ return;
73
+ }
74
+ const body = (0, auth_1.bodyOf)(req);
75
+ try {
76
+ (0, pulls_1.editPull)(root, found.repo.collection, found.repo.name, n, {
77
+ title: (0, auth_1.stringField)(body, 'title') ?? undefined,
78
+ body: (0, auth_1.stringField)(body, 'body') ?? undefined,
79
+ });
80
+ res.json((0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n));
81
+ }
82
+ catch (e) {
83
+ (0, auth_1.sendOpError)(res, e, 'could not edit the pull request');
84
+ }
85
+ });
86
+ app.post('/api/repos/:collection/:repo/pulls/:n/comments', (req, res) => {
87
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
88
+ if (!found)
89
+ return;
90
+ const n = pullNumber(req.params.n);
91
+ const text = (0, auth_1.stringField)((0, auth_1.bodyOf)(req), 'body');
92
+ if (n === null) {
93
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
94
+ return;
95
+ }
96
+ if (text === null) {
97
+ (0, auth_1.apiError)(res, 400, 'a "body" is required');
98
+ return;
99
+ }
100
+ try {
101
+ res.status(201).json((0, pulls_1.addComment)(root, found.repo.collection, found.repo.name, n, { author: found.actor.username, body: text }));
102
+ }
103
+ catch (e) {
104
+ (0, auth_1.sendOpError)(res, e, 'could not add the comment');
105
+ }
106
+ });
107
+ app.post('/api/repos/:collection/:repo/pulls/:n/state', (req, res) => {
108
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
109
+ if (!found)
110
+ return;
111
+ const n = pullNumber(req.params.n);
112
+ const state = (0, auth_1.stringField)((0, auth_1.bodyOf)(req), 'state');
113
+ if (n === null) {
114
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
115
+ return;
116
+ }
117
+ if (state !== 'open' && state !== 'closed') {
118
+ (0, auth_1.apiError)(res, 400, '"state" must be "open" or "closed"');
119
+ return;
120
+ }
121
+ try {
122
+ (0, pulls_1.setPullState)(root, found.repo.collection, found.repo.name, n, state, found.actor.username);
123
+ res.json((0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n));
124
+ }
125
+ catch (e) {
126
+ (0, auth_1.sendOpError)(res, e, 'could not change the state');
127
+ }
128
+ });
129
+ // The diff and the commits are questions for git, answered from base and head
130
+ // at the moment they are asked. A pull request never stores either.
131
+ const comparison = async (req, res, what) => {
132
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
133
+ if (!found)
134
+ return;
135
+ const n = pullNumber(req.params.n);
136
+ const pull = n === null ? null : (0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n);
137
+ if (!pull) {
138
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
139
+ return;
140
+ }
141
+ if (!(0, git_1.isValidRefName)(pull.base) || !(0, git_1.isValidRefName)(pull.head)) {
142
+ (0, auth_1.apiError)(res, 409, 'this pull request names a ref that is no longer usable');
143
+ return;
144
+ }
145
+ try {
146
+ const cmp = await found.repo.compare(pull.base, pull.head);
147
+ if (what === 'commits') {
148
+ res.json({ base: pull.base, head: pull.head, ahead: cmp.ahead, commits: cmp.commits });
149
+ return;
150
+ }
151
+ res.json({ base: pull.base, head: pull.head, ...cmp });
152
+ }
153
+ catch {
154
+ (0, auth_1.apiError)(res, 409, `could not compare ${pull.base} with ${pull.head}; one of them may be gone`);
155
+ }
156
+ };
157
+ app.get('/api/repos/:collection/:repo/pulls/:n/diff', (req, res) => comparison(req, res, 'diff'));
158
+ app.get('/api/repos/:collection/:repo/pulls/:n/commits', (req, res) => comparison(req, res, 'commits'));
159
+ // Mergeability without merging: the single most useful read for a caller that
160
+ // is about to write.
161
+ app.get('/api/repos/:collection/:repo/pulls/:n/merge', async (req, res) => {
162
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
163
+ if (!found)
164
+ return;
165
+ const n = pullNumber(req.params.n);
166
+ const pull = n === null ? null : (0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n);
167
+ if (!pull) {
168
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
169
+ return;
170
+ }
171
+ if (pull.state !== 'open') {
172
+ res.json({ state: pull.state, mergeable: false, status: pull.state });
173
+ return;
174
+ }
175
+ try {
176
+ const preview = await (0, ops_1.previewMerge)(found.repo.dir, pull.base, pull.head);
177
+ res.json({ state: pull.state, mergeable: preview.status === 'clean', ...preview });
178
+ }
179
+ catch (e) {
180
+ (0, auth_1.sendOpError)(res, e, 'the merge could not be computed');
181
+ }
182
+ });
183
+ app.post('/api/repos/:collection/:repo/pulls/:n/merge', async (req, res) => {
184
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
185
+ if (!found)
186
+ return;
187
+ const n = pullNumber(req.params.n);
188
+ if (n === null) {
189
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
190
+ return;
191
+ }
192
+ const body = (0, auth_1.bodyOf)(req);
193
+ const methodAsked = (0, auth_1.stringField)(body, 'method') ?? 'merge';
194
+ if (methodAsked !== 'merge' && methodAsked !== 'squash') {
195
+ (0, auth_1.apiError)(res, 400, '"method" must be "merge" or "squash"');
196
+ return;
197
+ }
198
+ const pull = (0, pulls_1.readPull)(root, found.repo.collection, found.repo.name, n);
199
+ const branches = await found.repo.listRefs('heads');
200
+ try {
201
+ const result = await (0, pulls_1.mergePull)(root, found.repo, n, found.actor, {
202
+ method: methodAsked,
203
+ message: (0, auth_1.stringField)(body, 'message') ?? undefined,
204
+ deleteBranch: body.deleteBranch === true,
205
+ }, { defaultBranch: await found.repo.defaultBranch(branches) });
206
+ // A merge moves a branch, which is a push as far as workflows are
207
+ // concerned. Forgetting this here would be a silent divergence from the
208
+ // web merge button, which fires it.
209
+ if (pull) {
210
+ (0, trigger_1.firePush)(root, engine, { collection: found.repo.collection, name: found.repo.name }, pull.base, result.before, result.sha, found.actor.username);
211
+ }
212
+ res.json(result);
213
+ }
214
+ catch (e) {
215
+ // A conflict is 409 with the conflicting paths, not a 400 and not a 500:
216
+ // the caller can act on the list.
217
+ if (e instanceof pulls_1.MergeConflict) {
218
+ res.status(409).json({ error: e.message, conflicts: e.paths });
219
+ return;
220
+ }
221
+ (0, auth_1.sendOpError)(res, e, 'the merge failed');
222
+ }
223
+ });
224
+ app.post('/api/repos/:collection/:repo/pulls/:n/delete-branch', async (req, res) => {
225
+ const found = (0, auth_1.requirePush)(root, limiter, req, res);
226
+ if (!found)
227
+ return;
228
+ const n = pullNumber(req.params.n);
229
+ if (n === null) {
230
+ (0, auth_1.apiError)(res, 404, `no pull request ${req.params.n}`);
231
+ return;
232
+ }
233
+ const branches = await found.repo.listRefs('heads');
234
+ try {
235
+ const deleted = await (0, pulls_1.deletePullBranch)(root, found.repo, n, {
236
+ defaultBranch: await found.repo.defaultBranch(branches),
237
+ });
238
+ res.json({ deleted });
239
+ }
240
+ catch (e) {
241
+ (0, auth_1.sendOpError)(res, e, 'could not delete the branch');
242
+ }
243
+ });
244
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerReleaseApi = registerReleaseApi;
4
+ const git_1 = require("../git");
5
+ const releases_1 = require("../releases");
6
+ const auth_1 = require("./auth");
7
+ // Releases: notes attached to a tag.
8
+ //
9
+ // A release's downloads are the archive routes, so there is nothing to upload and
10
+ // no asset endpoints. Anyone who knows `gh release upload` will look for one, and
11
+ // docs/agents.md says plainly that it is not here.
12
+ function registerReleaseApi(app, root, limiter) {
13
+ app.get('/api/repos/:collection/:repo/releases', (req, res) => {
14
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
15
+ if (!found)
16
+ return;
17
+ res.json({ releases: (0, releases_1.listReleases)(root, found.repo.collection, found.repo.name) });
18
+ });
19
+ app.get('/api/repos/:collection/:repo/releases/:tag', (req, res) => {
20
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
21
+ if (!found)
22
+ return;
23
+ const tag = decodeURIComponent(req.params.tag);
24
+ const release = (0, releases_1.readRelease)(root, found.repo.collection, found.repo.name, tag);
25
+ if (!release) {
26
+ (0, auth_1.apiError)(res, 404, `no release for tag ${tag}`);
27
+ return;
28
+ }
29
+ res.json(release);
30
+ });
31
+ // PUT rather than POST, because a release is keyed by its tag and the file is
32
+ // one saveRelease either way. Creating and editing being the same call takes a
33
+ // whole class of "already exists, retry as an edit" logic out of the caller.
34
+ app.put('/api/repos/:collection/:repo/releases/:tag', async (req, res) => {
35
+ const ctx = (0, auth_1.requirePush)(root, limiter, req, res);
36
+ if (!ctx)
37
+ return;
38
+ const tag = decodeURIComponent(req.params.tag);
39
+ if (!(0, git_1.isValidRefName)(tag)) {
40
+ (0, auth_1.apiError)(res, 400, 'that is not a usable tag name');
41
+ return;
42
+ }
43
+ // The tag has to exist in the repository, which is what the web form checks:
44
+ // release notes for a tag nobody can check out are notes about nothing.
45
+ const tags = await ctx.repo.listRefs('tags');
46
+ if (!tags.some((t) => t.name === tag)) {
47
+ (0, auth_1.apiError)(res, 404, `no tag ${tag} in this repository; create the tag first`);
48
+ return;
49
+ }
50
+ const body = (0, auth_1.bodyOf)(req);
51
+ const existing = (0, releases_1.readRelease)(root, ctx.repo.collection, ctx.repo.name, tag);
52
+ const prerelease = body.prerelease;
53
+ if (prerelease !== undefined && typeof prerelease !== 'boolean') {
54
+ (0, auth_1.apiError)(res, 400, '"prerelease" must be true or false');
55
+ return;
56
+ }
57
+ const release = {
58
+ tag,
59
+ name: (0, auth_1.stringField)(body, 'name') ?? existing?.name ?? '',
60
+ // The author is whoever first published it, so an edit by someone else
61
+ // does not quietly reassign it.
62
+ author: existing?.author ?? ctx.actor.username,
63
+ created: existing?.created ?? new Date().toISOString(),
64
+ prerelease: prerelease ?? existing?.prerelease ?? false,
65
+ body: (0, auth_1.stringField)(body, 'body') ?? existing?.body ?? '',
66
+ };
67
+ (0, releases_1.saveRelease)(root, ctx.repo.collection, ctx.repo.name, release);
68
+ res.status(existing ? 200 : 201).json(release);
69
+ });
70
+ app.delete('/api/repos/:collection/:repo/releases/:tag', (req, res) => {
71
+ const ctx = (0, auth_1.requirePush)(root, limiter, req, res);
72
+ if (!ctx)
73
+ return;
74
+ const tag = decodeURIComponent(req.params.tag);
75
+ if (!(0, releases_1.deleteRelease)(root, ctx.repo.collection, ctx.repo.name, tag)) {
76
+ (0, auth_1.apiError)(res, 404, `no release for tag ${tag}`);
77
+ return;
78
+ }
79
+ // The tag itself is left alone: deleting release notes is not deleting a
80
+ // release's history, and the two are separate operations on purpose.
81
+ res.json({ deleted: tag, tagKept: true });
82
+ });
83
+ }
@@ -0,0 +1,156 @@
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.registerRepoApi = registerRepoApi;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const layout_1 = require("../layout");
40
+ const perms_1 = require("../perms");
41
+ const scan_1 = require("../scan");
42
+ const issues_1 = require("../issues");
43
+ const pulls_1 = require("../pulls");
44
+ const releases_1 = require("../releases");
45
+ const auth_1 = require("./auth");
46
+ function summarize(root, collection, dirName) {
47
+ const name = (0, scan_1.displayName)(dirName);
48
+ const dir = (0, layout_1.repoPath)(root, collection, dirName);
49
+ return {
50
+ collection,
51
+ name,
52
+ description: (0, scan_1.repoDescription)(dir) ?? '',
53
+ forkedFrom: (0, scan_1.forkParent)(dir),
54
+ hasSite: (0, scan_1.siteDir)(root, collection, name) !== null,
55
+ private: (0, perms_1.repoIsPrivate)(dir),
56
+ };
57
+ }
58
+ function registerRepoApi(app, root, limiter) {
59
+ // Every repository in the vault, flat. A vault's collections are small and
60
+ // this is read off disk per request, so there is no pagination: --limit and
61
+ // the server-side caps are enough, and a cursor protocol would be machinery
62
+ // with nothing to carry.
63
+ app.get('/api/repos', (req, res) => {
64
+ const auth = (0, auth_1.requireApiAuth)(root, limiter, req, res);
65
+ if (!auth)
66
+ return;
67
+ const repos = [];
68
+ for (const c of (0, scan_1.listCollections)(root)) {
69
+ for (const dirName of (0, scan_1.listRepoDirs)(root, c.name)) {
70
+ const dir = (0, layout_1.repoPath)(root, c.name, dirName);
71
+ // A private repository the caller has no role on is not listed, the
72
+ // same way its page 404s.
73
+ if ((0, perms_1.repoRole)(root, auth, { collection: c.name, name: (0, scan_1.displayName)(dirName), dir }) === null)
74
+ continue;
75
+ repos.push(summarize(root, c.name, dirName));
76
+ }
77
+ }
78
+ res.json({ repos });
79
+ });
80
+ app.get('/api/repos/:collection/:repo', async (req, res) => {
81
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
82
+ if (!found)
83
+ return;
84
+ const { repo, auth } = found;
85
+ const role = (0, perms_1.repoRole)(root, auth, repo);
86
+ const branches = await repo.listRefs('heads');
87
+ const tags = await repo.listRefs('tags');
88
+ const issues = (0, issues_1.issueCounts)(root, repo.collection, repo.name);
89
+ const pulls = (0, pulls_1.pullCounts)(root, repo.collection, repo.name);
90
+ res.json({
91
+ ...summarize(root, repo.collection, path.basename(repo.dir)),
92
+ defaultBranch: await repo.defaultBranch(branches),
93
+ updated: await repo.lastUpdated(),
94
+ branches: branches.length,
95
+ tags: tags.length,
96
+ releases: (0, releases_1.listReleases)(root, repo.collection, repo.name).length,
97
+ openIssues: issues.open,
98
+ closedIssues: issues.closed,
99
+ openPulls: pulls.open,
100
+ closedPulls: pulls.closed,
101
+ // What this token may do here, so that a caller need not discover it by
102
+ // being refused.
103
+ role,
104
+ canPush: (0, perms_1.atLeast)(role, 'write'),
105
+ });
106
+ });
107
+ app.get('/api/repos/:collection/:repo/branches', async (req, res) => {
108
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
109
+ if (!found)
110
+ return;
111
+ const branches = await found.repo.listRefs('heads');
112
+ res.json({ branches, defaultBranch: await found.repo.defaultBranch(branches) });
113
+ });
114
+ app.get('/api/repos/:collection/:repo/tags', async (req, res) => {
115
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
116
+ if (!found)
117
+ return;
118
+ res.json({ tags: await found.repo.listRefs('tags') });
119
+ });
120
+ // Read only, and deliberately. Publishing a site is a workflow's job or a file
121
+ // copy into the vault; an upload path here would be a second way to write the
122
+ // one directory whose contents are served to browsers.
123
+ app.get('/api/repos/:collection/:repo/site', (req, res) => {
124
+ const found = (0, auth_1.requireRepo)(root, limiter, req, res);
125
+ if (!found)
126
+ return;
127
+ const dir = (0, scan_1.siteDir)(root, found.repo.collection, found.repo.name);
128
+ if (!dir) {
129
+ res.json({ exists: false });
130
+ return;
131
+ }
132
+ let entries = 0;
133
+ let updated = null;
134
+ try {
135
+ const walk = (d) => {
136
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
137
+ if (e.isDirectory())
138
+ walk(path.join(d, e.name));
139
+ else
140
+ entries++;
141
+ }
142
+ };
143
+ walk(dir);
144
+ updated = fs.statSync(dir).mtime.toISOString();
145
+ }
146
+ catch {
147
+ // An unreadable site directory reports what it can rather than failing.
148
+ }
149
+ res.json({ exists: true, entries, updated });
150
+ });
151
+ // A repository name that is not two path segments is a caller's mistake rather
152
+ // than a missing repository, and saying so is cheaper than letting it 404.
153
+ app.all('/api/repos/:collection', (_req, res) => {
154
+ (0, auth_1.apiError)(res, 404, 'address a repository as /api/repos/<collection>/<repo>');
155
+ });
156
+ }