@moritzwilksch/diffle 0.1.7 → 0.1.9
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/README.md +91 -113
- package/dist/client/assets/{angular-html-gsHTuMGB.js → angular-html-CJY2n6PT.js} +1 -1
- package/dist/client/assets/{angular-ts-xhBJShFG.js → angular-ts-CZTM8_xo.js} +1 -1
- package/dist/client/assets/{apl-nsC6ra4J.js → apl-Dcp77-Tz.js} +1 -1
- package/dist/client/assets/{astro-X5_I_ruP.js → astro-C0gdsdoF.js} +1 -1
- package/dist/client/assets/{blade-BqlxrrkS.js → blade-BYpI0fWU.js} +1 -1
- package/dist/client/assets/{c-DVgCXnBp.js → c-DvFVe1b3.js} +1 -1
- package/dist/client/assets/{chapel-LgKo5FoM.js → chapel-qAWjGK-V.js} +1 -1
- package/dist/client/assets/{cobol-BVkDOaHP.js → cobol-BeFQKGES.js} +1 -1
- package/dist/client/assets/{coffee-BVuWHkH6.js → coffee-XLni1sjM.js} +1 -1
- package/dist/client/assets/{cpp-DnxElGuW.js → cpp-ChsJNYBC.js} +1 -1
- package/dist/client/assets/{crystal-CC8Vxuob.js → crystal-D3SbG8yD.js} +1 -1
- package/dist/client/assets/{css-BBuKGmvC.js → css-VXnGhPbY.js} +1 -1
- package/dist/client/assets/{edge-CNOZBEfw.js → edge-Dbzmpk4W.js} +1 -1
- package/dist/client/assets/{elixir-BG8NE-xg.js → elixir-DvQULd_0.js} +1 -1
- package/dist/client/assets/{elm-Cyp6TnT_.js → elm-BM5mpULp.js} +1 -1
- package/dist/client/assets/{erb-C58VxpfX.js → erb-nYCgf5TS.js} +1 -1
- package/dist/client/assets/{git-rebase-A6HkVJC_.js → git-rebase-CcDsi2L8.js} +1 -1
- package/dist/client/assets/{glimmer-js-CqR_jN8G.js → glimmer-js-94igFmPU.js} +1 -1
- package/dist/client/assets/{glimmer-ts-M3ExOO4n.js → glimmer-ts-8aOBlhVe.js} +1 -1
- package/dist/client/assets/{glsl--WIxeXSU.js → glsl-DAW_sXZj.js} +1 -1
- package/dist/client/assets/{graphql-BXRMLGZY.js → graphql-BEeRZxhy.js} +1 -1
- package/dist/client/assets/{hack-C8N7KWCY.js → hack-e4pIPigB.js} +1 -1
- package/dist/client/assets/{haml-CxHPK-iA.js → haml-DvZH4eyc.js} +1 -1
- package/dist/client/assets/{handlebars-BNjxS1qh.js → handlebars-Bod10MIb.js} +1 -1
- package/dist/client/assets/{html-CO9JbMOk.js → html-BOKVgXUz.js} +1 -1
- package/dist/client/assets/{html-derivative-CD1w-J8T.js → html-derivative-B5DftdEs.js} +1 -1
- package/dist/client/assets/{http-DvdIZe4B.js → http-Bli9WHgJ.js} +1 -1
- package/dist/client/assets/{hurl-BPchDEWD.js → hurl-C45hn_gr.js} +1 -1
- package/dist/client/assets/index-BugDKSp3.js +2994 -0
- package/dist/client/assets/index-CvgG7WNQ.css +2 -0
- package/dist/client/assets/{java-Cw12dAwx.js → java-BxezylI4.js} +1 -1
- package/dist/client/assets/{javascript-CVrIgFqG.js → javascript-CGUJoh42.js} +1 -1
- package/dist/client/assets/{jinja-Dcw1Cb-z.js → jinja-Dhrq1TMH.js} +1 -1
- package/dist/client/assets/{jison-BCHOo1FF.js → jison-CK02w6aE.js} +1 -1
- package/dist/client/assets/{json-BkfEJ7uD.js → json-DbjMq68q.js} +1 -1
- package/dist/client/assets/{jsx-CBE97uyR.js → jsx-BVYjBFnX.js} +1 -1
- package/dist/client/assets/{julia-Dt37WVZB.js → julia-DUG0dPfw.js} +1 -1
- package/dist/client/assets/{just-DFHYF8cW.js → just-BzILq64e.js} +1 -1
- package/dist/client/assets/{latex-BtlIyAq9.js → latex-DJ-r-_gD.js} +1 -1
- package/dist/client/assets/{liquid-PdbqGeMi.js → liquid-CUeuBds8.js} +1 -1
- package/dist/client/assets/{lua-lhtz56hU.js → lua-CUx56wFu.js} +1 -1
- package/dist/client/assets/{marko-DPsaza10.js → marko-zidFMx4y.js} +1 -1
- package/dist/client/assets/{mdc-DIH2Ec5y.js → mdc-B8YDeE-G.js} +1 -1
- package/dist/client/assets/{nginx-bHk_XM7O.js → nginx-CLmnmibq.js} +1 -1
- package/dist/client/assets/{nim-BgUabkBA.js → nim-D_ow61_F.js} +1 -1
- package/dist/client/assets/{org-PMkj4aaP.js → org-D6yFID6y.js} +1 -1
- package/dist/client/assets/{perl-B1ZCbepN.js → perl-DlRNqEum.js} +1 -1
- package/dist/client/assets/{php-Vj6Jmzuy.js → php-CPe77eH-.js} +1 -1
- package/dist/client/assets/{pug-B0ziVZ95.js → pug-CzDq8hOp.js} +1 -1
- package/dist/client/assets/{qml--NUJOMX2.js → qml-CmVqwUZS.js} +1 -1
- package/dist/client/assets/{r-CCy6kg5Q.js → r-CZql-PDg.js} +1 -1
- package/dist/client/assets/{razor-DBTJol_D.js → razor-Dp5yql5G.js} +1 -1
- package/dist/client/assets/{regexp-B-0M8p3f.js → regexp-DjyDhvtX.js} +1 -1
- package/dist/client/assets/{rst-DVPZR2Zw.js → rst-B-Q6Ljb6.js} +1 -1
- package/dist/client/assets/{ruby-CY-1xRWL.js → ruby-BQD6FS2h.js} +1 -1
- package/dist/client/assets/{sas-BfWvhRC7.js → sas-DDGcym6k.js} +1 -1
- package/dist/client/assets/{scss-CDSNB-cI.js → scss-DNPPx0rx.js} +1 -1
- package/dist/client/assets/{shellscript-x3a-v0TL.js → shellscript-CHWpWo83.js} +1 -1
- package/dist/client/assets/{shellsession-CBXmn1pk.js → shellsession-_KE7LUMO.js} +1 -1
- package/dist/client/assets/{soy-Dx9wgv4j.js → soy-B57xVh7W.js} +1 -1
- package/dist/client/assets/{sql-GJE8dpQY.js → sql-8OXlDHCR.js} +1 -1
- package/dist/client/assets/{stata-Cu3eQvZM.js → stata-C452rT5l.js} +1 -1
- package/dist/client/assets/{surrealql-BSIyNQF3.js → surrealql-B5gNeOUk.js} +1 -1
- package/dist/client/assets/{svelte-BC05E7SM.js → svelte-Cv_BwjgR.js} +1 -1
- package/dist/client/assets/{templ-BZb4iLGg.js → templ-qZdOs42N.js} +1 -1
- package/dist/client/assets/{tex-BgSQdktB.js → tex-BK-MJpDM.js} +1 -1
- package/dist/client/assets/{ts-tags-ByZvgo6s.js → ts-tags-C5Q-Olrs.js} +1 -1
- package/dist/client/assets/{tsx-BkywoqnG.js → tsx-DwPm0lid.js} +1 -1
- package/dist/client/assets/{twig-DT3o-XyD.js → twig-DKj0IG75.js} +1 -1
- package/dist/client/assets/{typescript-Dc_VYLnG.js → typescript-BeWKsdVP.js} +1 -1
- package/dist/client/assets/{typst-Dufd3udg.js → typst-B7u-3Y0n.js} +1 -1
- package/dist/client/assets/{vue-CWow0PK2.js → vue-BkC1F40W.js} +1 -1
- package/dist/client/assets/{vue-html-Dukrnida.js → vue-html-BeYXHyr2.js} +1 -1
- package/dist/client/assets/{vue-vine-DeFkcU4G.js → vue-vine-ymsHpgRC.js} +1 -1
- package/dist/client/assets/{xml-w1QbHsNc.js → xml-B6M3xnfe.js} +1 -1
- package/dist/client/assets/{xsl-DQ1uA8pZ.js → xsl-CSlbm6tK.js} +1 -1
- package/dist/client/assets/{yaml-Bt6SDR__.js → yaml-fMwD58zK.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/main.js +426 -85
- package/package.json +19 -3
- package/dist/client/assets/index-B47iHZXu.css +0 -2
- package/dist/client/assets/index-BcTYvSA1.js +0 -2989
package/dist/server/main.js
CHANGED
|
@@ -11,6 +11,7 @@ import { copyFile, lstat, mkdir, mkdtemp, open, readFile, readlink, realpath, re
|
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
13
13
|
import { createServer } from "node:http";
|
|
14
|
+
import { getAssetKeys, getRawAsset, isSea } from "node:sea";
|
|
14
15
|
import { Http2ServerRequest, constants as constants$1 } from "http2";
|
|
15
16
|
import { Readable } from "stream";
|
|
16
17
|
import crypto from "crypto";
|
|
@@ -3951,7 +3952,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
|
|
|
3951
3952
|
}
|
|
3952
3953
|
//#endregion
|
|
3953
3954
|
//#region package.json
|
|
3954
|
-
var version$1 = "0.1.
|
|
3955
|
+
var version$1 = "0.1.9";
|
|
3955
3956
|
//#endregion
|
|
3956
3957
|
//#region src/server/comments/anchor.ts
|
|
3957
3958
|
function splitLines(contents) {
|
|
@@ -4198,6 +4199,7 @@ var GitRepo = class GitRepo {
|
|
|
4198
4199
|
new: next
|
|
4199
4200
|
};
|
|
4200
4201
|
}
|
|
4202
|
+
/** A commit's hash, abbreviation and full message; null when `rev` names no commit. */
|
|
4201
4203
|
async commitInfo(rev) {
|
|
4202
4204
|
let sha;
|
|
4203
4205
|
try {
|
|
@@ -4221,6 +4223,66 @@ var GitRepo = class GitRepo {
|
|
|
4221
4223
|
message: output.slice(separator + 1).trimEnd()
|
|
4222
4224
|
};
|
|
4223
4225
|
}
|
|
4226
|
+
/**
|
|
4227
|
+
* `sha`'s first parent, and the commit toward HEAD whose first parent it is. The
|
|
4228
|
+
* child is null at HEAD, on an unborn branch, or when `sha` is off HEAD's
|
|
4229
|
+
* first-parent line, e.g. on a merged side branch.
|
|
4230
|
+
*/
|
|
4231
|
+
async commitNeighbours(sha) {
|
|
4232
|
+
const [parent, list] = await Promise.all([this.resolve(`${sha}^`).catch((e) => {
|
|
4233
|
+
if (e instanceof GitError && e.code === 1) return null;
|
|
4234
|
+
throw e;
|
|
4235
|
+
}), this.text([
|
|
4236
|
+
"rev-list",
|
|
4237
|
+
"--first-parent",
|
|
4238
|
+
"--ancestry-path",
|
|
4239
|
+
"--parents",
|
|
4240
|
+
`${sha}..HEAD`,
|
|
4241
|
+
"--"
|
|
4242
|
+
]).catch(() => "")]);
|
|
4243
|
+
const [child, firstParent] = list.trim().split("\n").pop()?.split(" ") ?? [];
|
|
4244
|
+
return {
|
|
4245
|
+
parent,
|
|
4246
|
+
child: child && firstParent === sha ? child : null
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
/** The commits a revision range such as `a..b` or `c^!` selects, oldest first: the newest `limit`, and the total. */
|
|
4250
|
+
async rangeCommits(range, limit) {
|
|
4251
|
+
const [count, log] = await Promise.all([this.text([
|
|
4252
|
+
"rev-list",
|
|
4253
|
+
"--count",
|
|
4254
|
+
"--end-of-options",
|
|
4255
|
+
range,
|
|
4256
|
+
"--"
|
|
4257
|
+
]), this.text([
|
|
4258
|
+
"log",
|
|
4259
|
+
"-z",
|
|
4260
|
+
"--no-show-signature",
|
|
4261
|
+
"--reverse",
|
|
4262
|
+
`--max-count=${limit}`,
|
|
4263
|
+
"--format=%H%x00%h%x00%an%x00%ae%x00%at%x00%B",
|
|
4264
|
+
"--end-of-options",
|
|
4265
|
+
range,
|
|
4266
|
+
"--"
|
|
4267
|
+
])]);
|
|
4268
|
+
const fields = log.split("\0");
|
|
4269
|
+
const list = [];
|
|
4270
|
+
for (let i = 0; i + 5 < fields.length; i += 6) {
|
|
4271
|
+
const [sha = "", short = "", author = "", email = "", time = "", message = ""] = fields.slice(i, i + 6);
|
|
4272
|
+
list.push({
|
|
4273
|
+
sha,
|
|
4274
|
+
short,
|
|
4275
|
+
message: message.trimEnd(),
|
|
4276
|
+
author,
|
|
4277
|
+
email,
|
|
4278
|
+
date: Number(time) * 1e3
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
return {
|
|
4282
|
+
list,
|
|
4283
|
+
total: Number(count.trim())
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4224
4286
|
/** The empty tree under the repository's hash algorithm: the old side of an unborn branch. */
|
|
4225
4287
|
async emptyTree() {
|
|
4226
4288
|
return (await this.text([
|
|
@@ -4369,7 +4431,7 @@ var GitRepo = class GitRepo {
|
|
|
4369
4431
|
}
|
|
4370
4432
|
/**
|
|
4371
4433
|
* Changed files between two revs, or a rev and the worktree (incl. untracked).
|
|
4372
|
-
* One diff call yields status, line counts and, against a commit,
|
|
4434
|
+
* One diff call yields status, line counts and, against a commit, each side's
|
|
4373
4435
|
* blob; the worktree's blobs are hashed from disk.
|
|
4374
4436
|
*/
|
|
4375
4437
|
async numstat(oldRev, newRev) {
|
|
@@ -4395,6 +4457,7 @@ var GitRepo = class GitRepo {
|
|
|
4395
4457
|
deletions: 0,
|
|
4396
4458
|
binary: false,
|
|
4397
4459
|
blob: "",
|
|
4460
|
+
oldBlob: "",
|
|
4398
4461
|
generated: false
|
|
4399
4462
|
};
|
|
4400
4463
|
const st = await lstat(resolve(this.root, path)).catch(() => null);
|
|
@@ -4411,44 +4474,68 @@ var GitRepo = class GitRepo {
|
|
|
4411
4474
|
return file;
|
|
4412
4475
|
});
|
|
4413
4476
|
const all = [...files, ...extra].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
4414
|
-
if (newRev === "worktree")
|
|
4477
|
+
if (newRev === "worktree") {
|
|
4478
|
+
const present = all.filter((f) => f.status !== "D");
|
|
4479
|
+
(await this.hashWorktree(present)).forEach((sha, i) => present[i].blob = sha);
|
|
4480
|
+
}
|
|
4481
|
+
if (reverse) {
|
|
4482
|
+
const present = all.filter((f) => f.status !== "A");
|
|
4483
|
+
const entries = present.map((f) => ({
|
|
4484
|
+
path: f.oldPath ?? f.path,
|
|
4485
|
+
submodule: f.submodule
|
|
4486
|
+
}));
|
|
4487
|
+
(await this.hashWorktree(entries)).forEach((sha, i) => present[i].oldBlob = sha);
|
|
4488
|
+
}
|
|
4415
4489
|
return all;
|
|
4416
4490
|
}
|
|
4417
4491
|
/**
|
|
4418
|
-
*
|
|
4492
|
+
* What git would store for each worktree entry, in order: the file's
|
|
4419
4493
|
* blob, a symlink's target blob, a submodule's checked-out commit. Regular
|
|
4420
4494
|
* files go to one `hash-object --stdin-paths` call; everything else is hashed
|
|
4421
4495
|
* on its own, so a gitlink, a broken link or an unreadable path never fails
|
|
4422
4496
|
* the batch. A path containing a newline cannot ride the stdin list either.
|
|
4423
|
-
* A path that cannot be hashed
|
|
4497
|
+
* A path that cannot be hashed gets an empty blob.
|
|
4424
4498
|
*/
|
|
4425
4499
|
async hashWorktree(files) {
|
|
4500
|
+
const shas = files.map(() => "");
|
|
4426
4501
|
const regular = [];
|
|
4427
|
-
await mapLimit(files, READ_CONCURRENCY$1, async (
|
|
4428
|
-
|
|
4429
|
-
if (
|
|
4430
|
-
|
|
4502
|
+
await mapLimit([...files.keys()], READ_CONCURRENCY$1, async (i) => {
|
|
4503
|
+
const { path, submodule } = files[i];
|
|
4504
|
+
if (submodule) {
|
|
4505
|
+
shas[i] = await this.submoduleHead(path);
|
|
4431
4506
|
return;
|
|
4432
4507
|
}
|
|
4433
|
-
const st = await lstat(resolve(this.root,
|
|
4434
|
-
if (st?.isSymbolicLink())
|
|
4435
|
-
else if (st?.isFile()) regular.push(
|
|
4508
|
+
const st = await lstat(resolve(this.root, path)).catch(() => null);
|
|
4509
|
+
if (st?.isSymbolicLink()) shas[i] = blobSha(Buffer.from(await readlink(resolve(this.root, path)), "utf8"));
|
|
4510
|
+
else if (st?.isFile()) regular.push(i);
|
|
4436
4511
|
});
|
|
4437
|
-
const batch = regular.filter((
|
|
4438
|
-
const single = regular.filter((
|
|
4512
|
+
const batch = regular.filter((i) => !files[i].path.includes("\n"));
|
|
4513
|
+
const single = regular.filter((i) => files[i].path.includes("\n"));
|
|
4439
4514
|
if (batch.length) try {
|
|
4440
|
-
const
|
|
4441
|
-
batch.forEach((
|
|
4515
|
+
const lines = (await this.text(["hash-object", "--stdin-paths"], { input: batch.map((i) => files[i].path).join("\n") + "\n" })).trim().split("\n");
|
|
4516
|
+
batch.forEach((i, j) => shas[i] = lines[j] ?? "");
|
|
4442
4517
|
} catch {
|
|
4443
4518
|
single.push(...batch);
|
|
4444
4519
|
}
|
|
4445
|
-
await mapLimit(single, READ_CONCURRENCY$1, async (
|
|
4446
|
-
|
|
4520
|
+
await mapLimit(single, READ_CONCURRENCY$1, async (i) => {
|
|
4521
|
+
shas[i] = await this.text([
|
|
4447
4522
|
"hash-object",
|
|
4448
4523
|
"--",
|
|
4449
|
-
|
|
4524
|
+
files[i].path
|
|
4450
4525
|
]).then((s) => s.trim()).catch(() => "");
|
|
4451
4526
|
});
|
|
4527
|
+
return shas;
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* What git would store for `content` at worktree `path`, its clean filters applied: equals the
|
|
4531
|
+
* snapshot's blob for a worktree file whose bytes are still the snapshot's.
|
|
4532
|
+
*/
|
|
4533
|
+
async hashObject(content, path) {
|
|
4534
|
+
return (await this.text([
|
|
4535
|
+
"hash-object",
|
|
4536
|
+
"--stdin",
|
|
4537
|
+
`--path=${path}`
|
|
4538
|
+
], { input: content })).trim();
|
|
4452
4539
|
}
|
|
4453
4540
|
/** The commit a checked-out submodule is at, or '' if it is not a repository. */
|
|
4454
4541
|
async submoduleHead(path) {
|
|
@@ -5048,8 +5135,8 @@ function splitZ(buf) {
|
|
|
5048
5135
|
* `-z -M --raw --no-abbrev --numstat`: the raw records come first, then the numstat records,
|
|
5049
5136
|
* each path-terminated by NUL. Raw: ":mode mode sha sha X\0path\0", renames and
|
|
5050
5137
|
* copies "R100\0old\0new\0". Numstat: "add\tdel\tpath\0", renames "add\tdel\t\0old\0new\0".
|
|
5051
|
-
* Binary files count as "-\t-". The dst
|
|
5052
|
-
* commit; against the worktree
|
|
5138
|
+
* Binary files count as "-\t-". The src and dst shas are each side's blob
|
|
5139
|
+
* against a commit; against the worktree, or where a side is absent, they are zeros.
|
|
5053
5140
|
* Mode 160000 on either side marks a gitlink, which no file operation may touch.
|
|
5054
5141
|
*/
|
|
5055
5142
|
function parseRawNumstat(buf) {
|
|
@@ -5057,7 +5144,7 @@ function parseRawNumstat(buf) {
|
|
|
5057
5144
|
const byPath = /* @__PURE__ */ new Map();
|
|
5058
5145
|
let i = 0;
|
|
5059
5146
|
for (; i < parts.length && parts[i].startsWith(":"); i++) {
|
|
5060
|
-
const [srcMode = "", dstMode = "", , dstSha = "", code = ""] = parts[i].split(" ");
|
|
5147
|
+
const [srcMode = "", dstMode = "", srcSha = "", dstSha = "", code = ""] = parts[i].split(" ");
|
|
5061
5148
|
const status = code[0];
|
|
5062
5149
|
const oldPath = status === "R" || status === "C" ? parts[++i] : void 0;
|
|
5063
5150
|
const path = parts[++i];
|
|
@@ -5068,6 +5155,7 @@ function parseRawNumstat(buf) {
|
|
|
5068
5155
|
deletions: 0,
|
|
5069
5156
|
binary: false,
|
|
5070
5157
|
blob: /^0+$/.test(dstSha) ? "" : dstSha,
|
|
5158
|
+
oldBlob: /^0+$/.test(srcSha) ? "" : srcSha,
|
|
5071
5159
|
generated: false
|
|
5072
5160
|
};
|
|
5073
5161
|
if (oldPath != null) file.oldPath = oldPath;
|
|
@@ -12073,14 +12161,26 @@ const ModeRequestSchema = discriminatedUnion("kind", [
|
|
|
12073
12161
|
object({
|
|
12074
12162
|
kind: literal("revspec"),
|
|
12075
12163
|
args: string().array()
|
|
12164
|
+
}),
|
|
12165
|
+
object({
|
|
12166
|
+
kind: literal("focus"),
|
|
12167
|
+
commit: string().nullable()
|
|
12076
12168
|
})
|
|
12077
12169
|
]);
|
|
12078
|
-
/**
|
|
12079
|
-
|
|
12170
|
+
/**
|
|
12171
|
+
* How the old side is derived: `old` itself, merge-base(old, new) with worktree
|
|
12172
|
+
* taken as HEAD, or new's first parent (the empty tree for a root commit).
|
|
12173
|
+
*/
|
|
12174
|
+
const ComparisonBaseSchema = _enum([
|
|
12175
|
+
"direct",
|
|
12176
|
+
"merge-base",
|
|
12177
|
+
"parent"
|
|
12178
|
+
]);
|
|
12179
|
+
/** A comparison; both endpoints accept a Git revision or "worktree". Under `parent`, both name the one commit. */
|
|
12180
|
+
const ComparisonSchema = object({
|
|
12080
12181
|
old: string(),
|
|
12081
12182
|
new: string(),
|
|
12082
|
-
|
|
12083
|
-
mergeBase: boolean(),
|
|
12183
|
+
base: ComparisonBaseSchema,
|
|
12084
12184
|
live: _enum([
|
|
12085
12185
|
"worktree",
|
|
12086
12186
|
"refs",
|
|
@@ -12089,10 +12189,18 @@ const ModeSpecSchema = object({
|
|
|
12089
12189
|
/** Fixed when the comparison is entered; discovery never changes comment storage. */
|
|
12090
12190
|
commentKey: string()
|
|
12091
12191
|
});
|
|
12092
|
-
|
|
12192
|
+
const ModeSpecSchema = ComparisonSchema.extend({
|
|
12193
|
+
/** Set while one commit of a range is focused: the range, whose commits the snapshot keeps listing. */
|
|
12194
|
+
within: ComparisonSchema.optional() });
|
|
12195
|
+
/**
|
|
12196
|
+
* Display comparison endpoints with branch names instead of internal ref namespaces; a single commit as
|
|
12197
|
+
* `<sha>^!`, or as `<range> @ <sha>` when focused within a range.
|
|
12198
|
+
*/
|
|
12093
12199
|
function comparisonLabel(mode) {
|
|
12200
|
+
if (mode.within) return `${comparisonLabel(mode.within)} @ ${mode.new.slice(0, 7)}`;
|
|
12201
|
+
if (mode.base === "parent") return `${mode.new.slice(0, 7)}^!`;
|
|
12094
12202
|
const name = (rev) => rev.replace(/^refs\/diffle\/[^/]+\/\d+\/(?:base|head)\//, "").replace(/^refs\/(?:heads|remotes)\//, "");
|
|
12095
|
-
return `${name(mode.old)}${mode.
|
|
12203
|
+
return `${name(mode.old)}${mode.base === "merge-base" ? "..." : ".."}${name(mode.new)}`;
|
|
12096
12204
|
}
|
|
12097
12205
|
const GithubPullRequestSchema = object({
|
|
12098
12206
|
repository: string(),
|
|
@@ -12135,11 +12243,39 @@ const ChangedFileSchema = object({
|
|
|
12135
12243
|
binary: boolean(),
|
|
12136
12244
|
/** New-side blob sha. '' for deletions. Keys the viewed state. */
|
|
12137
12245
|
blob: string(),
|
|
12246
|
+
/** Old-side blob sha. '' for additions. A worktree side's blob, like `blob`, is '' when the file cannot be hashed. */
|
|
12247
|
+
oldBlob: string(),
|
|
12138
12248
|
/** The `linguist-generated` gitattribute, a path pattern or a content sniff said this file is generated. Feeds auto-collapse and risk ranking. */
|
|
12139
12249
|
generated: boolean(),
|
|
12140
12250
|
/** A gitlink (mode 160000): `blob` is the recorded commit, the patch shows the commit-id change, and there is no file to open. */
|
|
12141
12251
|
submodule: literal(true).optional()
|
|
12142
12252
|
});
|
|
12253
|
+
const CommitInfoSchema = object({
|
|
12254
|
+
sha: string(),
|
|
12255
|
+
short: string(),
|
|
12256
|
+
message: string()
|
|
12257
|
+
});
|
|
12258
|
+
/** A single reviewed commit and its neighbours along HEAD's first-parent history, for stepping through it. */
|
|
12259
|
+
const ReviewedCommitSchema = CommitInfoSchema.extend({
|
|
12260
|
+
/** First parent; null for a root commit. */
|
|
12261
|
+
parent: string().nullable(),
|
|
12262
|
+
/** The commit whose first parent this is, toward HEAD; null at HEAD or off HEAD's first-parent line. */
|
|
12263
|
+
child: string().nullable()
|
|
12264
|
+
});
|
|
12265
|
+
/**
|
|
12266
|
+
* The commits a comparison spans: old..new, with the worktree taken as HEAD, or the one commit under `parent`.
|
|
12267
|
+
* A focused commit lists its range's.
|
|
12268
|
+
*/
|
|
12269
|
+
const RangeCommitsSchema = object({
|
|
12270
|
+
/** Oldest first; the newest MAX_RANGE_COMMITS when there are more. */
|
|
12271
|
+
list: CommitInfoSchema.extend({
|
|
12272
|
+
author: string(),
|
|
12273
|
+
email: string(),
|
|
12274
|
+
/** Author date, epoch milliseconds. */
|
|
12275
|
+
date: number()
|
|
12276
|
+
}).array(),
|
|
12277
|
+
total: number()
|
|
12278
|
+
});
|
|
12143
12279
|
object({
|
|
12144
12280
|
root: string(),
|
|
12145
12281
|
mode: ModeSpecSchema,
|
|
@@ -12154,7 +12290,10 @@ object({
|
|
|
12154
12290
|
context: number(),
|
|
12155
12291
|
changed: ChangedFileSchema.array(),
|
|
12156
12292
|
/** All paths on the new side: the new commit's tree, or index ∪ untracked for the worktree. Sorted. */
|
|
12157
|
-
tree: string().array()
|
|
12293
|
+
tree: string().array(),
|
|
12294
|
+
/** The reviewed commit when the comparison is a single commit against its parent; otherwise null. */
|
|
12295
|
+
commit: ReviewedCommitSchema.nullable(),
|
|
12296
|
+
commits: RangeCommitsSchema
|
|
12158
12297
|
});
|
|
12159
12298
|
/** Body of `POST /api/patch`: the changed files whose patches to return, concatenated. Unknown paths are skipped. */
|
|
12160
12299
|
const PatchRequestSchema = object({ paths: string().array().max(200) });
|
|
@@ -12251,11 +12390,6 @@ const ViewedEntrySchema = object({
|
|
|
12251
12390
|
blob: string(),
|
|
12252
12391
|
viewed: boolean()
|
|
12253
12392
|
});
|
|
12254
|
-
const CommitInfoSchema = object({
|
|
12255
|
-
sha: string(),
|
|
12256
|
-
short: string(),
|
|
12257
|
-
message: string()
|
|
12258
|
-
});
|
|
12259
12393
|
object({
|
|
12260
12394
|
old: CommitInfoSchema.nullable(),
|
|
12261
12395
|
new: CommitInfoSchema.nullable()
|
|
@@ -12498,6 +12632,21 @@ const FileQuerySchema = object({
|
|
|
12498
12632
|
path: string().min(1),
|
|
12499
12633
|
rev: SideSchema
|
|
12500
12634
|
});
|
|
12635
|
+
/**
|
|
12636
|
+
* `GET /api/image`: one side of a file as image bytes. The server answers 404 unless `key` is that side's
|
|
12637
|
+
* `imageKey` in its current snapshot, the bytes are still the ones the key names, and they are a raster image.
|
|
12638
|
+
*/
|
|
12639
|
+
const ImageQuerySchema = FileQuerySchema.extend({ key: string().min(1) });
|
|
12640
|
+
/**
|
|
12641
|
+
* Names the bytes of one side of `path`, or null when the side has no name: a changed file's blob on that
|
|
12642
|
+
* side, else the commit of a side that is one, since an unchanged path is alike on both. What a key names
|
|
12643
|
+
* never changes, so an image served under it may be cached for good.
|
|
12644
|
+
*/
|
|
12645
|
+
function imageKey(snap, path, side) {
|
|
12646
|
+
const file = snap.changed.find((f) => f.path === path);
|
|
12647
|
+
if (file) return (side === "new" ? file.blob : file.oldBlob) || null;
|
|
12648
|
+
return [snap.newSha, snap.oldSha].find((rev) => rev !== "worktree") ?? null;
|
|
12649
|
+
}
|
|
12501
12650
|
const offsetSchema = string().regex(/^[0-9]+$/).transform(Number).pipe(number().int().nonnegative());
|
|
12502
12651
|
const LastCommitsQuerySchema = object({
|
|
12503
12652
|
oldOffset: offsetSchema,
|
|
@@ -14001,6 +14150,7 @@ const WORKTREE = "worktree";
|
|
|
14001
14150
|
* "a..b" → a vs b
|
|
14002
14151
|
* "a...b" → merge-base(a, b) vs b
|
|
14003
14152
|
* "a" "b" → a vs b
|
|
14153
|
+
* "a^!" → a vs its first parent: the one commit, as git show shows it
|
|
14004
14154
|
* Empty sides default to HEAD. Either side accepts "worktree" for the uncommitted
|
|
14005
14155
|
* tree; a merge base against it is taken against HEAD, the commit it sits on.
|
|
14006
14156
|
*/
|
|
@@ -14009,34 +14159,45 @@ function parseRevspec(args) {
|
|
|
14009
14159
|
if (args.length === 2) {
|
|
14010
14160
|
const [a, b] = args;
|
|
14011
14161
|
if (a.includes("..") || b.includes("..")) throw new RevspecError("cannot combine \"..\" with two revisions");
|
|
14162
|
+
if (a.endsWith("^!") || b.endsWith("^!")) throw new RevspecError("cannot combine \"^!\" with two revisions");
|
|
14012
14163
|
return {
|
|
14013
14164
|
old: a,
|
|
14014
14165
|
new: b,
|
|
14015
|
-
|
|
14166
|
+
base: "direct"
|
|
14016
14167
|
};
|
|
14017
14168
|
}
|
|
14018
14169
|
const arg = args[0];
|
|
14170
|
+
if (arg.endsWith("^!")) {
|
|
14171
|
+
const commit = arg.slice(0, -2) || "HEAD";
|
|
14172
|
+
if (commit.includes("..")) throw new RevspecError("cannot combine \"^!\" with \"..\"");
|
|
14173
|
+
if (commit === WORKTREE) throw new RevspecError("the worktree is not a commit");
|
|
14174
|
+
return {
|
|
14175
|
+
old: commit,
|
|
14176
|
+
new: commit,
|
|
14177
|
+
base: "parent"
|
|
14178
|
+
};
|
|
14179
|
+
}
|
|
14019
14180
|
const three = arg.indexOf("...");
|
|
14020
14181
|
if (three !== -1) return {
|
|
14021
14182
|
old: arg.slice(0, three) || "HEAD",
|
|
14022
14183
|
new: arg.slice(three + 3) || "HEAD",
|
|
14023
|
-
|
|
14184
|
+
base: "merge-base"
|
|
14024
14185
|
};
|
|
14025
14186
|
const two = arg.indexOf("..");
|
|
14026
14187
|
if (two !== -1) return {
|
|
14027
14188
|
old: arg.slice(0, two) || "HEAD",
|
|
14028
14189
|
new: arg.slice(two + 2) || "HEAD",
|
|
14029
|
-
|
|
14190
|
+
base: "direct"
|
|
14030
14191
|
};
|
|
14031
14192
|
if (arg === WORKTREE) return {
|
|
14032
14193
|
old: "HEAD",
|
|
14033
14194
|
new: WORKTREE,
|
|
14034
|
-
|
|
14195
|
+
base: "direct"
|
|
14035
14196
|
};
|
|
14036
14197
|
return {
|
|
14037
14198
|
old: arg,
|
|
14038
14199
|
new: "HEAD",
|
|
14039
|
-
|
|
14200
|
+
base: "merge-base"
|
|
14040
14201
|
};
|
|
14041
14202
|
}
|
|
14042
14203
|
var RevspecError = class extends Error {};
|
|
@@ -16766,7 +16927,7 @@ const LOOPBACK = /* @__PURE__ */ new Set([
|
|
|
16766
16927
|
"::1",
|
|
16767
16928
|
"localhost"
|
|
16768
16929
|
]);
|
|
16769
|
-
const
|
|
16930
|
+
const UNCONFIGURED = "if a reverse proxy sent it, trust that origin with --allowed-origin";
|
|
16770
16931
|
/**
|
|
16771
16932
|
* Restricts API and WebSocket requests to local hosts and the configured public origin.
|
|
16772
16933
|
* Proxies may preserve the public Host or rewrite it to an allowed upstream Host.
|
|
@@ -16780,15 +16941,16 @@ function requestGuard(bindHost, names = ownNames(), allowedOrigin) {
|
|
|
16780
16941
|
...names.map((n) => n.toLowerCase())
|
|
16781
16942
|
]);
|
|
16782
16943
|
const publicHost = allowedOrigin == null ? void 0 : new URL(allowedOrigin).host;
|
|
16944
|
+
const hint = allowedOrigin == null ? UNCONFIGURED : `allowed origin is "${allowedOrigin}"`;
|
|
16783
16945
|
return ({ host, origin }) => {
|
|
16784
16946
|
if (!host) return "missing Host header";
|
|
16785
16947
|
const name = hostnameOf(host).toLowerCase();
|
|
16786
16948
|
const localHost = loopback ? LOOPBACK.has(name) : isIP(name) !== 0 || allowed.has(name);
|
|
16787
16949
|
const proxyHost = publicHost != null && host.toLowerCase() === publicHost;
|
|
16788
|
-
if (!localHost && !proxyHost) return `forbidden Host "${host}"; ${
|
|
16950
|
+
if (!localHost && !proxyHost) return `forbidden Host "${host}"; ${hint}`;
|
|
16789
16951
|
if (origin == null || allowedOrigin != null && origin === allowedOrigin) return null;
|
|
16790
16952
|
if (!proxyHost && parseHost(origin) === host) return null;
|
|
16791
|
-
return `forbidden Origin "${origin}" for Host "${host}"; ${
|
|
16953
|
+
return `forbidden Origin "${origin}" for Host "${host}"; ${hint}`;
|
|
16792
16954
|
};
|
|
16793
16955
|
}
|
|
16794
16956
|
/** The host of a syntactically valid origin; undefined for `null` and other non-URLs. */
|
|
@@ -17285,6 +17447,67 @@ function migrateV1(v1) {
|
|
|
17285
17447
|
};
|
|
17286
17448
|
}
|
|
17287
17449
|
//#endregion
|
|
17450
|
+
//#region src/server/image.ts
|
|
17451
|
+
/** Leading bytes of the raster formats browsers display; `null` matches any byte. */
|
|
17452
|
+
const SIGNATURES = [
|
|
17453
|
+
["image/png", [
|
|
17454
|
+
137,
|
|
17455
|
+
80,
|
|
17456
|
+
78,
|
|
17457
|
+
71,
|
|
17458
|
+
13,
|
|
17459
|
+
10,
|
|
17460
|
+
26,
|
|
17461
|
+
10
|
|
17462
|
+
]],
|
|
17463
|
+
["image/jpeg", [
|
|
17464
|
+
255,
|
|
17465
|
+
216,
|
|
17466
|
+
255
|
|
17467
|
+
]],
|
|
17468
|
+
["image/gif", ascii("GIF8")],
|
|
17469
|
+
["image/webp", [
|
|
17470
|
+
...ascii("RIFF"),
|
|
17471
|
+
null,
|
|
17472
|
+
null,
|
|
17473
|
+
null,
|
|
17474
|
+
null,
|
|
17475
|
+
...ascii("WEBP")
|
|
17476
|
+
]],
|
|
17477
|
+
["image/avif", [
|
|
17478
|
+
null,
|
|
17479
|
+
null,
|
|
17480
|
+
null,
|
|
17481
|
+
null,
|
|
17482
|
+
...ascii("ftypavif")
|
|
17483
|
+
]],
|
|
17484
|
+
["image/avif", [
|
|
17485
|
+
null,
|
|
17486
|
+
null,
|
|
17487
|
+
null,
|
|
17488
|
+
null,
|
|
17489
|
+
...ascii("ftypavis")
|
|
17490
|
+
]],
|
|
17491
|
+
["image/bmp", ascii("BM")],
|
|
17492
|
+
["image/x-icon", [
|
|
17493
|
+
0,
|
|
17494
|
+
0,
|
|
17495
|
+
1,
|
|
17496
|
+
0
|
|
17497
|
+
]]
|
|
17498
|
+
];
|
|
17499
|
+
function ascii(s) {
|
|
17500
|
+
return [...s].map((ch) => ch.charCodeAt(0));
|
|
17501
|
+
}
|
|
17502
|
+
/**
|
|
17503
|
+
* The media type of a raster image, read from its leading bytes; null for anything else. The
|
|
17504
|
+
* extension is not consulted: only bytes that are an image get served as one.
|
|
17505
|
+
*/
|
|
17506
|
+
function imageType(buf) {
|
|
17507
|
+
for (const [type, magic] of SIGNATURES) if (buf.length >= magic.length && magic.every((b, i) => b == null || buf[i] === b)) return type;
|
|
17508
|
+
return null;
|
|
17509
|
+
}
|
|
17510
|
+
//#endregion
|
|
17288
17511
|
//#region src/server/routes.ts
|
|
17289
17512
|
/** Invalid JSON is a request error; failures after parsing still propagate. */
|
|
17290
17513
|
async function readJson(c) {
|
|
@@ -17350,6 +17573,18 @@ function createApi(deps) {
|
|
|
17350
17573
|
};
|
|
17351
17574
|
return c.json(body);
|
|
17352
17575
|
});
|
|
17576
|
+
app.get("/api/image", async (c) => {
|
|
17577
|
+
const { path, rev, key } = ImageQuerySchema.parse(c.req.query());
|
|
17578
|
+
const buf = await session.readImage(await session.snapshotter.current(), path, rev, key);
|
|
17579
|
+
const type = buf && imageType(buf);
|
|
17580
|
+
if (!type) return c.json({ error: "no image under that key" }, 404);
|
|
17581
|
+
return c.body(new Uint8Array(buf), 200, {
|
|
17582
|
+
"content-type": type,
|
|
17583
|
+
"cache-control": "private, max-age=31536000, immutable",
|
|
17584
|
+
"x-content-type-options": "nosniff",
|
|
17585
|
+
"cross-origin-resource-policy": "same-origin"
|
|
17586
|
+
});
|
|
17587
|
+
});
|
|
17353
17588
|
app.get("/api/search", async (c) => {
|
|
17354
17589
|
const query = SearchQuerySchema.parse(c.req.query());
|
|
17355
17590
|
const { q, scope } = query;
|
|
@@ -17588,7 +17823,8 @@ var Server = class {
|
|
|
17588
17823
|
app.use("/api/*", compress());
|
|
17589
17824
|
app.route("/", createApi(this.deps));
|
|
17590
17825
|
app.all("/api/*", (c) => c.json({ error: "API endpoint not found. Restart diffle to load the latest server." }, 404));
|
|
17591
|
-
if (!this.opts.dev)
|
|
17826
|
+
if (!this.opts.dev && isSea()) app.get("*", serveEmbedded);
|
|
17827
|
+
else if (!this.opts.dev) {
|
|
17592
17828
|
const clientDir = resolveClientDir();
|
|
17593
17829
|
app.use("*", serveStatic({ root: clientDir }));
|
|
17594
17830
|
app.get("*", serveStatic({
|
|
@@ -17675,7 +17911,14 @@ function resolveClientDir() {
|
|
|
17675
17911
|
return join(projectRoot(), "dist", "client");
|
|
17676
17912
|
}
|
|
17677
17913
|
function hasClientBuild() {
|
|
17678
|
-
return existsSync(join(resolveClientDir(), "index.html"));
|
|
17914
|
+
return isSea() || existsSync(join(resolveClientDir(), "index.html"));
|
|
17915
|
+
}
|
|
17916
|
+
const embedded = isSea() ? new Set(getAssetKeys()) : /* @__PURE__ */ new Set();
|
|
17917
|
+
function serveEmbedded(c) {
|
|
17918
|
+
const path = c.req.path.slice(1);
|
|
17919
|
+
const key = embedded.has(path) ? path : "index.html";
|
|
17920
|
+
const type = getMimeType(key) ?? "application/octet-stream";
|
|
17921
|
+
return c.body(new Uint8Array(getRawAsset(key)), 200, { "content-type": type });
|
|
17679
17922
|
}
|
|
17680
17923
|
//#endregion
|
|
17681
17924
|
//#region src/server/github/pulls.ts
|
|
@@ -18190,17 +18433,19 @@ function describe(skipped) {
|
|
|
18190
18433
|
* A--B--C main
|
|
18191
18434
|
* \--D--E HEAD
|
|
18192
18435
|
*
|
|
18193
|
-
* old new
|
|
18194
|
-
* main HEAD
|
|
18195
|
-
* main HEAD
|
|
18196
|
-
* HEAD worktree
|
|
18197
|
-
* worktree HEAD
|
|
18198
|
-
* main worktree
|
|
18199
|
-
* <C hash> <E hash>
|
|
18436
|
+
* old new base oldSha newSha live
|
|
18437
|
+
* main HEAD direct C E refs
|
|
18438
|
+
* main HEAD merge-base B E refs
|
|
18439
|
+
* HEAD worktree direct E worktree worktree
|
|
18440
|
+
* worktree HEAD direct worktree E worktree
|
|
18441
|
+
* main worktree merge-base B worktree worktree
|
|
18442
|
+
* <C hash> <E hash> direct C E none
|
|
18443
|
+
* <E hash> <E hash> parent D E none
|
|
18200
18444
|
*
|
|
18201
|
-
*
|
|
18202
|
-
* HEAD for any worktree input
|
|
18203
|
-
*
|
|
18445
|
+
* merge-base replaces only the old endpoint with the common ancestor, using
|
|
18446
|
+
* HEAD for any worktree input; parent replaces it with new's first parent, or
|
|
18447
|
+
* the empty tree for a root commit. live is "worktree" if either input is
|
|
18448
|
+
* worktree, "none" if both inputs are pinned hashes, and "refs" otherwise.
|
|
18204
18449
|
*
|
|
18205
18450
|
* In an unborn repository, HEAD compared directly with worktree resolves to
|
|
18206
18451
|
* the empty tree. Invalid revisions or missing merge bases throw RevspecError.
|
|
@@ -18212,14 +18457,14 @@ async function resolveComparison(repo, comparison) {
|
|
|
18212
18457
|
try {
|
|
18213
18458
|
return await repo.resolve(rev);
|
|
18214
18459
|
} catch (e) {
|
|
18215
|
-
if (
|
|
18460
|
+
if (comparison.base === "direct" && rev === "HEAD" && endpoints.includes("worktree") && e instanceof GitError && e.code === 1) return repo.emptyTree();
|
|
18216
18461
|
if (e instanceof GitError) throw new RevspecError(`unknown revision: ${rev}`);
|
|
18217
18462
|
throw e;
|
|
18218
18463
|
}
|
|
18219
18464
|
};
|
|
18220
18465
|
const [old, next] = await Promise.all([resolve(comparison.old), resolve(comparison.new)]);
|
|
18221
18466
|
let oldSha = old;
|
|
18222
|
-
if (comparison.
|
|
18467
|
+
if (comparison.base === "merge-base") {
|
|
18223
18468
|
const [a, b] = await Promise.all([old === "worktree" ? resolve("HEAD") : old, next === "worktree" ? resolve("HEAD") : next]);
|
|
18224
18469
|
try {
|
|
18225
18470
|
oldSha = await repo.mergeBase(a, b);
|
|
@@ -18228,6 +18473,13 @@ async function resolveComparison(repo, comparison) {
|
|
|
18228
18473
|
throw e;
|
|
18229
18474
|
}
|
|
18230
18475
|
}
|
|
18476
|
+
if (comparison.base === "parent") {
|
|
18477
|
+
if (next === "worktree") throw new RevspecError("the worktree is not a commit");
|
|
18478
|
+
oldSha = await repo.resolve(`${next}^`).catch((e) => {
|
|
18479
|
+
if (e instanceof GitError && e.code === 1) return repo.emptyTree();
|
|
18480
|
+
throw e;
|
|
18481
|
+
});
|
|
18482
|
+
}
|
|
18231
18483
|
const pinned = (rev, sha) => rev.length >= 7 && sha.startsWith(rev.toLowerCase());
|
|
18232
18484
|
return {
|
|
18233
18485
|
oldSha,
|
|
@@ -18241,8 +18493,18 @@ async function resolveReview(req, repo, github) {
|
|
|
18241
18493
|
const parsed = req.kind === "working" ? {
|
|
18242
18494
|
old: "HEAD",
|
|
18243
18495
|
new: "worktree",
|
|
18244
|
-
|
|
18496
|
+
base: "direct"
|
|
18245
18497
|
} : parseRevspec(req.args);
|
|
18498
|
+
if (parsed.base === "parent") {
|
|
18499
|
+
const { newSha: sha } = await resolveComparison(repo, parsed);
|
|
18500
|
+
return { mode: {
|
|
18501
|
+
old: sha,
|
|
18502
|
+
new: sha,
|
|
18503
|
+
base: "parent",
|
|
18504
|
+
live: "none",
|
|
18505
|
+
commentKey: `commit:${sha}`
|
|
18506
|
+
} };
|
|
18507
|
+
}
|
|
18246
18508
|
const { oldSha, newSha, live } = await resolveComparison(repo, parsed);
|
|
18247
18509
|
return { mode: {
|
|
18248
18510
|
...parsed,
|
|
@@ -18252,19 +18514,21 @@ async function resolveReview(req, repo, github) {
|
|
|
18252
18514
|
}
|
|
18253
18515
|
/**
|
|
18254
18516
|
* Branch identities keep review state stable across pushes and local branch aliases.
|
|
18517
|
+
* Keys read like the comparison, with ... for a merge base and .. otherwise.
|
|
18255
18518
|
* For origin = github.com/o/r and HEAD on feat tracking origin/feat:
|
|
18256
|
-
* origin/main...HEAD →
|
|
18257
|
-
* origin/main..HEAD →
|
|
18519
|
+
* origin/main...HEAD → range:o/r:main...o/r:feat
|
|
18520
|
+
* origin/main..HEAD → range:o/r:main..o/r:feat
|
|
18258
18521
|
* The matching explicit PR uses the first key too. If feat tracks a fork,
|
|
18259
18522
|
* its identity becomes e.g. "contributor/r:feat", keeping forks distinct.
|
|
18260
18523
|
*
|
|
18261
18524
|
* Without a GitHub upstream, branches use full refs:
|
|
18262
|
-
* main...feat →
|
|
18525
|
+
* main...feat → range:refs/heads/main...refs/heads/feat
|
|
18263
18526
|
* Non-branch endpoints use their resolved hash or literal "worktree":
|
|
18264
|
-
* main..worktree →
|
|
18265
|
-
*
|
|
18527
|
+
* main..worktree → range:refs/heads/main..worktree
|
|
18528
|
+
* <C>..<E> → range:<C hash>..<E hash>
|
|
18266
18529
|
* Hash placeholders denote full resolved hashes, with oldSha already adjusted
|
|
18267
|
-
* for
|
|
18530
|
+
* for the merge base. The working command bypasses this helper and uses
|
|
18531
|
+
* "working"; a single commit uses "commit:<hash>".
|
|
18268
18532
|
*/
|
|
18269
18533
|
async function commentKey(repo, comparison, oldSha, newSha) {
|
|
18270
18534
|
const remotes = await repo.remotes();
|
|
@@ -18278,14 +18542,10 @@ async function commentKey(repo, comparison, oldSha, newSha) {
|
|
|
18278
18542
|
return repo.branchRef(rev);
|
|
18279
18543
|
};
|
|
18280
18544
|
const [old, next] = await Promise.all([identity(comparison.old), identity(comparison.new)]);
|
|
18281
|
-
return
|
|
18545
|
+
return rangeKey(old ?? oldSha, next ?? newSha, comparison.base === "merge-base");
|
|
18282
18546
|
}
|
|
18283
|
-
function
|
|
18284
|
-
return `
|
|
18285
|
-
old,
|
|
18286
|
-
next,
|
|
18287
|
-
mergeBase
|
|
18288
|
-
])}`;
|
|
18547
|
+
function rangeKey(old, next, mergeBase) {
|
|
18548
|
+
return `range:${old}${mergeBase ? "..." : ".."}${next}`;
|
|
18289
18549
|
}
|
|
18290
18550
|
/**
|
|
18291
18551
|
* A GitHub pull request, as GitHub shows it: merge-base(base tip, head) vs head.
|
|
@@ -18304,16 +18564,16 @@ async function resolvePr(req, repo, github) {
|
|
|
18304
18564
|
await resolveComparison(repo, {
|
|
18305
18565
|
old: base,
|
|
18306
18566
|
new: head,
|
|
18307
|
-
|
|
18567
|
+
base: "merge-base"
|
|
18308
18568
|
});
|
|
18309
18569
|
return {
|
|
18310
18570
|
prUrl: pr.url,
|
|
18311
18571
|
mode: {
|
|
18312
18572
|
old: base,
|
|
18313
18573
|
new: head,
|
|
18314
|
-
|
|
18574
|
+
base: "merge-base",
|
|
18315
18575
|
live: "none",
|
|
18316
|
-
commentKey:
|
|
18576
|
+
commentKey: rangeKey(`${pr.repository.toLowerCase()}:${pr.baseRefName}`, `${pr.headRepository?.toLowerCase() ?? `deleted:${pr.url}`}:${pr.headRefName}`, true)
|
|
18317
18577
|
}
|
|
18318
18578
|
};
|
|
18319
18579
|
}
|
|
@@ -18462,7 +18722,12 @@ var Snapshotter = class {
|
|
|
18462
18722
|
}
|
|
18463
18723
|
async compute(version) {
|
|
18464
18724
|
const [{ oldSha, newSha }, headSha] = await Promise.all([resolveComparison(this.repo, this.mode), this.repo.resolve("HEAD").catch(() => "")]);
|
|
18465
|
-
const [tracked, changed] = await Promise.all([
|
|
18725
|
+
const [tracked, changed, commit, commits] = await Promise.all([
|
|
18726
|
+
newSha === "worktree" ? Promise.all([this.repo.lsFiles(), this.repo.untracked()]).then((lists) => lists.flat().filter((p) => !p.endsWith("/"))) : this.repo.lsTree(newSha),
|
|
18727
|
+
this.repo.numstat(oldSha, newSha),
|
|
18728
|
+
this.mode.base === "parent" ? this.reviewedCommit(newSha) : null,
|
|
18729
|
+
this.rangeCommits(oldSha, newSha, headSha)
|
|
18730
|
+
]);
|
|
18466
18731
|
await this.fillGenerated(changed, newSha);
|
|
18467
18732
|
const tree = new Set(tracked);
|
|
18468
18733
|
for (const f of changed) {
|
|
@@ -18479,11 +18744,36 @@ var Snapshotter = class {
|
|
|
18479
18744
|
headSha,
|
|
18480
18745
|
context: this.context,
|
|
18481
18746
|
changed,
|
|
18482
|
-
tree: [...tree].sort()
|
|
18747
|
+
tree: [...tree].sort(),
|
|
18748
|
+
commit,
|
|
18749
|
+
commits
|
|
18483
18750
|
};
|
|
18484
18751
|
for (const f of changed.slice(0, PREWARM)) this.patchFor(snap, f).catch(() => {});
|
|
18485
18752
|
return snap;
|
|
18486
18753
|
}
|
|
18754
|
+
async reviewedCommit(sha) {
|
|
18755
|
+
const [info, neighbours] = await Promise.all([this.repo.commitInfo(sha), this.repo.commitNeighbours(sha)]);
|
|
18756
|
+
return info && {
|
|
18757
|
+
...info,
|
|
18758
|
+
...neighbours
|
|
18759
|
+
};
|
|
18760
|
+
}
|
|
18761
|
+
async rangeCommits(oldSha, newSha, headSha) {
|
|
18762
|
+
if (this.mode.within) {
|
|
18763
|
+
const range = await resolveComparison(this.repo, this.mode.within);
|
|
18764
|
+
return this.spanned(range.oldSha, range.newSha, headSha);
|
|
18765
|
+
}
|
|
18766
|
+
if (this.mode.base === "parent") return this.repo.rangeCommits(`${newSha}^!`, 250);
|
|
18767
|
+
return this.spanned(oldSha, newSha, headSha);
|
|
18768
|
+
}
|
|
18769
|
+
spanned(oldSha, newSha, headSha) {
|
|
18770
|
+
const [from, to] = [oldSha, newSha].map((sha) => sha === "worktree" ? headSha : sha);
|
|
18771
|
+
if (!from || !to || from === to) return Promise.resolve({
|
|
18772
|
+
list: [],
|
|
18773
|
+
total: 0
|
|
18774
|
+
});
|
|
18775
|
+
return this.repo.rangeCommits(`${from}..${to}`, 250);
|
|
18776
|
+
}
|
|
18487
18777
|
/**
|
|
18488
18778
|
* Sets `generated` on each non-binary changed file. A `linguist-generated`
|
|
18489
18779
|
* gitattribute on the new side decides outright, as on GitHub; otherwise a
|
|
@@ -18634,10 +18924,17 @@ var Session = class {
|
|
|
18634
18924
|
console.error("[diffle] snapshot listener failed:", e);
|
|
18635
18925
|
}
|
|
18636
18926
|
}
|
|
18637
|
-
/**
|
|
18638
|
-
|
|
18927
|
+
/**
|
|
18928
|
+
* Resolves a request into a review without activating it, so a caller can reject
|
|
18929
|
+
* invalid input before serving. Queued, so `close()` waits for a PR fetch.
|
|
18930
|
+
*/
|
|
18931
|
+
resolve(req) {
|
|
18932
|
+
return this.run(() => resolveReview(req, this.repo, this.opts.github));
|
|
18933
|
+
}
|
|
18934
|
+
/** First mode, from `resolve`. Rejects `ready()` on failure so early requests error out. */
|
|
18935
|
+
async start(review) {
|
|
18639
18936
|
try {
|
|
18640
|
-
const snap = await this.
|
|
18937
|
+
const snap = await this.run(() => this.activate(review));
|
|
18641
18938
|
this.resolveReady();
|
|
18642
18939
|
return snap;
|
|
18643
18940
|
} catch (e) {
|
|
@@ -18647,7 +18944,7 @@ var Session = class {
|
|
|
18647
18944
|
}
|
|
18648
18945
|
/** Switch modes at runtime. Broadcasts a snapshot bump on success. */
|
|
18649
18946
|
async switchMode(req) {
|
|
18650
|
-
const snap = await this.
|
|
18947
|
+
const snap = await this.run(async () => this.activate(req.kind === "focus" ? await this.focus(req.commit) : await resolveReview(req, this.repo, this.opts.github)));
|
|
18651
18948
|
this.hub.broadcast({
|
|
18652
18949
|
type: "snapshot",
|
|
18653
18950
|
version: snap.version
|
|
@@ -18655,6 +18952,33 @@ var Session = class {
|
|
|
18655
18952
|
return snap;
|
|
18656
18953
|
}
|
|
18657
18954
|
/**
|
|
18955
|
+
* Focuses `commit`, by hash or short hash, of the active range's listed commits, or with null returns to
|
|
18956
|
+
* the range. A focused commit keeps its own comments, as when entered alone, and the range's PR identity.
|
|
18957
|
+
*/
|
|
18958
|
+
async focus(commit) {
|
|
18959
|
+
const a = this.require();
|
|
18960
|
+
const { within, ...self } = a.mode;
|
|
18961
|
+
if (!within && self.base === "parent") throw new RevspecError("a single commit has no commits to focus");
|
|
18962
|
+
const range = within ?? self;
|
|
18963
|
+
if (commit === null) return {
|
|
18964
|
+
mode: range,
|
|
18965
|
+
prUrl: a.prUrl
|
|
18966
|
+
};
|
|
18967
|
+
const sha = (await a.snapshotter.current()).commits.list.find((c) => c.sha === commit || c.short === commit)?.sha;
|
|
18968
|
+
if (!sha) throw new RevspecError(`not a listed commit of ${comparisonLabel(range)}: ${commit}`);
|
|
18969
|
+
return {
|
|
18970
|
+
prUrl: a.prUrl,
|
|
18971
|
+
mode: {
|
|
18972
|
+
old: sha,
|
|
18973
|
+
new: sha,
|
|
18974
|
+
base: "parent",
|
|
18975
|
+
live: range.live === "none" ? "none" : "refs",
|
|
18976
|
+
commentKey: `commit:${sha}`,
|
|
18977
|
+
within: range
|
|
18978
|
+
}
|
|
18979
|
+
};
|
|
18980
|
+
}
|
|
18981
|
+
/**
|
|
18658
18982
|
* Runs `fn` after every earlier mutation settled. The only way to touch
|
|
18659
18983
|
* `active`, `version` or `opts.context`.
|
|
18660
18984
|
*/
|
|
@@ -18663,11 +18987,7 @@ var Session = class {
|
|
|
18663
18987
|
this.queue = r.catch(() => {});
|
|
18664
18988
|
return r;
|
|
18665
18989
|
}
|
|
18666
|
-
|
|
18667
|
-
return this.run(() => this.transition(req));
|
|
18668
|
-
}
|
|
18669
|
-
async transition(req) {
|
|
18670
|
-
const { mode, prUrl } = await resolveReview(req, this.repo, this.opts.github);
|
|
18990
|
+
async activate({ mode, prUrl }) {
|
|
18671
18991
|
const snapshotter = new Snapshotter(this.repo, mode, ++this.version, this.opts.context);
|
|
18672
18992
|
const [comments, snap] = await Promise.all([CommentStore.open(this.repo.gitDir, mode.commentKey), snapshotter.current()]);
|
|
18673
18993
|
const next = {
|
|
@@ -18760,6 +19080,18 @@ var Session = class {
|
|
|
18760
19080
|
if (side === "new") return snap.newSha === "worktree" ? this.repo.readWorktree(target) : this.repo.show(snap.newSha, target);
|
|
18761
19081
|
return snap.oldSha === "worktree" ? this.repo.readWorktree(target) : this.repo.show(snap.oldSha, target);
|
|
18762
19082
|
}
|
|
19083
|
+
/**
|
|
19084
|
+
* The bytes `key` names on one side of `path`, or null unless `key` is that side's `imageKey` in `snap`.
|
|
19085
|
+
* A worktree file that changed since `snap` reads as null too, so a key never serves other bytes.
|
|
19086
|
+
*/
|
|
19087
|
+
async readImage(snap, path, side, key) {
|
|
19088
|
+
if (key !== imageKey(snap, path, side)) return null;
|
|
19089
|
+
if (!snap.changed.some((f) => f.path === path)) return this.readSide(snap, path, key === snap.newSha ? "new" : "old");
|
|
19090
|
+
const buf = await this.readSide(snap, path, side);
|
|
19091
|
+
if (buf == null || (side === "new" ? snap.newSha : snap.oldSha) !== "worktree") return buf;
|
|
19092
|
+
const target = sidePath(this.readablePaths(snap), path, side);
|
|
19093
|
+
return target != null && await this.repo.hashObject(buf, target) === key ? buf : null;
|
|
19094
|
+
}
|
|
18763
19095
|
/** Whether `readSide` would find `path` on `side`: the allowlist alone, no read. */
|
|
18764
19096
|
hasSide(snap, path, side) {
|
|
18765
19097
|
return sidePath(this.readablePaths(snap), path, side) != null;
|
|
@@ -22763,6 +23095,7 @@ Shorthands (in place of <revs>):
|
|
|
22763
23095
|
working same as HEAD..worktree: uncommitted changes, staged and untracked included
|
|
22764
23096
|
pr [number|url] a GitHub pull request: its base...head, fetched if needed
|
|
22765
23097
|
(without an argument: the pull request for this branch)
|
|
23098
|
+
show [rev] same as <rev>^!: one commit against its first parent, pinned (default: HEAD)
|
|
22766
23099
|
|
|
22767
23100
|
Revisions follow git diff, except that a lone one compares from the merge base:
|
|
22768
23101
|
diffle main same as main...HEAD: what this branch added since it left main
|
|
@@ -22770,6 +23103,7 @@ Revisions follow git diff, except that a lone one compares from the merge base:
|
|
|
22770
23103
|
diffle main..feat main vs feat
|
|
22771
23104
|
diffle main...feat what feat added since it left main
|
|
22772
23105
|
diffle main..worktree main vs the uncommitted tree ("worktree" works on either side)
|
|
23106
|
+
diffle a1b2c3d^! the commit a1b2c3d alone, like git show
|
|
22773
23107
|
|
|
22774
23108
|
Without revisions, diffle compares HEAD..worktree.
|
|
22775
23109
|
|
|
@@ -22782,6 +23116,10 @@ Ctrl+C always stops it.`).action(async (revs, _o, cmd) => {
|
|
|
22782
23116
|
}, cmd.optsWithGlobals());
|
|
22783
23117
|
});
|
|
22784
23118
|
program.command("working", { hidden: true }).summary("same as HEAD..worktree").description("Same as `diffle HEAD..worktree`: uncommitted changes, staged and untracked files included.").action(async (_o, cmd) => run({ kind: "working" }, cmd.optsWithGlobals()));
|
|
23119
|
+
program.command("show", { hidden: true }).argument("[rev]", "the commit to review", "HEAD").summary("one commit").description("Same as `diffle <rev>^!`: one commit against its first parent, as git show shows it.").action(async (rev, _o, cmd) => run({
|
|
23120
|
+
kind: "revspec",
|
|
23121
|
+
args: [`${rev}^!`]
|
|
23122
|
+
}, cmd.optsWithGlobals()));
|
|
22785
23123
|
program.command("pr", { hidden: true }).argument("[pr]", "pull request number or url; default: the pull request for this branch").summary("a GitHub pull request").description("A GitHub pull request, as GitHub shows it: merge-base(base, head) vs head.").action(async (pr, _o, cmd) => run({
|
|
22786
23124
|
kind: "pr",
|
|
22787
23125
|
pr
|
|
@@ -22917,6 +23255,9 @@ async function serve(req, opts, repo, closeRepo, config, github, timing) {
|
|
|
22917
23255
|
process.on("SIGTERM", shutdown);
|
|
22918
23256
|
stopBrowserWatch = watchBrowserLifetime(hub, opts.open && !opts.keepAlive, shutdown);
|
|
22919
23257
|
try {
|
|
23258
|
+
const review = await session.resolve(req);
|
|
23259
|
+
timing.mark("resolve");
|
|
23260
|
+
if (closing) return;
|
|
22920
23261
|
const url = await server.listen();
|
|
22921
23262
|
timing.mark("listen");
|
|
22922
23263
|
if (opts.open) {
|
|
@@ -22928,7 +23269,7 @@ async function serve(req, opts, repo, closeRepo, config, github, timing) {
|
|
|
22928
23269
|
console.error(`📂 ${c.dim("repo")} ${repo.root}`);
|
|
22929
23270
|
if (github.client) console.error(`🐙 ${c.dim("github")} ${c.dim(`token from ${github.source}`)}`);
|
|
22930
23271
|
else console.error(`🐙 ${c.dim("github")} ${c.dim("off: no token in GITHUB_TOKEN, GH_TOKEN or gh auth token")}`);
|
|
22931
|
-
const snap = await session.start(
|
|
23272
|
+
const snap = await session.start(review);
|
|
22932
23273
|
timing.mark("snapshot");
|
|
22933
23274
|
const n = snap.changed.length;
|
|
22934
23275
|
const adds = snap.changed.reduce((a, f) => a + f.additions, 0);
|