@magland/mochi 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
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;