@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
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
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.migrateLayout = migrateLayout;
|
|
37
|
+
exports.migratePermissions = migratePermissions;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const layout_1 = require("./layout");
|
|
41
|
+
const perms_1 = require("./perms");
|
|
42
|
+
const scan_1 = require("./scan");
|
|
43
|
+
const vault_1 = require("./vault");
|
|
44
|
+
/**
|
|
45
|
+
* Moving a vault from the older layout, where collections sat directly in the
|
|
46
|
+
* vault directory and repositories directly in a collection, to the current
|
|
47
|
+
* one, where both sit one level down (see src/layout.ts).
|
|
48
|
+
*
|
|
49
|
+
* This runs on startup, before anything is served, and is renames only: no
|
|
50
|
+
* file is read, written, or copied, so the cost does not depend on how large
|
|
51
|
+
* the repositories are, and a vault on a full disk migrates as well as one on
|
|
52
|
+
* an empty disk. The server refuses to serve a vault it could not migrate,
|
|
53
|
+
* rather than serving what would look like an empty vault.
|
|
54
|
+
*/
|
|
55
|
+
/** Where a collection is parked while its own contents are being rearranged. */
|
|
56
|
+
const STAGING_DIR = '.collections-migrating';
|
|
57
|
+
/**
|
|
58
|
+
* Whether a directory is a collection under the old layout: it holds a bare
|
|
59
|
+
* repository, or one of the directories a repository keeps beside it. An empty
|
|
60
|
+
* collection matches neither, and is decided by its company - see oldCollections.
|
|
61
|
+
*/
|
|
62
|
+
function looksLikeOldCollection(dir) {
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return entries.some((e) => {
|
|
71
|
+
if (!e.isDirectory())
|
|
72
|
+
return false;
|
|
73
|
+
const lower = e.name.toLowerCase();
|
|
74
|
+
if (scan_1.repoSiblingSuffixes.some((s) => lower.endsWith(s)) || lower.endsWith('.lfs'))
|
|
75
|
+
return true;
|
|
76
|
+
return (0, scan_1.isBareRepo)(path.join(dir, e.name));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether a `collections` directory in the vault root is this layout's
|
|
81
|
+
* container rather than a collection that happens to be named that. It is the
|
|
82
|
+
* container when it is empty, or when something in it has a `repos` directory,
|
|
83
|
+
* which is the shape only the current layout produces.
|
|
84
|
+
*/
|
|
85
|
+
function isCollectionsContainer(dir) {
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (entries.length === 0)
|
|
94
|
+
return true;
|
|
95
|
+
return entries.some((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, layout_1.REPOS_DIR)));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The collections to move, or an empty list if the vault is already laid out
|
|
99
|
+
* the current way.
|
|
100
|
+
*
|
|
101
|
+
* A directory in the vault root is a collection to move when it holds
|
|
102
|
+
* something a repository would put there. An empty directory holds nothing to
|
|
103
|
+
* judge by, so it is taken as a collection only when some other directory
|
|
104
|
+
* proves the vault is on the old layout; in a vault already migrated, an empty
|
|
105
|
+
* stray directory is left where it is rather than being adopted as a
|
|
106
|
+
* collection nobody made.
|
|
107
|
+
*
|
|
108
|
+
* `collections` is the one ambiguous name, since a vault on the old layout may
|
|
109
|
+
* have a collection called that. Asked of it is the narrower question above:
|
|
110
|
+
* a `collections` directory holding this layout is the container and is left
|
|
111
|
+
* alone, and one holding repositories is an ordinary collection and is moved,
|
|
112
|
+
* ending up at `collections/collections`.
|
|
113
|
+
*/
|
|
114
|
+
function oldCollections(root) {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const candidates = entries
|
|
123
|
+
.filter((e) => e.isDirectory() && !e.isSymbolicLink() && (0, scan_1.isValidName)(e.name))
|
|
124
|
+
.map((e) => e.name)
|
|
125
|
+
.filter((name) => name !== layout_1.COLLECTIONS_DIR || !isCollectionsContainer(path.join(root, name)))
|
|
126
|
+
.sort();
|
|
127
|
+
const definite = candidates.filter((name) => looksLikeOldCollection(path.join(root, name)));
|
|
128
|
+
if (definite.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
return candidates;
|
|
131
|
+
}
|
|
132
|
+
/** Move everything in a collection's directory into its `repos` subdirectory. */
|
|
133
|
+
function nestRepos(dir) {
|
|
134
|
+
const repos = path.join(dir, layout_1.REPOS_DIR);
|
|
135
|
+
const entries = fs.readdirSync(dir);
|
|
136
|
+
// Already nested, by an earlier run that was interrupted after this
|
|
137
|
+
// collection and before the last step. A collection may hold a repository
|
|
138
|
+
// named `repos`, so this asks what the directory is rather than assuming
|
|
139
|
+
// from its name: a bare repository there is a repository to move, not a
|
|
140
|
+
// collection already done.
|
|
141
|
+
if (entries.length === 1 && entries[0] === layout_1.REPOS_DIR && !(0, scan_1.isBareRepo)(repos))
|
|
142
|
+
return;
|
|
143
|
+
// Everything moves into a directory under a temporary name, which is renamed
|
|
144
|
+
// into place at the end. A collection is therefore never left holding a
|
|
145
|
+
// half-filled repos directory beside the repositories still to go, and a
|
|
146
|
+
// repository that is itself called `repos` moves like any other.
|
|
147
|
+
//
|
|
148
|
+
// A scratch directory from an interrupted run is filled the rest of the way
|
|
149
|
+
// rather than cleared: what is in it are repositories that were moved and
|
|
150
|
+
// not yet renamed into place, and nothing in this file deletes those.
|
|
151
|
+
const scratchName = `.${layout_1.REPOS_DIR}-incoming`;
|
|
152
|
+
const scratch = path.join(dir, scratchName);
|
|
153
|
+
if (!fs.existsSync(scratch))
|
|
154
|
+
fs.mkdirSync(scratch);
|
|
155
|
+
for (const name of entries) {
|
|
156
|
+
if (name === scratchName)
|
|
157
|
+
continue;
|
|
158
|
+
fs.renameSync(path.join(dir, name), path.join(scratch, name));
|
|
159
|
+
}
|
|
160
|
+
fs.renameSync(scratch, repos);
|
|
161
|
+
}
|
|
162
|
+
/** Move each entry of `from` into `to`, refusing rather than overwriting. */
|
|
163
|
+
function drain(from, to) {
|
|
164
|
+
const moved = [];
|
|
165
|
+
for (const name of fs.readdirSync(from).sort()) {
|
|
166
|
+
const dest = path.join(to, name);
|
|
167
|
+
if (fs.existsSync(dest)) {
|
|
168
|
+
throw new Error(`cannot move ${name} into ${layout_1.COLLECTIONS_DIR}/: a collection of that name is already there`);
|
|
169
|
+
}
|
|
170
|
+
fs.renameSync(path.join(from, name), dest);
|
|
171
|
+
moved.push(name);
|
|
172
|
+
}
|
|
173
|
+
return moved;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Bring a vault up to the current layout, returning what was moved, or null if
|
|
177
|
+
* there was nothing to move. Safe to call on every start: a vault already laid
|
|
178
|
+
* out this way is read once and left alone.
|
|
179
|
+
*
|
|
180
|
+
* The order is what makes an interrupted run recoverable. Each collection is
|
|
181
|
+
* moved into a staging directory and rearranged there, so a collection is
|
|
182
|
+
* either where it was or finished, never half-nested in place; the staging
|
|
183
|
+
* directory then becomes `collections/` in a single rename, or is drained into
|
|
184
|
+
* it if one already exists. A run interrupted before that last step leaves the
|
|
185
|
+
* staging directory behind, and the next start finds it and finishes.
|
|
186
|
+
*/
|
|
187
|
+
function migrateLayout(root) {
|
|
188
|
+
const staging = path.join(root, STAGING_DIR);
|
|
189
|
+
const collections = (0, layout_1.collectionsDir)(root);
|
|
190
|
+
const stale = fs.existsSync(staging);
|
|
191
|
+
const names = oldCollections(root);
|
|
192
|
+
if (names.length === 0 && !stale)
|
|
193
|
+
return null;
|
|
194
|
+
try {
|
|
195
|
+
if (!stale)
|
|
196
|
+
fs.mkdirSync(staging);
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
fs.renameSync(path.join(root, name), path.join(staging, name));
|
|
199
|
+
}
|
|
200
|
+
for (const name of fs.readdirSync(staging)) {
|
|
201
|
+
nestRepos(path.join(staging, name));
|
|
202
|
+
}
|
|
203
|
+
let moved;
|
|
204
|
+
if (fs.existsSync(collections)) {
|
|
205
|
+
moved = drain(staging, collections);
|
|
206
|
+
fs.rmdirSync(staging);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
moved = fs.readdirSync(staging).sort();
|
|
210
|
+
fs.renameSync(staging, collections);
|
|
211
|
+
}
|
|
212
|
+
return { collections: moved };
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
// Said in full, because this is printed instead of a vault starting, and
|
|
216
|
+
// the next thing the reader does depends on which of its parts is true.
|
|
217
|
+
throw new Error(`This vault is laid out the older way, with its collections in ${root} rather than ` +
|
|
218
|
+
`in ${layout_1.COLLECTIONS_DIR}/, and moving them failed: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
219
|
+
'Nothing was deleted, and what was moved is under ' +
|
|
220
|
+
`${STAGING_DIR}/ or ${layout_1.COLLECTIONS_DIR}/; starting again finishes the move once the ` +
|
|
221
|
+
'cause is fixed. The vault is not served meanwhile, since serving it would show an ' +
|
|
222
|
+
'empty vault while every repository is still on disk.');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ---- glob scopes to roles ----
|
|
226
|
+
/**
|
|
227
|
+
* Translating a pre-roles vault.json into the role model: glob scopes on
|
|
228
|
+
* users become the site-admin bit, collection owners, and repository
|
|
229
|
+
* collaborators. This runs on startup, like the layout migration above, and
|
|
230
|
+
* the original file is copied to vault.json.pre-roles first, so the
|
|
231
|
+
* translation can be audited or undone by hand.
|
|
232
|
+
*
|
|
233
|
+
* The two models do not line up exactly, and where they differ the
|
|
234
|
+
* translation rounds up to the nearest role rather than dropping access:
|
|
235
|
+
*
|
|
236
|
+
* - Push scope over a whole collection (`alice/*`) becomes ownership of the
|
|
237
|
+
* collection, which also carries the admin role there. There is no
|
|
238
|
+
* collection-wide write role to round down to.
|
|
239
|
+
* - Admin scope whose collection part is itself a pattern (`*`) becomes the
|
|
240
|
+
* site-admin bit.
|
|
241
|
+
* - Push scope whose collection part is a pattern becomes ownership of every
|
|
242
|
+
* collection the pattern matches today; a collection created later is not
|
|
243
|
+
* covered, where the glob would have covered it.
|
|
244
|
+
*
|
|
245
|
+
* Each rounding is reported in the returned notes, which the server prints on
|
|
246
|
+
* the migration run. Scope over a single repository (`alice/webapp`) has an
|
|
247
|
+
* exact counterpart and is translated silently: write for push scope, admin
|
|
248
|
+
* for admin scope. A glob naming a repository that does not exist is dropped
|
|
249
|
+
* with a note, since collaborators are recorded in the repository itself.
|
|
250
|
+
*/
|
|
251
|
+
function migratePermissions(root) {
|
|
252
|
+
const state = (0, vault_1.loadVault)(root);
|
|
253
|
+
if (state.status !== 'ok' || !state.vault.legacy)
|
|
254
|
+
return null;
|
|
255
|
+
const file = (0, vault_1.vaultFilePath)(root);
|
|
256
|
+
const backup = `${file}.pre-roles`;
|
|
257
|
+
if (!fs.existsSync(backup))
|
|
258
|
+
fs.copyFileSync(file, backup);
|
|
259
|
+
const notes = [`translated glob scopes to roles; the old file is kept at ${path.basename(backup)}`];
|
|
260
|
+
const siteAdmins = [];
|
|
261
|
+
const collections = (0, scan_1.listCollections)(root).map((c) => c.name);
|
|
262
|
+
const collectionOf = (glob) => (glob.includes('/') ? glob.slice(0, glob.indexOf('/')) : glob);
|
|
263
|
+
const repoOf = (glob) => (glob.includes('/') ? glob.slice(glob.indexOf('/') + 1) : '*');
|
|
264
|
+
const isPattern = (s) => /[*?]/.test(s);
|
|
265
|
+
const makeOwner = (username, collection) => {
|
|
266
|
+
if (collection === username)
|
|
267
|
+
return; // implicit
|
|
268
|
+
(0, perms_1.addCollectionOwner)(root, collection, username);
|
|
269
|
+
notes.push(`${username}: now an owner of ${collection}`);
|
|
270
|
+
};
|
|
271
|
+
const makeCollaborator = (username, collection, repoName, role) => {
|
|
272
|
+
const repo = (0, scan_1.findRepo)(root, collection, repoName);
|
|
273
|
+
if (!repo) {
|
|
274
|
+
notes.push(`${username}: scope over ${collection}/${repoName} dropped; no such repository`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
(0, perms_1.setCollaborator)(repo.dir, username, role);
|
|
278
|
+
};
|
|
279
|
+
for (const [username, user] of Object.entries(state.vault.users)) {
|
|
280
|
+
const legacy = user.legacy ?? { scope: [], admin: [] };
|
|
281
|
+
for (const kind of ['admin', 'scope']) {
|
|
282
|
+
// A site admin holds the admin role everywhere, so once the admin pass
|
|
283
|
+
// has granted the bit there is nothing the push globs could add.
|
|
284
|
+
if (siteAdmins.includes(username))
|
|
285
|
+
break;
|
|
286
|
+
const role = kind === 'admin' ? 'admin' : 'write';
|
|
287
|
+
for (const glob of legacy[kind]) {
|
|
288
|
+
const c = collectionOf(glob);
|
|
289
|
+
const r = repoOf(glob);
|
|
290
|
+
if (isPattern(c)) {
|
|
291
|
+
if (kind === 'admin') {
|
|
292
|
+
if (!siteAdmins.includes(username)) {
|
|
293
|
+
siteAdmins.push(username);
|
|
294
|
+
notes.push(`${username}: admin scope ${glob} becomes site admin`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
for (const name of collections.filter((n) => (0, vault_1.globMatch)(c, n))) {
|
|
299
|
+
if (r === '*')
|
|
300
|
+
makeOwner(username, name);
|
|
301
|
+
else
|
|
302
|
+
for (const d of (0, scan_1.listRepoDirs)(root, name)
|
|
303
|
+
.map(scan_1.displayName)
|
|
304
|
+
.filter((n) => (0, vault_1.globMatch)(r, n))) {
|
|
305
|
+
makeCollaborator(username, name, d, role);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
notes.push(`${username}: push scope ${glob} covers only what exists today, as ownership or write`);
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (r === '*') {
|
|
313
|
+
makeOwner(username, c);
|
|
314
|
+
if (kind === 'scope' && c !== username) {
|
|
315
|
+
notes.push(`${username}: push scope ${glob} rounds up to ownership of ${c}`);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (isPattern(r)) {
|
|
320
|
+
for (const d of (0, scan_1.listRepoDirs)(root, c)
|
|
321
|
+
.map(scan_1.displayName)
|
|
322
|
+
.filter((n) => (0, vault_1.globMatch)(r, n))) {
|
|
323
|
+
makeCollaborator(username, c, d, role);
|
|
324
|
+
}
|
|
325
|
+
notes.push(`${username}: scope ${glob} covers only the repositories that exist today`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
makeCollaborator(username, c, r, role);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
(0, vault_1.finishPermsMigration)(root, siteAdmins);
|
|
333
|
+
return notes;
|
|
334
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Parsing multipart/form-data, which is how a browser sends files.
|
|
3
|
+
//
|
|
4
|
+
// mochi parses this itself rather than taking a dependency, for the same
|
|
5
|
+
// reason it renders its own HTML: the format is small, the surface it is
|
|
6
|
+
// exposed to is one form, and a parser in the tree is one that can be read.
|
|
7
|
+
// The body arrives whole (express.raw with a cap) rather than as a stream,
|
|
8
|
+
// which is what lets the parser be forty lines: an upload here is bounded by
|
|
9
|
+
// the same cap either way, and streaming would buy nothing but complexity.
|
|
10
|
+
//
|
|
11
|
+
// The grammar, from RFC 7578 and RFC 2046: parts are separated by a line
|
|
12
|
+
// `--<boundary>`, each part has headers, then a blank line, then its bytes,
|
|
13
|
+
// and the epilogue begins at `--<boundary>--`. Every separator is preceded by
|
|
14
|
+
// CRLF that belongs to the separator, not to the part's content.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.boundaryOf = boundaryOf;
|
|
17
|
+
exports.parseMultipart = parseMultipart;
|
|
18
|
+
exports.partField = partField;
|
|
19
|
+
exports.partFiles = partFiles;
|
|
20
|
+
/** The boundary out of a Content-Type header, or null if there is not one. */
|
|
21
|
+
function boundaryOf(contentType) {
|
|
22
|
+
if (!contentType)
|
|
23
|
+
return null;
|
|
24
|
+
const m = contentType.match(/^\s*multipart\/form-data\s*;(.*)$/i);
|
|
25
|
+
if (!m)
|
|
26
|
+
return null;
|
|
27
|
+
const b = m[1].match(/boundary=(?:"([^"]+)"|([^;\s]+))/i);
|
|
28
|
+
if (!b)
|
|
29
|
+
return null;
|
|
30
|
+
return b[1] ?? b[2] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function headerValue(headers, name) {
|
|
33
|
+
const m = headers.match(new RegExp(`^${name}:\\s*(.*)$`, 'im'));
|
|
34
|
+
return m ? m[1].trim() : undefined;
|
|
35
|
+
}
|
|
36
|
+
// Content-Disposition parameters. A filename may be quoted and may contain
|
|
37
|
+
// escaped quotes; anything else is taken up to the next semicolon.
|
|
38
|
+
function dispositionParam(disposition, key) {
|
|
39
|
+
const m = disposition.match(new RegExp(`${key}=(?:"((?:[^"\\\\]|\\\\.)*)"|([^;]*))`, 'i'));
|
|
40
|
+
if (!m)
|
|
41
|
+
return undefined;
|
|
42
|
+
return (m[1] !== undefined ? m[1].replace(/\\(.)/g, '$1') : (m[2] ?? '')).trim();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Split a body into its parts. Anything malformed yields the parts that were
|
|
46
|
+
* well formed rather than an exception: a caller checks for what it needs.
|
|
47
|
+
*/
|
|
48
|
+
function parseMultipart(body, boundary) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
const sep = Buffer.from(`--${boundary}`);
|
|
51
|
+
let index = body.indexOf(sep);
|
|
52
|
+
if (index === -1)
|
|
53
|
+
return parts;
|
|
54
|
+
while (index !== -1) {
|
|
55
|
+
const after = index + sep.length;
|
|
56
|
+
// `--` here closes the whole body; anything else is a CRLF and a part.
|
|
57
|
+
if (body.length >= after + 2 && body[after] === 0x2d && body[after + 1] === 0x2d)
|
|
58
|
+
break;
|
|
59
|
+
const headerStart = after + 2;
|
|
60
|
+
const blank = body.indexOf('\r\n\r\n', headerStart);
|
|
61
|
+
const next = body.indexOf(sep, headerStart);
|
|
62
|
+
if (blank === -1 || next === -1 || blank > next)
|
|
63
|
+
break;
|
|
64
|
+
const headers = body.subarray(headerStart, blank).toString('utf8');
|
|
65
|
+
// The CRLF before the next separator belongs to the separator.
|
|
66
|
+
const data = body.subarray(blank + 4, Math.max(blank + 4, next - 2));
|
|
67
|
+
const disposition = headerValue(headers, 'content-disposition') ?? '';
|
|
68
|
+
const name = dispositionParam(disposition, 'name');
|
|
69
|
+
if (name !== undefined) {
|
|
70
|
+
const filename = dispositionParam(disposition, 'filename');
|
|
71
|
+
parts.push({
|
|
72
|
+
name,
|
|
73
|
+
filename: filename === undefined || filename === '' ? undefined : filename,
|
|
74
|
+
contentType: headerValue(headers, 'content-type'),
|
|
75
|
+
data: Buffer.from(data),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
index = next;
|
|
79
|
+
}
|
|
80
|
+
return parts;
|
|
81
|
+
}
|
|
82
|
+
/** The value of a plain (non-file) field. */
|
|
83
|
+
function partField(parts, name) {
|
|
84
|
+
const part = parts.find((p) => p.name === name && p.filename === undefined);
|
|
85
|
+
return part ? part.data.toString('utf8') : '';
|
|
86
|
+
}
|
|
87
|
+
/** Every part that carries a file, in the order the browser sent them. */
|
|
88
|
+
function partFiles(parts, name) {
|
|
89
|
+
return parts.filter((p) => p.name === name && p.filename !== undefined && p.data.length > 0);
|
|
90
|
+
}
|