@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/git.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitRepo = exports.GitError = void 0;
|
|
4
|
+
exports.execGit = execGit;
|
|
5
|
+
exports.execGitStatus = execGitStatus;
|
|
6
|
+
exports.isValidRefName = isValidRefName;
|
|
7
|
+
exports.isValidRepoPath = isValidRepoPath;
|
|
8
|
+
exports.isValidSha = isValidSha;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
11
|
+
class GitError extends Error {
|
|
12
|
+
}
|
|
13
|
+
exports.GitError = GitError;
|
|
14
|
+
function execGit(repoDir, args, opts = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = (0, child_process_1.execFile)('git', ['-C', repoDir, ...args], { maxBuffer: MAX_BUFFER, encoding: 'buffer', env: opts.env, timeout: opts.timeoutMs }, (err, stdout, stderr) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
const msg = stderr ? stderr.toString().trim() : err.message;
|
|
19
|
+
reject(new GitError(`git ${args[0]} failed: ${msg}`));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
resolve(Buffer.from(stdout));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (child.stdin) {
|
|
26
|
+
if (opts.input !== undefined)
|
|
27
|
+
child.stdin.write(opts.input);
|
|
28
|
+
child.stdin.end();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run git and report how it went rather than throwing, for the commands whose
|
|
34
|
+
* non-zero exit is an answer rather than a failure: `merge-tree` says "these
|
|
35
|
+
* files conflict" by exiting 1 with the conflict on stdout.
|
|
36
|
+
*/
|
|
37
|
+
function execGitStatus(repoDir, args, opts = {}) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
(0, child_process_1.execFile)('git', ['-C', repoDir, ...args], { maxBuffer: MAX_BUFFER, env: opts.env, timeout: opts.timeoutMs }, (err, stdout, stderr) => {
|
|
40
|
+
const failed = err;
|
|
41
|
+
if (failed && typeof failed.code !== 'number') {
|
|
42
|
+
reject(new GitError(`git ${args[0]} failed: ${stderr || failed.message}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
resolve({ code: failed?.code === undefined ? 0 : Number(failed.code), stdout, stderr });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function isValidRefName(ref) {
|
|
50
|
+
if (ref.length === 0 || ref.length > 300)
|
|
51
|
+
return false;
|
|
52
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._@+/-]*$/.test(ref))
|
|
53
|
+
return false;
|
|
54
|
+
if (ref.includes('..') || ref.endsWith('/') || ref.endsWith('.lock'))
|
|
55
|
+
return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function isValidRepoPath(p) {
|
|
59
|
+
if (p === '')
|
|
60
|
+
return true;
|
|
61
|
+
// A control character in a path is never meant, and a newline in one would
|
|
62
|
+
// let a later line of a git plumbing command be forged.
|
|
63
|
+
if (p.startsWith('/') || p.endsWith('/') || /[\x00-\x1f]/.test(p))
|
|
64
|
+
return false;
|
|
65
|
+
return p.split('/').every((seg) => seg !== '' && seg !== '.' && seg !== '..');
|
|
66
|
+
}
|
|
67
|
+
function isValidSha(s) {
|
|
68
|
+
return /^[0-9a-f]{40}$/i.test(s);
|
|
69
|
+
}
|
|
70
|
+
class GitRepo {
|
|
71
|
+
dir;
|
|
72
|
+
collection;
|
|
73
|
+
name;
|
|
74
|
+
constructor(dir, collection, name) {
|
|
75
|
+
this.dir = dir;
|
|
76
|
+
this.collection = collection;
|
|
77
|
+
this.name = name;
|
|
78
|
+
}
|
|
79
|
+
async lines(args) {
|
|
80
|
+
const out = (await execGit(this.dir, args)).toString('utf8');
|
|
81
|
+
return out.split('\n').filter((l) => l.length > 0);
|
|
82
|
+
}
|
|
83
|
+
async listRefs(kind) {
|
|
84
|
+
const fmt = '%(refname:short)%00%(objectname)%00%(creatordate:iso8601-strict)%00%(contents:subject)';
|
|
85
|
+
const ls = await this.lines(['for-each-ref', `--format=${fmt}`, '--sort=-creatordate', `refs/${kind}`]);
|
|
86
|
+
return ls.map((l) => {
|
|
87
|
+
const [name, sha, date, subject] = l.split('\0');
|
|
88
|
+
return { name, sha, date, subject: subject ?? '' };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async defaultBranch(branches) {
|
|
92
|
+
try {
|
|
93
|
+
const out = (await execGit(this.dir, ['symbolic-ref', '--short', 'HEAD'])).toString('utf8').trim();
|
|
94
|
+
if (branches.some((b) => b.name === out))
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// detached or unborn HEAD; fall through
|
|
99
|
+
}
|
|
100
|
+
return branches.length > 0 ? branches[0].name : null;
|
|
101
|
+
}
|
|
102
|
+
async lastUpdated() {
|
|
103
|
+
const ls = await this.lines([
|
|
104
|
+
'for-each-ref',
|
|
105
|
+
'--count=1',
|
|
106
|
+
'--sort=-committerdate',
|
|
107
|
+
'--format=%(committerdate:iso8601-strict)',
|
|
108
|
+
'refs/heads',
|
|
109
|
+
]);
|
|
110
|
+
return ls[0] ?? null;
|
|
111
|
+
}
|
|
112
|
+
async listTree(ref, path) {
|
|
113
|
+
const spec = path ? `${ref}:${path}` : ref;
|
|
114
|
+
const out = (await execGit(this.dir, ['ls-tree', '-z', '-l', spec])).toString('utf8');
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const item of out.split('\0')) {
|
|
117
|
+
if (!item)
|
|
118
|
+
continue;
|
|
119
|
+
const m = item.match(/^(\S+) (\S+) (\S+) +(\S+)\t([\s\S]*)$/);
|
|
120
|
+
if (!m)
|
|
121
|
+
continue;
|
|
122
|
+
entries.push({
|
|
123
|
+
mode: m[1],
|
|
124
|
+
type: m[2],
|
|
125
|
+
sha: m[3],
|
|
126
|
+
size: m[4] === '-' ? null : parseInt(m[4], 10),
|
|
127
|
+
name: m[5],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
entries.sort((a, b) => {
|
|
131
|
+
const ad = a.type === 'tree' ? 0 : 1;
|
|
132
|
+
const bd = b.type === 'tree' ? 0 : 1;
|
|
133
|
+
return ad !== bd ? ad - bd : a.name.localeCompare(b.name);
|
|
134
|
+
});
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
async entryType(ref, path) {
|
|
138
|
+
const spec = path ? `${ref}:${path}` : ref;
|
|
139
|
+
try {
|
|
140
|
+
return (await execGit(this.dir, ['cat-file', '-t', spec])).toString('utf8').trim();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async catBlob(ref, path) {
|
|
147
|
+
return execGit(this.dir, ['cat-file', 'blob', `${ref}:${path}`]);
|
|
148
|
+
}
|
|
149
|
+
async log(ref, skip, limit, path, author) {
|
|
150
|
+
const fmt = '%H%x00%an%x00%aI%x00%s';
|
|
151
|
+
const args = ['log', `--format=${fmt}`, '-n', String(limit), `--skip=${skip}`];
|
|
152
|
+
// -F makes --author a literal string rather than a pattern, so a name
|
|
153
|
+
// with a dot or a plus in it means itself.
|
|
154
|
+
if (author)
|
|
155
|
+
args.push('-F', `--author=${author}`);
|
|
156
|
+
args.push(ref, '--');
|
|
157
|
+
if (path)
|
|
158
|
+
args.push(path);
|
|
159
|
+
let ls;
|
|
160
|
+
try {
|
|
161
|
+
ls = await this.lines(args);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return ls.map((l) => {
|
|
167
|
+
const [sha, author, date, subject] = l.split('\0');
|
|
168
|
+
return { sha, author, date, subject: subject ?? '' };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The newest commit touching each of `paths`, keyed by path: what fills the
|
|
173
|
+
* message and age columns of a directory listing. A path with no commit (a
|
|
174
|
+
* submodule gitlink, say) is simply absent from the map.
|
|
175
|
+
*
|
|
176
|
+
* One `git log -1 -- path` per path is the obvious way to ask, and it is what
|
|
177
|
+
* this did, but it costs a process per row: a 150-entry listing spent about a
|
|
178
|
+
* second in `fork` alone. Instead one `git log --name-status` walks the
|
|
179
|
+
* history once, newest first, and each path takes the first commit whose
|
|
180
|
+
* changeset touches it. The walk is bounded twice over: git is asked only
|
|
181
|
+
* about the directory being listed, and the stream is closed as soon as the
|
|
182
|
+
* last path has an answer, which for a listing whose files are all recent is
|
|
183
|
+
* a handful of commits rather than the whole history.
|
|
184
|
+
*
|
|
185
|
+
* Attribution follows the diff, so a merge that carries no changes of its own
|
|
186
|
+
* is not the answer for anything -- the commit that made the change is, which
|
|
187
|
+
* is what `git log -- path` reports too. A rename counts for both of its
|
|
188
|
+
* names, so the row for either one dates from the rename.
|
|
189
|
+
*/
|
|
190
|
+
async lastCommits(ref, paths) {
|
|
191
|
+
const found = new Map();
|
|
192
|
+
if (paths.length === 0)
|
|
193
|
+
return found;
|
|
194
|
+
const want = new Set(paths);
|
|
195
|
+
// A changed file is claimed by whichever requested path contains it, so a
|
|
196
|
+
// directory row shows the newest commit under it. Walking up the file's own
|
|
197
|
+
// ancestors keeps this independent of where the requested paths sit.
|
|
198
|
+
const claim = (file) => {
|
|
199
|
+
if (want.has(file))
|
|
200
|
+
return file;
|
|
201
|
+
for (let cut = file.lastIndexOf('/'); cut > 0; cut = file.lastIndexOf('/', cut - 1)) {
|
|
202
|
+
const dir = file.slice(0, cut);
|
|
203
|
+
if (want.has(dir))
|
|
204
|
+
return dir;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
};
|
|
208
|
+
// The listed paths share a parent, so naming it keeps git from walking
|
|
209
|
+
// commits that cannot possibly touch any of them.
|
|
210
|
+
const first = paths[0];
|
|
211
|
+
const cut = first.lastIndexOf('/');
|
|
212
|
+
const scope = cut < 0 ? null : first.slice(0, cut);
|
|
213
|
+
const sharedScope = scope !== null && paths.every((p) => p.startsWith(`${scope}/`));
|
|
214
|
+
const args = [
|
|
215
|
+
'-C',
|
|
216
|
+
this.dir,
|
|
217
|
+
'log',
|
|
218
|
+
'-z',
|
|
219
|
+
'--name-status',
|
|
220
|
+
// Renames are wanted as renames rather than as an add and a delete, so
|
|
221
|
+
// that both names date from the same commit.
|
|
222
|
+
'--find-renames',
|
|
223
|
+
'--format=%x01%H%x00%an%x00%aI%x00%s',
|
|
224
|
+
ref,
|
|
225
|
+
'--',
|
|
226
|
+
];
|
|
227
|
+
if (sharedScope)
|
|
228
|
+
args.push(scope);
|
|
229
|
+
await new Promise((resolve) => {
|
|
230
|
+
const child = (0, child_process_1.spawn)('git', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
231
|
+
let done = false;
|
|
232
|
+
// Closing the pipe is what stops git; it takes a SIGPIPE on its next
|
|
233
|
+
// write. Resolving here rather than on 'close' means a listing satisfied
|
|
234
|
+
// by the first commit does not wait for a walk it no longer needs.
|
|
235
|
+
const finish = () => {
|
|
236
|
+
if (done)
|
|
237
|
+
return;
|
|
238
|
+
done = true;
|
|
239
|
+
child.stdout.destroy();
|
|
240
|
+
child.kill('SIGKILL');
|
|
241
|
+
resolve();
|
|
242
|
+
};
|
|
243
|
+
let buf = '';
|
|
244
|
+
child.stdout.setEncoding('utf8');
|
|
245
|
+
child.stdout.on('data', (chunk) => {
|
|
246
|
+
buf += chunk;
|
|
247
|
+
// The last record may still be growing, so hold it back until the
|
|
248
|
+
// marker that begins the next one has arrived.
|
|
249
|
+
const end = buf.lastIndexOf('\x01');
|
|
250
|
+
if (end <= 0)
|
|
251
|
+
return;
|
|
252
|
+
const ready = buf.slice(0, end);
|
|
253
|
+
buf = buf.slice(end);
|
|
254
|
+
if (this.takeCommits(ready, claim, found, want))
|
|
255
|
+
finish();
|
|
256
|
+
});
|
|
257
|
+
child.stdout.on('end', () => {
|
|
258
|
+
if (!done)
|
|
259
|
+
this.takeCommits(buf, claim, found, want);
|
|
260
|
+
finish();
|
|
261
|
+
});
|
|
262
|
+
child.on('error', finish);
|
|
263
|
+
child.on('close', finish);
|
|
264
|
+
});
|
|
265
|
+
return found;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Read whole `\x01`-led records out of a chunk of `git log --name-status`
|
|
269
|
+
* output, recording the first commit seen for each still-wanted path.
|
|
270
|
+
* Returns true once nothing is left to look for.
|
|
271
|
+
*/
|
|
272
|
+
takeCommits(chunk, claim, found, want) {
|
|
273
|
+
for (const record of chunk.split('\x01')) {
|
|
274
|
+
if (!record)
|
|
275
|
+
continue;
|
|
276
|
+
const parts = record.split('\0');
|
|
277
|
+
const [sha, author, date, subject] = parts;
|
|
278
|
+
if (!sha)
|
|
279
|
+
continue;
|
|
280
|
+
const commit = { sha, author, date, subject: subject ?? '' };
|
|
281
|
+
// parts[4] onwards is the diff: a status, then the one path it renames
|
|
282
|
+
// from and to, or the one path it touches.
|
|
283
|
+
for (let i = 4; i < parts.length; i++) {
|
|
284
|
+
const status = parts[i].replace(/^\n/, '');
|
|
285
|
+
if (!status)
|
|
286
|
+
continue;
|
|
287
|
+
const twoPaths = status[0] === 'R' || status[0] === 'C';
|
|
288
|
+
const files = twoPaths ? [parts[i + 1], parts[i + 2]] : [parts[i + 1]];
|
|
289
|
+
i += twoPaths ? 2 : 1;
|
|
290
|
+
for (const file of files) {
|
|
291
|
+
if (!file)
|
|
292
|
+
continue;
|
|
293
|
+
const path = claim(file);
|
|
294
|
+
if (path === null || found.has(path))
|
|
295
|
+
continue;
|
|
296
|
+
found.set(path, commit);
|
|
297
|
+
want.delete(path);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (want.size === 0)
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return want.size === 0;
|
|
304
|
+
}
|
|
305
|
+
async commitCount(ref, path, author) {
|
|
306
|
+
const args = ['rev-list', '--count'];
|
|
307
|
+
if (author)
|
|
308
|
+
args.push('-F', `--author=${author}`);
|
|
309
|
+
args.push(ref, '--');
|
|
310
|
+
if (path)
|
|
311
|
+
args.push(path);
|
|
312
|
+
const out = (await execGit(this.dir, args)).toString('utf8').trim();
|
|
313
|
+
return parseInt(out, 10);
|
|
314
|
+
}
|
|
315
|
+
async commit(sha) {
|
|
316
|
+
const fmt = '%H%x00%an%x00%ae%x00%aI%x00%P%x00%B';
|
|
317
|
+
let out;
|
|
318
|
+
try {
|
|
319
|
+
out = (await execGit(this.dir, ['log', '-1', `--format=${fmt}`, sha, '--'])).toString('utf8');
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const [h, an, ae, date, parents, message] = out.split('\0');
|
|
325
|
+
if (!h)
|
|
326
|
+
return null;
|
|
327
|
+
return {
|
|
328
|
+
sha: h,
|
|
329
|
+
author: an,
|
|
330
|
+
email: ae,
|
|
331
|
+
date,
|
|
332
|
+
parents: parents ? parents.split(' ').filter((p) => p) : [],
|
|
333
|
+
message: (message ?? '').replace(/\n+$/, ''),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/** Every file at a ref, in git's order. */
|
|
337
|
+
async listPaths(ref, limit) {
|
|
338
|
+
const out = (await execGit(this.dir, ['ls-tree', '-r', '--name-only', '-z', ref])).toString('utf8');
|
|
339
|
+
const all = out.split('\0').filter((p) => p !== '');
|
|
340
|
+
return { paths: all.slice(0, limit), truncated: all.length > limit };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Text search over the files at a ref, as `git grep` does it: fixed strings
|
|
344
|
+
* rather than a regular expression, since a search box is not a place to
|
|
345
|
+
* hand a reader's typing to a matcher that can be made to run away, and
|
|
346
|
+
* skipping binary files, which have no lines to show. The query travels as
|
|
347
|
+
* the argument of -e, so a leading dash is a search term and not an option.
|
|
348
|
+
*
|
|
349
|
+
* Both the number of results and the time git may spend are bounded: this
|
|
350
|
+
* is the one read in the interface whose cost the caller cannot predict.
|
|
351
|
+
*/
|
|
352
|
+
async search(ref, query, limit = 200, timeoutMs = 10000) {
|
|
353
|
+
let out;
|
|
354
|
+
try {
|
|
355
|
+
out = (await execGit(this.dir, ['grep', '-I', '-n', '-z', '--fixed-strings', '--ignore-case', '-e', query, ref, '--'], { timeoutMs })).toString('utf8');
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// git grep exits non-zero when nothing matched, which is not an error.
|
|
359
|
+
return { hits: [], truncated: false };
|
|
360
|
+
}
|
|
361
|
+
const hits = [];
|
|
362
|
+
let truncated = false;
|
|
363
|
+
for (const line of out.split('\n')) {
|
|
364
|
+
if (line === '')
|
|
365
|
+
continue;
|
|
366
|
+
if (hits.length >= limit) {
|
|
367
|
+
truncated = true;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
// <rev>:<path>\0<lineno>\0<text>
|
|
371
|
+
const [head, lineNo, ...rest] = line.split('\0');
|
|
372
|
+
if (lineNo === undefined)
|
|
373
|
+
continue;
|
|
374
|
+
const colon = head.indexOf(':');
|
|
375
|
+
if (colon === -1)
|
|
376
|
+
continue;
|
|
377
|
+
hits.push({ path: head.slice(colon + 1), line: parseInt(lineNo, 10), text: rest.join('\0') });
|
|
378
|
+
}
|
|
379
|
+
return { hits, truncated };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* `git blame` for one file, one entry per line in file order. The porcelain
|
|
383
|
+
* format repeats every header for every line, which costs a little output
|
|
384
|
+
* and saves the parser from carrying state between lines; the caller groups
|
|
385
|
+
* consecutive lines that share a commit.
|
|
386
|
+
*/
|
|
387
|
+
async blame(ref, path) {
|
|
388
|
+
const out = (await execGit(this.dir, ['blame', '--line-porcelain', ref, '--', path])).toString('utf8');
|
|
389
|
+
const lines = [];
|
|
390
|
+
let cur = {};
|
|
391
|
+
for (const line of out.split('\n')) {
|
|
392
|
+
if (line.startsWith('\t')) {
|
|
393
|
+
// The content line closes an entry: everything before it described it.
|
|
394
|
+
lines.push({
|
|
395
|
+
sha: cur.sha ?? '',
|
|
396
|
+
author: cur.author ?? '',
|
|
397
|
+
date: cur.time ? new Date(cur.time * 1000).toISOString() : '',
|
|
398
|
+
summary: cur.summary ?? '',
|
|
399
|
+
previous: cur.previous ?? null,
|
|
400
|
+
text: line.slice(1),
|
|
401
|
+
});
|
|
402
|
+
cur = {};
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const m = line.match(/^([0-9a-f]{40}) \d+ \d+/);
|
|
406
|
+
if (m) {
|
|
407
|
+
cur = { sha: m[1] };
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const sp = line.indexOf(' ');
|
|
411
|
+
const key = sp === -1 ? line : line.slice(0, sp);
|
|
412
|
+
const value = sp === -1 ? '' : line.slice(sp + 1);
|
|
413
|
+
if (key === 'author')
|
|
414
|
+
cur.author = value;
|
|
415
|
+
else if (key === 'author-time')
|
|
416
|
+
cur.time = parseInt(value, 10);
|
|
417
|
+
else if (key === 'summary')
|
|
418
|
+
cur.summary = value;
|
|
419
|
+
else if (key === 'previous') {
|
|
420
|
+
const sep = value.indexOf(' ');
|
|
421
|
+
if (sep !== -1)
|
|
422
|
+
cur.previous = { sha: value.slice(0, sep), path: value.slice(sep + 1) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Comparing two revisions, the way `git log base..head` and
|
|
429
|
+
* `git diff base...head` do: the commits head has that base does not, and
|
|
430
|
+
* the patch that would bring base up to head. The three-dot diff is taken
|
|
431
|
+
* from the merge base, so a base that has moved on of its own does not show
|
|
432
|
+
* up as changes head made; that is the comparison GitHub shows and the one
|
|
433
|
+
* a reader almost always means.
|
|
434
|
+
*/
|
|
435
|
+
async compare(base, head, limit = 250) {
|
|
436
|
+
const fmt = '%H%x00%an%x00%aI%x00%s';
|
|
437
|
+
const [counts, log, patch, mergeBase] = await Promise.all([
|
|
438
|
+
execGit(this.dir, ['rev-list', '--left-right', '--count', `${base}...${head}`])
|
|
439
|
+
.then((b) => b.toString('utf8').trim().split(/\s+/))
|
|
440
|
+
.catch(() => ['0', '0']),
|
|
441
|
+
execGit(this.dir, ['log', `--format=${fmt}`, '-n', String(limit), `${base}..${head}`, '--'])
|
|
442
|
+
.then((b) => b.toString('utf8'))
|
|
443
|
+
.catch(() => ''),
|
|
444
|
+
execGit(this.dir, ['diff', '--no-color', `${base}...${head}`])
|
|
445
|
+
.then((b) => b.toString('utf8'))
|
|
446
|
+
.catch(() => ''),
|
|
447
|
+
execGit(this.dir, ['merge-base', base, head])
|
|
448
|
+
.then((b) => b.toString('utf8').trim())
|
|
449
|
+
.catch(() => null),
|
|
450
|
+
]);
|
|
451
|
+
const commits = log
|
|
452
|
+
.split('\n')
|
|
453
|
+
.filter((l) => l !== '')
|
|
454
|
+
.map((l) => {
|
|
455
|
+
const [sha, author, date, subject] = l.split('\0');
|
|
456
|
+
return { sha, author, date, subject: subject ?? '' };
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
behind: parseInt(counts[0], 10) || 0,
|
|
460
|
+
ahead: parseInt(counts[1], 10) || 0,
|
|
461
|
+
commits,
|
|
462
|
+
patch,
|
|
463
|
+
mergeBase: mergeBase || null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Who has committed here and how much, as `git shortlog -sne` reports it,
|
|
468
|
+
* ordered by commit count. Mail addresses come back so that an avatar and a
|
|
469
|
+
* contact are both available; the interface shows the name.
|
|
470
|
+
*/
|
|
471
|
+
async contributors(ref) {
|
|
472
|
+
let out;
|
|
473
|
+
try {
|
|
474
|
+
out = (await execGit(this.dir, ['shortlog', '-sne', '--no-merges', ref, '--'])).toString('utf8');
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
const people = [];
|
|
480
|
+
for (const line of out.split('\n')) {
|
|
481
|
+
const m = line.match(/^\s*(\d+)\t(.*?)\s*<([^>]*)>\s*$/);
|
|
482
|
+
if (m)
|
|
483
|
+
people.push({ commits: parseInt(m[1], 10), name: m[2], email: m[3] });
|
|
484
|
+
}
|
|
485
|
+
return people;
|
|
486
|
+
}
|
|
487
|
+
async commitPatch(sha) {
|
|
488
|
+
return (await execGit(this.dir, ['show', '--format=', '--patch', '--no-color', sha, '--'])).toString('utf8');
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Stream `git archive` for `ref` into `out`, as GitHub's source downloads
|
|
492
|
+
* do. Streamed rather than buffered because an archive of a large
|
|
493
|
+
* repository has no business sitting in memory, which also means the
|
|
494
|
+
* caller must satisfy itself that the ref exists before the first byte
|
|
495
|
+
* goes out: once the response has begun there is no status code left to
|
|
496
|
+
* change. `prefix` is the directory the archive unpacks into.
|
|
497
|
+
*/
|
|
498
|
+
archiveTo(ref, format, prefix, out) {
|
|
499
|
+
return new Promise((resolve, reject) => {
|
|
500
|
+
const child = (0, child_process_1.spawn)('git', ['-C', this.dir, 'archive', `--format=${format}`, `--prefix=${prefix}`, ref], {
|
|
501
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
502
|
+
});
|
|
503
|
+
let stderr = '';
|
|
504
|
+
child.stderr.on('data', (d) => {
|
|
505
|
+
stderr += d.toString();
|
|
506
|
+
});
|
|
507
|
+
child.stdout.on('data', (chunk) => {
|
|
508
|
+
if (!out.write(chunk)) {
|
|
509
|
+
child.stdout.pause();
|
|
510
|
+
out.once('drain', () => child.stdout.resume());
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
child.on('error', reject);
|
|
514
|
+
child.on('close', (code) => {
|
|
515
|
+
if (code === 0)
|
|
516
|
+
resolve();
|
|
517
|
+
else
|
|
518
|
+
reject(new GitError(`git archive failed: ${stderr.trim() || `exit ${code}`}`));
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** The object a rev names, or null if git does not know it. */
|
|
523
|
+
async resolve(rev) {
|
|
524
|
+
try {
|
|
525
|
+
return (await execGit(this.dir, ['rev-parse', '--verify', '--end-of-options', `${rev}^{commit}`]))
|
|
526
|
+
.toString('utf8')
|
|
527
|
+
.trim();
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
resolveRefAndPath(rest, refNames) {
|
|
534
|
+
const sorted = [...refNames].sort((a, b) => b.length - a.length);
|
|
535
|
+
for (const r of sorted) {
|
|
536
|
+
if (rest === r)
|
|
537
|
+
return { ref: r, path: '' };
|
|
538
|
+
if (rest.startsWith(r + '/'))
|
|
539
|
+
return { ref: r, path: rest.slice(r.length + 1) };
|
|
540
|
+
}
|
|
541
|
+
const i = rest.indexOf('/');
|
|
542
|
+
if (i === -1)
|
|
543
|
+
return { ref: rest, path: '' };
|
|
544
|
+
return { ref: rest.slice(0, i), path: rest.slice(i + 1) };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
exports.GitRepo = GitRepo;
|