@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/diff.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDiff = renderDiff;
|
|
4
|
+
const icons_1 = require("./icons");
|
|
5
|
+
const render_1 = require("./render");
|
|
6
|
+
// Unified diff to HTML. Two things a patch carries that a plain rendering
|
|
7
|
+
// throws away are recovered here: the line numbers each side of a hunk, and
|
|
8
|
+
// the shape of the change (how many lines each file gained and lost, and
|
|
9
|
+
// whether it was added, deleted, or renamed). Both are what a reader uses to
|
|
10
|
+
// orient in a diff, so both are rendered.
|
|
11
|
+
// Lines of a patch that describe the file rather than its content. The file
|
|
12
|
+
// header already says what these say - the path, the status, the counts - so
|
|
13
|
+
// they are dropped from the body, as GitHub drops them.
|
|
14
|
+
const HEADER_PREFIXES = [
|
|
15
|
+
'+++',
|
|
16
|
+
'---',
|
|
17
|
+
'index ',
|
|
18
|
+
'new file',
|
|
19
|
+
'deleted file',
|
|
20
|
+
'similarity',
|
|
21
|
+
'dissimilarity',
|
|
22
|
+
'rename ',
|
|
23
|
+
'copy ',
|
|
24
|
+
'old mode',
|
|
25
|
+
'new mode',
|
|
26
|
+
];
|
|
27
|
+
// These two do belong in the body: they say something about the content.
|
|
28
|
+
const NOTE_PREFIXES = ['Binary files', '\\'];
|
|
29
|
+
/** How many lines of one file's diff we render before offering the file instead. */
|
|
30
|
+
const MAX_FILE_LINES = 2000;
|
|
31
|
+
function parse(patch) {
|
|
32
|
+
const files = [];
|
|
33
|
+
let cur = null;
|
|
34
|
+
for (const line of patch.split('\n')) {
|
|
35
|
+
const start = line.match(/^diff --git a\/(.*) b\/(.*)$/);
|
|
36
|
+
if (line.startsWith('diff --git ')) {
|
|
37
|
+
cur = {
|
|
38
|
+
path: start ? start[2] : line.slice('diff --git '.length),
|
|
39
|
+
oldPath: start ? start[1] : '',
|
|
40
|
+
status: 'modified',
|
|
41
|
+
binary: false,
|
|
42
|
+
added: 0,
|
|
43
|
+
removed: 0,
|
|
44
|
+
lines: [],
|
|
45
|
+
};
|
|
46
|
+
files.push(cur);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!cur)
|
|
50
|
+
continue;
|
|
51
|
+
if (line.startsWith('new file'))
|
|
52
|
+
cur.status = 'added';
|
|
53
|
+
else if (line.startsWith('deleted file'))
|
|
54
|
+
cur.status = 'deleted';
|
|
55
|
+
else if (line.startsWith('rename to '))
|
|
56
|
+
cur.status = 'renamed';
|
|
57
|
+
else if (line.startsWith('Binary files'))
|
|
58
|
+
cur.binary = true;
|
|
59
|
+
else if (line.startsWith('+') && !line.startsWith('+++'))
|
|
60
|
+
cur.added++;
|
|
61
|
+
else if (line.startsWith('-') && !line.startsWith('---'))
|
|
62
|
+
cur.removed++;
|
|
63
|
+
cur.lines.push(line);
|
|
64
|
+
}
|
|
65
|
+
// A patch ends with a newline, which leaves an empty last line that is not
|
|
66
|
+
// part of anyone's file.
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
while (f.lines.length > 0 && f.lines[f.lines.length - 1] === '')
|
|
69
|
+
f.lines.pop();
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A bar beside a file's counts, in the proportion of the change: the share
|
|
75
|
+
* that was additions, then the share that was deletions, so a glance
|
|
76
|
+
* separates a rewrite from a one-line fix. A file that changed at all gets
|
|
77
|
+
* at least a tenth of the bar for each side it changed on, since a single
|
|
78
|
+
* deletion among four hundred additions should still be visible. The widths
|
|
79
|
+
* are inline because they are the datum; only the bar's shape is style.
|
|
80
|
+
*/
|
|
81
|
+
function statBar(added, removed) {
|
|
82
|
+
const total = added + removed;
|
|
83
|
+
if (total === 0)
|
|
84
|
+
return '';
|
|
85
|
+
const add = added > 0 && removed > 0
|
|
86
|
+
? Math.min(90, Math.max(10, Math.round((added / total) * 100)))
|
|
87
|
+
: added > 0
|
|
88
|
+
? 100
|
|
89
|
+
: 0;
|
|
90
|
+
return `<span class="statbar" aria-hidden="true"><span class="add" style="width:${add}%"></span><span class="del" style="width:${100 - add}%"></span></span>`;
|
|
91
|
+
}
|
|
92
|
+
function counts(added, removed) {
|
|
93
|
+
return `<span class="stat-add">+${added}</span><span class="stat-del">−${removed}</span>${statBar(added, removed)}`;
|
|
94
|
+
}
|
|
95
|
+
const STATUS_LABEL = {
|
|
96
|
+
added: 'added',
|
|
97
|
+
deleted: 'deleted',
|
|
98
|
+
renamed: 'renamed',
|
|
99
|
+
modified: 'modified',
|
|
100
|
+
};
|
|
101
|
+
function renderBody(file) {
|
|
102
|
+
if (file.binary)
|
|
103
|
+
return `<div class="diff-none muted">Binary file not shown.</div>`;
|
|
104
|
+
if (file.lines.length > MAX_FILE_LINES) {
|
|
105
|
+
return `<div class="diff-none muted">This diff is ${file.lines.length.toLocaleString('en-US')} lines long and is not shown.</div>`;
|
|
106
|
+
}
|
|
107
|
+
// Line numbers come from the hunk headers: a context line advances both
|
|
108
|
+
// sides, an addition only the new one, a removal only the old one.
|
|
109
|
+
let oldNo = 0;
|
|
110
|
+
let newNo = 0;
|
|
111
|
+
const rows = [];
|
|
112
|
+
for (const l of file.lines) {
|
|
113
|
+
let cls = 'ctx';
|
|
114
|
+
let left = '';
|
|
115
|
+
let right = '';
|
|
116
|
+
const hunk = l.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
117
|
+
if (HEADER_PREFIXES.some((p) => l.startsWith(p))) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
else if (hunk) {
|
|
121
|
+
oldNo = parseInt(hunk[1], 10);
|
|
122
|
+
newNo = parseInt(hunk[2], 10);
|
|
123
|
+
cls = 'hunk';
|
|
124
|
+
}
|
|
125
|
+
else if (NOTE_PREFIXES.some((p) => l.startsWith(p))) {
|
|
126
|
+
cls = 'meta';
|
|
127
|
+
}
|
|
128
|
+
else if (l.startsWith('+')) {
|
|
129
|
+
cls = 'add';
|
|
130
|
+
right = String(newNo++);
|
|
131
|
+
}
|
|
132
|
+
else if (l.startsWith('-')) {
|
|
133
|
+
cls = 'del';
|
|
134
|
+
left = String(oldNo++);
|
|
135
|
+
}
|
|
136
|
+
else if (l !== '') {
|
|
137
|
+
left = String(oldNo++);
|
|
138
|
+
right = String(newNo++);
|
|
139
|
+
}
|
|
140
|
+
rows.push(`<div class="dline ${cls}"><span class="dnum">${left}</span><span class="dnum">${right}</span><span class="dtext">${(0, render_1.esc)(l) || ' '}</span></div>`);
|
|
141
|
+
}
|
|
142
|
+
if (rows.length === 0)
|
|
143
|
+
return `<div class="diff-none muted">No changes to the file's content.</div>`;
|
|
144
|
+
return `<div class="diff-body">${rows.join('')}</div>`;
|
|
145
|
+
}
|
|
146
|
+
function renderDiff(patch, opts = {}) {
|
|
147
|
+
const files = parse(patch);
|
|
148
|
+
if (files.length === 0)
|
|
149
|
+
return '<p class="muted">No changes.</p>';
|
|
150
|
+
const added = files.reduce((n, f) => n + f.added, 0);
|
|
151
|
+
const removed = files.reduce((n, f) => n + f.removed, 0);
|
|
152
|
+
const summary = `<div class="diff-summary"><b>${files.length} changed file${files.length === 1 ? '' : 's'}</b>${counts(added, removed)}</div>`;
|
|
153
|
+
const boxes = files
|
|
154
|
+
.map((f, i) => {
|
|
155
|
+
const view = opts.blobBase
|
|
156
|
+
? `<a class="btn" href="${opts.blobBase}/${f.path
|
|
157
|
+
.split('/')
|
|
158
|
+
.map(encodeURIComponent)
|
|
159
|
+
.join('/')}" title="View the whole file at this revision">View file</a>`
|
|
160
|
+
: '';
|
|
161
|
+
const renamed = f.status === 'renamed' && f.oldPath ? `<span class="muted small">${(0, render_1.esc)(f.oldPath)} →</span>` : '';
|
|
162
|
+
// Each file is a <details>, so a reader can fold away what they have
|
|
163
|
+
// read; the summary keeps the name, the counts, and the way to the file.
|
|
164
|
+
return `<details class="diff-file" id="diff-${i}" open>
|
|
165
|
+
<summary class="diff-file-header">${(0, icons_1.icon)('chevron-down', 'fold')}${renamed}<span class="diff-path mono">${(0, render_1.esc)(f.path)}</span><span class="chip">${STATUS_LABEL[f.status]}</span><span class="diff-file-stat">${counts(f.added, f.removed)}</span>${view}</summary>
|
|
166
|
+
${renderBody(f)}
|
|
167
|
+
</details>`;
|
|
168
|
+
})
|
|
169
|
+
.join('');
|
|
170
|
+
return `${summary}${boxes}`;
|
|
171
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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.MAX_BODY = exports.MAX_TITLE = void 0;
|
|
37
|
+
exports.itemDir = itemDir;
|
|
38
|
+
exports.numericDirs = numericDirs;
|
|
39
|
+
exports.readDoc = readDoc;
|
|
40
|
+
exports.writeDoc = writeDoc;
|
|
41
|
+
exports.str = str;
|
|
42
|
+
exports.countComments = countComments;
|
|
43
|
+
exports.touch = touch;
|
|
44
|
+
exports.readComments = readComments;
|
|
45
|
+
exports.checkTitle = checkTitle;
|
|
46
|
+
exports.checkBody = checkBody;
|
|
47
|
+
exports.openFor = openFor;
|
|
48
|
+
exports.addComment = addComment;
|
|
49
|
+
exports.allocate = allocate;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const YAML = __importStar(require("yaml"));
|
|
53
|
+
const atomic_1 = require("./atomic");
|
|
54
|
+
const ops_1 = require("./ops");
|
|
55
|
+
// What issues and pull requests have in common.
|
|
56
|
+
//
|
|
57
|
+
// The two are stored the same way on purpose: a numbered directory holding a
|
|
58
|
+
// markdown file with a YAML frontmatter header, and a `comments/` directory of
|
|
59
|
+
// further markdown files beside it. A pull request is a discussion with a
|
|
60
|
+
// branch pair attached, so everything about the discussion half was written
|
|
61
|
+
// twice, and the copies had begun to drift in the small ways copies do. The
|
|
62
|
+
// bodies of `touch` differed only in a comment; `addComment` differed only in
|
|
63
|
+
// the noun in one message and the name of the header file.
|
|
64
|
+
//
|
|
65
|
+
// What is left in issues.ts and pulls.ts is what genuinely differs: labels and
|
|
66
|
+
// four sort orders on one side, base and head and merging on the other. What
|
|
67
|
+
// moved here is the storage shape, which neither owns.
|
|
68
|
+
//
|
|
69
|
+
// A `DiscussionKind` is how the caller says which of the two it is. It carries
|
|
70
|
+
// the header file's name, the directory the numbered ones sit in, and the noun
|
|
71
|
+
// to use in a message, because a person reading "That is longer than a
|
|
72
|
+
// discussion may be" would rightly wonder what a discussion is.
|
|
73
|
+
exports.MAX_TITLE = 200;
|
|
74
|
+
exports.MAX_BODY = 64 * 1024;
|
|
75
|
+
/** The directory for one numbered issue or pull request. */
|
|
76
|
+
function itemDir(kind, root, collection, repo, n) {
|
|
77
|
+
const dir = kind.dirFor(root, collection, repo);
|
|
78
|
+
if (!dir || !Number.isInteger(n) || n < 1)
|
|
79
|
+
return null;
|
|
80
|
+
return path.join(dir, String(n));
|
|
81
|
+
}
|
|
82
|
+
/** The numbered subdirectories of `dir`, in order. */
|
|
83
|
+
function numericDirs(dir) {
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return entries
|
|
92
|
+
.filter((e) => e.isDirectory() && /^[1-9][0-9]*$/.test(e.name))
|
|
93
|
+
.map((e) => parseInt(e.name, 10))
|
|
94
|
+
.sort((a, b) => a - b);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A markdown file with a YAML frontmatter header, which is the shape of every
|
|
98
|
+
* file here. A file without a header is not one of ours and is skipped rather
|
|
99
|
+
* than guessed at.
|
|
100
|
+
*/
|
|
101
|
+
function readDoc(file) {
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = fs.readFileSync(file, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
110
|
+
if (!m)
|
|
111
|
+
return null;
|
|
112
|
+
let meta;
|
|
113
|
+
try {
|
|
114
|
+
meta = YAML.parse(m[1]);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (typeof meta !== 'object' || meta === null)
|
|
120
|
+
return null;
|
|
121
|
+
return { meta: meta, body: m[2] };
|
|
122
|
+
}
|
|
123
|
+
function writeDoc(file, meta, body) {
|
|
124
|
+
const text = `---\n${YAML.stringify(meta).trimEnd()}\n---\n${body.replace(/\s+$/, '')}\n`;
|
|
125
|
+
(0, atomic_1.writeFileAtomic)(file, text, { mode: 0o600 });
|
|
126
|
+
}
|
|
127
|
+
function str(v, fallback = '') {
|
|
128
|
+
return typeof v === 'string' ? v : fallback;
|
|
129
|
+
}
|
|
130
|
+
function countComments(dir) {
|
|
131
|
+
try {
|
|
132
|
+
return fs.readdirSync(path.join(dir, 'comments')).filter((f) => /^[1-9][0-9]*\.md$/.test(f)).length;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Move a directory's timestamp, which is what invalidates the memoized open and
|
|
140
|
+
* closed counts shown on every repository page. Every write calls it.
|
|
141
|
+
*/
|
|
142
|
+
function touch(dir) {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
try {
|
|
145
|
+
fs.utimesSync(dir, now, now);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// The directory may not exist yet; the next read simply misses the cache.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Every comment on one issue or pull request, oldest first. */
|
|
152
|
+
function readComments(dir) {
|
|
153
|
+
let files = [];
|
|
154
|
+
try {
|
|
155
|
+
files = fs.readdirSync(path.join(dir, 'comments'));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const f of files.filter((x) => /^[1-9][0-9]*\.md$/.test(x)).sort((a, b) => parseInt(a, 10) - parseInt(b, 10))) {
|
|
162
|
+
const c = readDoc(path.join(dir, 'comments', f));
|
|
163
|
+
if (!c)
|
|
164
|
+
continue;
|
|
165
|
+
out.push({
|
|
166
|
+
id: parseInt(f, 10),
|
|
167
|
+
author: str(c.meta.author, 'unknown'),
|
|
168
|
+
created: str(c.meta.created),
|
|
169
|
+
body: c.body,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function checkTitle(kind, title) {
|
|
175
|
+
const t = title.trim().replace(/\s+/g, ' ');
|
|
176
|
+
if (t === '')
|
|
177
|
+
throw new ops_1.OpError(`${kind.Indefinite} needs a title.`);
|
|
178
|
+
if (t.length > exports.MAX_TITLE)
|
|
179
|
+
throw new ops_1.OpError(`A title may be at most ${exports.MAX_TITLE} characters.`);
|
|
180
|
+
return t;
|
|
181
|
+
}
|
|
182
|
+
function checkBody(kind, body) {
|
|
183
|
+
const b = body.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
184
|
+
if (b.length > exports.MAX_BODY)
|
|
185
|
+
throw new ops_1.OpError(`That is longer than ${kind.indefinite} may be.`);
|
|
186
|
+
return b;
|
|
187
|
+
}
|
|
188
|
+
/** Read a header for a write, refusing if it is not there. */
|
|
189
|
+
function openFor(kind, root, collection, repo, n) {
|
|
190
|
+
const dir = itemDir(kind, root, collection, repo, n);
|
|
191
|
+
const doc = dir ? readDoc(path.join(dir, kind.doc)) : null;
|
|
192
|
+
if (!dir || !doc)
|
|
193
|
+
throw new ops_1.OpError(`${kind.Bare} ${n} does not exist.`, 'notfound');
|
|
194
|
+
return { dir, doc };
|
|
195
|
+
}
|
|
196
|
+
function addComment(kind, root, collection, repo, n, input) {
|
|
197
|
+
const { dir, doc } = openFor(kind, root, collection, repo, n);
|
|
198
|
+
const body = checkBody(kind, input.body);
|
|
199
|
+
if (body.trim() === '')
|
|
200
|
+
throw new ops_1.OpError('A comment needs something in it.');
|
|
201
|
+
const commentsDir = path.join(dir, 'comments');
|
|
202
|
+
fs.mkdirSync(commentsDir, { recursive: true });
|
|
203
|
+
const used = fs
|
|
204
|
+
.readdirSync(commentsDir)
|
|
205
|
+
.filter((f) => /^[1-9][0-9]*\.md$/.test(f))
|
|
206
|
+
.map((f) => parseInt(f, 10));
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
let id = (used.length ? Math.max(...used) : 0) + 1;
|
|
209
|
+
for (let attempt = 0; attempt < 50; attempt++, id++) {
|
|
210
|
+
const file = path.join(commentsDir, `${id}.md`);
|
|
211
|
+
// Exclusive create is the allocation, as mkdir is for the numbers: an
|
|
212
|
+
// existsSync test leaves a window in which two writers pick the same id,
|
|
213
|
+
// and the second write would then replace the first comment outright.
|
|
214
|
+
try {
|
|
215
|
+
fs.closeSync(fs.openSync(file, 'wx'));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
writeDoc(file, { author: input.author, created: now }, body);
|
|
221
|
+
writeDoc(path.join(dir, kind.doc), { ...doc.meta, updated: now }, doc.body);
|
|
222
|
+
const container = kind.dirFor(root, collection, repo);
|
|
223
|
+
if (container)
|
|
224
|
+
touch(container);
|
|
225
|
+
return { id, author: input.author, created: now, body };
|
|
226
|
+
}
|
|
227
|
+
throw new ops_1.OpError('Could not add the comment; try again.', 'conflict');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Allocate the next free number and build the directory for it.
|
|
231
|
+
*
|
|
232
|
+
* mkdir is the allocation: whoever creates the directory owns the number, and
|
|
233
|
+
* the filesystem decides that rather than a check that two writers could both
|
|
234
|
+
* pass. A racing writer gets EEXIST and tries the next one.
|
|
235
|
+
*/
|
|
236
|
+
function allocate(kind, dir, write) {
|
|
237
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
238
|
+
let n = (numericDirs(dir).pop() ?? 0) + 1;
|
|
239
|
+
for (let attempt = 0; attempt < 50; attempt++, n++) {
|
|
240
|
+
const sub = path.join(dir, String(n));
|
|
241
|
+
try {
|
|
242
|
+
fs.mkdirSync(sub);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
fs.mkdirSync(path.join(sub, 'comments'), { recursive: true });
|
|
248
|
+
write(sub, n);
|
|
249
|
+
touch(dir);
|
|
250
|
+
return n;
|
|
251
|
+
}
|
|
252
|
+
throw new ops_1.OpError(`Could not allocate ${kind.indefinite} number; try again.`, 'conflict');
|
|
253
|
+
}
|