@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,455 @@
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.REDIRECTS_FILE = void 0;
37
+ exports.redirectsFilePath = redirectsFilePath;
38
+ exports.loadRedirects = loadRedirects;
39
+ exports.hasRedirects = hasRedirects;
40
+ exports.resolveRepoRedirect = resolveRepoRedirect;
41
+ exports.resolveCollectionRedirect = resolveCollectionRedirect;
42
+ exports.recordRepoRename = recordRepoRename;
43
+ exports.recordCollectionRename = recordCollectionRename;
44
+ exports.forgetRepoRedirects = forgetRepoRedirects;
45
+ exports.forgetCollectionRedirects = forgetCollectionRedirects;
46
+ exports.redirectTargetPath = redirectTargetPath;
47
+ exports.redirectTarget = redirectTarget;
48
+ exports.registerRedirects = registerRedirects;
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const atomic_1 = require("./atomic");
52
+ const filecache_1 = require("./filecache");
53
+ const layout_1 = require("./layout");
54
+ const perms_1 = require("./perms");
55
+ const scan_1 = require("./scan");
56
+ const session_1 = require("./session");
57
+ // Where a renamed thing used to be, in <vault>/redirects.json.
58
+ //
59
+ // A rename moves directories, so every address anyone already holds stops
60
+ // working: a clone's remote, a link in an issue, a bookmark, a workflow that
61
+ // fetches a file over HTTP. The alternative to remembering the old address is
62
+ // asking everyone who held one to notice and fix it, which for a clone means
63
+ // the failure arrives as an authentication prompt or a 404 in the middle of
64
+ // someone's day.
65
+ //
66
+ // So a rename records where it went, and a request for a name nothing answers
67
+ // to any more is redirected to wherever that name ended up. This is the
68
+ // behaviour GitHub has, and the important half of it is the limit: the
69
+ // redirect is consulted only when the old name is free. A repository created
70
+ // later under the old name owns it outright, and the redirect for it goes
71
+ // quiet from that moment, with nothing to undo and no state to clear. The same
72
+ // holds for a collection.
73
+ //
74
+ // The map is keyed by name and not by any identity a repository carries, which
75
+ // is the honest limit of this approach. A repository deleted and re-created
76
+ // under the same name is a different repository wearing an answered-for name,
77
+ // so deleteRepo drops the entries that point at what it removed rather than
78
+ // letting them land somewhere unrelated.
79
+ exports.REDIRECTS_FILE = 'redirects.json';
80
+ /**
81
+ * How far a lookup will follow the map before giving up. A name renamed, and
82
+ * then renamed again, is two hops, and a vault where something has been
83
+ * renamed a dozen times in sequence is real; a cycle is not reachable through
84
+ * the recording path, but a hand-edited file can hold one, and this is what
85
+ * keeps that from being a hang.
86
+ */
87
+ const MAX_HOPS = 16;
88
+ function redirectsFilePath(root) {
89
+ return path.join(root, exports.REDIRECTS_FILE);
90
+ }
91
+ function stringMap(raw, field) {
92
+ if (raw === undefined)
93
+ return {};
94
+ if (typeof raw !== 'object' || raw === null)
95
+ throw new Error(`redirects.json: "${field}" must be an object`);
96
+ const out = {};
97
+ for (const [k, v] of Object.entries(raw)) {
98
+ if (typeof v !== 'string')
99
+ throw new Error(`redirects.json: "${field}.${k}" must be a string`);
100
+ out[k] = v;
101
+ }
102
+ return out;
103
+ }
104
+ function normalize(parsed) {
105
+ if (typeof parsed !== 'object' || parsed === null)
106
+ throw new Error('redirects.json must be a JSON object');
107
+ const raw = parsed;
108
+ return { repos: stringMap(raw.repos, 'repos'), collections: stringMap(raw.collections, 'collections') };
109
+ }
110
+ const EMPTY = { repos: {}, collections: {} };
111
+ const cache = (0, filecache_1.fileCache)({
112
+ read: (file) => {
113
+ try {
114
+ return normalize(JSON.parse(fs.readFileSync(file, 'utf8')));
115
+ }
116
+ catch (e) {
117
+ console.error(`redirects.json could not be read: ${e instanceof Error ? e.message : e}`);
118
+ return EMPTY;
119
+ }
120
+ },
121
+ missing: () => EMPTY,
122
+ });
123
+ /**
124
+ * The map as it is on disk, or an empty one if there is no file yet.
125
+ *
126
+ * Stat-cached like the runner registry and for the same reason: this is read
127
+ * on the way in to every request, and a vault that has never renamed anything
128
+ * should pay one stat for the feature and nothing else.
129
+ */
130
+ function loadRedirects(root) {
131
+ return cache.get(redirectsFilePath(root));
132
+ }
133
+ /** Whether the vault has any redirect at all, which is the fast path's question. */
134
+ function hasRedirects(root) {
135
+ const map = loadRedirects(root);
136
+ return Object.keys(map.repos).length > 0 || Object.keys(map.collections).length > 0;
137
+ }
138
+ function collectionExists(root, collection) {
139
+ if (!(0, scan_1.isValidName)(collection))
140
+ return false;
141
+ try {
142
+ return fs.statSync((0, layout_1.collectionDir)(root, collection)).isDirectory();
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ function splitFull(full) {
149
+ const i = full.indexOf('/');
150
+ if (i <= 0)
151
+ return null;
152
+ const collection = full.slice(0, i);
153
+ const repo = full.slice(i + 1);
154
+ if (!(0, scan_1.isValidName)(collection) || !(0, scan_1.isValidName)(repo))
155
+ return null;
156
+ return { collection, repo };
157
+ }
158
+ /**
159
+ * Where a repository asked for by an address it no longer has can be found, or
160
+ * null if that address needs no redirect.
161
+ *
162
+ * Null covers two cases deliberately, since neither is a redirect: the name is
163
+ * a repository that exists, and the name leads nowhere. A caller that gets
164
+ * null carries on and answers the request, or 404s it, exactly as it did
165
+ * before this existed.
166
+ *
167
+ * A repository entry is preferred over a collection one at each hop, being the
168
+ * more specific of the two: a repository moved out of a collection that was
169
+ * then renamed should follow the repository.
170
+ */
171
+ function resolveRepoRedirect(root, collection, repo, map = loadRedirects(root)) {
172
+ let c = collection;
173
+ let r = repo;
174
+ let moved = false;
175
+ for (let hop = 0; hop < MAX_HOPS; hop++) {
176
+ if ((0, scan_1.findRepo)(root, c, r))
177
+ return moved ? { collection: c, repo: r } : null;
178
+ const target = map.repos[`${c}/${r}`];
179
+ if (target !== undefined) {
180
+ const next = splitFull(target);
181
+ if (!next)
182
+ return null;
183
+ c = next.collection;
184
+ r = next.repo;
185
+ moved = true;
186
+ continue;
187
+ }
188
+ const renamed = map.collections[c];
189
+ if (renamed !== undefined && (0, scan_1.isValidName)(renamed)) {
190
+ c = renamed;
191
+ moved = true;
192
+ continue;
193
+ }
194
+ return null;
195
+ }
196
+ return null;
197
+ }
198
+ /**
199
+ * The collection a former collection name now points at, or null if that name
200
+ * needs no redirect. As above, null means both "it is there" and "it leads
201
+ * nowhere".
202
+ */
203
+ function resolveCollectionRedirect(root, collection, map = loadRedirects(root)) {
204
+ let c = collection;
205
+ let moved = false;
206
+ for (let hop = 0; hop < MAX_HOPS; hop++) {
207
+ if (collectionExists(root, c))
208
+ return moved ? c : null;
209
+ const renamed = map.collections[c];
210
+ if (renamed === undefined || !(0, scan_1.isValidName)(renamed))
211
+ return null;
212
+ c = renamed;
213
+ moved = true;
214
+ }
215
+ return null;
216
+ }
217
+ function write(root, map) {
218
+ const empty = Object.keys(map.repos).length === 0 && Object.keys(map.collections).length === 0;
219
+ if (empty) {
220
+ // Nothing to remember, so nothing on disk. A vault whose only rename was
221
+ // undone looks like one that never renamed anything, which is what it is.
222
+ fs.rmSync(redirectsFilePath(root), { force: true });
223
+ }
224
+ else {
225
+ (0, atomic_1.writeFileAtomic)(redirectsFilePath(root), JSON.stringify(map, null, 2) + '\n');
226
+ }
227
+ cache.invalidate(redirectsFilePath(root));
228
+ }
229
+ /**
230
+ * Drop every entry that no longer redirects anything: one whose own name is a
231
+ * repository or collection that exists again, and one whose chain leads
232
+ * nowhere. Both are entries a lookup already ignores, so this changes no
233
+ * answer; it keeps the file from accumulating a line per rename forever, and
234
+ * keeps it readable as a statement of what is actually being redirected.
235
+ *
236
+ * Run on the way out of every edit, under the lock, since that is the only
237
+ * time the file is written and the map is small enough for the pass to cost
238
+ * nothing worth measuring.
239
+ */
240
+ function prune(root, map) {
241
+ for (const key of Object.keys(map.repos)) {
242
+ const from = splitFull(key);
243
+ if (!from || !resolveRepoRedirect(root, from.collection, from.repo, map))
244
+ delete map.repos[key];
245
+ }
246
+ for (const key of Object.keys(map.collections)) {
247
+ if (!resolveCollectionRedirect(root, key, map))
248
+ delete map.collections[key];
249
+ }
250
+ }
251
+ /**
252
+ * Hold the lock across a read, an edit, and the write back, as the runner
253
+ * registry does: two renames at once through two processes would otherwise
254
+ * lose one of the two redirects. The cache is dropped on the way in because
255
+ * the copy about to be edited has to be the one on disk.
256
+ */
257
+ function edit(root, fn) {
258
+ (0, atomic_1.withFileLock)(`${redirectsFilePath(root)}.lock`, () => {
259
+ cache.invalidate(redirectsFilePath(root));
260
+ const current = loadRedirects(root);
261
+ const map = { repos: { ...current.repos }, collections: { ...current.collections } };
262
+ fn(map);
263
+ prune(root, map);
264
+ write(root, map);
265
+ });
266
+ }
267
+ /**
268
+ * Remember that a repository moved. Called after the directories have moved,
269
+ * so that a rename which failed halfway leaves no redirect claiming otherwise.
270
+ *
271
+ * An entry for the destination name is dropped: the name now belongs to this
272
+ * repository, so whatever it used to redirect to is not where a request for it
273
+ * should go.
274
+ */
275
+ function recordRepoRename(root, collection, name, toCollection, toName) {
276
+ edit(root, (map) => {
277
+ delete map.repos[`${toCollection}/${toName}`];
278
+ map.repos[`${collection}/${name}`] = `${toCollection}/${toName}`;
279
+ });
280
+ }
281
+ /** Remember that a collection was renamed, as above. */
282
+ function recordCollectionRename(root, name, toName) {
283
+ edit(root, (map) => {
284
+ delete map.collections[toName];
285
+ map.collections[name] = toName;
286
+ });
287
+ }
288
+ /**
289
+ * Forget a repository that has been deleted: both the redirect out of its own
290
+ * name and every redirect that pointed at it.
291
+ *
292
+ * The second half is the one that matters. Without it, a repository deleted
293
+ * and a new one created under the same name would inherit the old one's
294
+ * redirects, so an address that used to reach the deleted repository would
295
+ * quietly start reaching someone else's.
296
+ */
297
+ function forgetRepoRedirects(root, collection, name) {
298
+ const full = `${collection}/${name}`;
299
+ const map = loadRedirects(root);
300
+ if (map.repos[full] === undefined && !Object.values(map.repos).includes(full))
301
+ return;
302
+ edit(root, (m) => {
303
+ delete m.repos[full];
304
+ for (const [key, target] of Object.entries(m.repos)) {
305
+ if (target === full)
306
+ delete m.repos[key];
307
+ }
308
+ });
309
+ }
310
+ /**
311
+ * Forget a collection that has been removed, on the same terms: both the
312
+ * redirect out of its own name and every redirect that led to it, so that a
313
+ * collection created later under that name does not inherit traffic meant for
314
+ * the one that is gone.
315
+ */
316
+ function forgetCollectionRedirects(root, name) {
317
+ const map = loadRedirects(root);
318
+ if (map.collections[name] === undefined && !Object.values(map.collections).includes(name))
319
+ return;
320
+ edit(root, (m) => {
321
+ delete m.collections[name];
322
+ for (const [key, target] of Object.entries(m.collections)) {
323
+ if (target === name)
324
+ delete m.collections[key];
325
+ }
326
+ });
327
+ }
328
+ // ---- serving the redirect ----
329
+ function decodeSegment(segment) {
330
+ if (!segment.includes('%'))
331
+ return segment;
332
+ try {
333
+ return decodeURIComponent(segment);
334
+ }
335
+ catch {
336
+ return null;
337
+ }
338
+ }
339
+ /**
340
+ * The path a request should be sent to instead, or null if it should be
341
+ * answered where it is.
342
+ *
343
+ * Three shapes of address name a collection or a repository, and all three are
344
+ * rewritten the same way, since a redirect covering only the web pages would
345
+ * leave a clone, an LFS fetch, and a program using the API failing on an
346
+ * address a browser follows happily:
347
+ *
348
+ * /<collection>[/<repo>][/...] the web, git over HTTP, LFS
349
+ * /api/repos/<collection>/<repo>[/...] the repository API
350
+ * /api/collections/<collection>[/...] the collection API
351
+ *
352
+ * The tail of the path is carried across untouched. It names something inside
353
+ * the repository - a branch, a file, an issue number, a git service - and a
354
+ * rename changes none of that.
355
+ */
356
+ function redirectTargetPath(root, pathname) {
357
+ return redirectTarget(root, pathname)?.path ?? null;
358
+ }
359
+ /**
360
+ * The redirect for a path, along with the repository the new address names
361
+ * when it names one, so the middleware below can ask whether the requester
362
+ * may be told where it went.
363
+ */
364
+ function redirectTarget(root, pathname) {
365
+ const segs = pathname.split('/');
366
+ let offset = 1;
367
+ let repoSegment = true;
368
+ if (segs[1] === 'api') {
369
+ if (segs[2] === 'repos')
370
+ offset = 3;
371
+ else if (segs[2] === 'collections') {
372
+ offset = 3;
373
+ repoSegment = false;
374
+ }
375
+ else
376
+ return null;
377
+ }
378
+ const rawCollection = segs[offset];
379
+ if (!rawCollection)
380
+ return null;
381
+ const collection = decodeSegment(rawCollection);
382
+ if (collection === null || !(0, scan_1.isValidName)(collection))
383
+ return null;
384
+ const rawRepo = repoSegment ? segs[offset + 1] : undefined;
385
+ if (rawRepo) {
386
+ const repo = decodeSegment(rawRepo);
387
+ if (repo !== null && (0, scan_1.isValidName)(repo)) {
388
+ // `.git` is optional in a git URL and is kept as the request wrote it, so
389
+ // a clone that follows a redirect keeps asking for the same spelling it
390
+ // started with.
391
+ const base = (0, scan_1.displayName)(repo);
392
+ const suffix = repo === base ? '' : '.git';
393
+ const to = resolveRepoRedirect(root, collection, base);
394
+ if (to) {
395
+ segs[offset] = encodeURIComponent(to.collection);
396
+ segs[offset + 1] = encodeURIComponent(to.repo) + suffix;
397
+ return { path: segs.join('/'), repo: to };
398
+ }
399
+ }
400
+ }
401
+ // No repository answered for it, so the collection may still have moved.
402
+ // Every address under a renamed collection follows it, including one naming
403
+ // a repository that is not in it, which then 404s at the address the
404
+ // collection has now rather than the one it used to have.
405
+ const toCollection = resolveCollectionRedirect(root, collection);
406
+ if (toCollection === null)
407
+ return null;
408
+ segs[offset] = encodeURIComponent(toCollection);
409
+ return { path: segs.join('/'), repo: null };
410
+ }
411
+ /**
412
+ * Send a request to where the thing it names now lives.
413
+ *
414
+ * Registered ahead of every route that resolves a collection or a repository,
415
+ * which is safe because a redirect is only ever found for a name nothing
416
+ * answers to: a request for something that is there passes straight through,
417
+ * having cost the stat that found it, and a vault that has never renamed
418
+ * anything costs one stat of a file that is not there.
419
+ *
420
+ * 301 for a GET or a HEAD, and 308 for anything else. The difference is the
421
+ * method: a client following a 301 may turn a POST into a GET, which would
422
+ * turn an API call or a git push into a request for a page, while a 308 keeps
423
+ * the method and the body. Both say the move is permanent, which is what has
424
+ * git record the new address rather than following a redirect on every fetch.
425
+ *
426
+ * Permanent, but not cacheable. A redirect here is conditional on the old name
427
+ * being free, so a browser holding a 301 for a year would keep following it
428
+ * past the day someone creates a repository under that name, which is the one
429
+ * case where the redirect has to stop. no-store costs a round trip that the
430
+ * rename made necessary anyway.
431
+ */
432
+ function registerRedirects(app, root) {
433
+ app.use((req, res, next) => {
434
+ if (!hasRedirects(root))
435
+ return next();
436
+ const to = redirectTarget(root, req.path);
437
+ if (to === null)
438
+ return next();
439
+ // A redirect that lands on a private repository would tell whoever probes
440
+ // the old name that the repository exists and where it went, so it is
441
+ // answered only for a viewer who could read the destination; everyone
442
+ // else falls through to the 404 the old name would give anyway. Only the
443
+ // browser session is consulted: a git client following an old remote of a
444
+ // private repository gets the 404 and updates its remote by hand.
445
+ if (to.repo) {
446
+ const target = (0, scan_1.findRepo)(root, to.repo.collection, to.repo.repo);
447
+ if (target && !(0, perms_1.canReadRepo)(root, (0, session_1.getViewer)(req, root)?.auth ?? null, target))
448
+ return next();
449
+ }
450
+ const q = req.originalUrl.indexOf('?');
451
+ const query = q === -1 ? '' : req.originalUrl.slice(q);
452
+ const permanent = req.method === 'GET' || req.method === 'HEAD' ? 301 : 308;
453
+ res.set('Cache-Control', 'no-store').redirect(permanent, to.path + query);
454
+ });
455
+ }