@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.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- 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
|
+
}
|