@moritzwilksch/diffle 0.1.9 → 0.2.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/README.md +5 -2
- package/dist/client/assets/{angular-html-CJY2n6PT.js → angular-html-ulrXXC4T.js} +1 -1
- package/dist/client/assets/{angular-ts-CZTM8_xo.js → angular-ts-cWUkbqrc.js} +1 -1
- package/dist/client/assets/{apl-Dcp77-Tz.js → apl-CYwKvDlF.js} +1 -1
- package/dist/client/assets/{astro-C0gdsdoF.js → astro-BleB-Dvw.js} +1 -1
- package/dist/client/assets/{blade-BYpI0fWU.js → blade-UxS1gLIJ.js} +1 -1
- package/dist/client/assets/{c-DvFVe1b3.js → c-Dy61fyeR.js} +1 -1
- package/dist/client/assets/{chapel-qAWjGK-V.js → chapel-DA_5Jb29.js} +1 -1
- package/dist/client/assets/{cobol-BeFQKGES.js → cobol-DF_09Fay.js} +1 -1
- package/dist/client/assets/{coffee-XLni1sjM.js → coffee-Cv1DVgPp.js} +1 -1
- package/dist/client/assets/{cpp-ChsJNYBC.js → cpp-BlnMjTh9.js} +1 -1
- package/dist/client/assets/{crystal-D3SbG8yD.js → crystal-B8onZGWh.js} +1 -1
- package/dist/client/assets/{css-VXnGhPbY.js → css-D-x-Ue0G.js} +1 -1
- package/dist/client/assets/{edge-Dbzmpk4W.js → edge-Fqle09cH.js} +1 -1
- package/dist/client/assets/{elixir-DvQULd_0.js → elixir-CL3sU1gy.js} +1 -1
- package/dist/client/assets/{elm-BM5mpULp.js → elm-Blr9Ia2N.js} +1 -1
- package/dist/client/assets/{erb-nYCgf5TS.js → erb-Ck5t-jn2.js} +1 -1
- package/dist/client/assets/{git-rebase-CcDsi2L8.js → git-rebase-Dt2TndDV.js} +1 -1
- package/dist/client/assets/{glimmer-js-94igFmPU.js → glimmer-js-Cwn-aFNH.js} +1 -1
- package/dist/client/assets/{glimmer-ts-8aOBlhVe.js → glimmer-ts-CKev0hwM.js} +1 -1
- package/dist/client/assets/{glsl-DAW_sXZj.js → glsl-ClWhfdlL.js} +1 -1
- package/dist/client/assets/{graphql-BEeRZxhy.js → graphql-C8TyhWXe.js} +1 -1
- package/dist/client/assets/{hack-e4pIPigB.js → hack-C9oIcSTK.js} +1 -1
- package/dist/client/assets/{haml-DvZH4eyc.js → haml-BP7tD858.js} +1 -1
- package/dist/client/assets/{handlebars-Bod10MIb.js → handlebars-D7ZldTVB.js} +1 -1
- package/dist/client/assets/{html-BOKVgXUz.js → html-BxXjaAye.js} +1 -1
- package/dist/client/assets/{html-derivative-B5DftdEs.js → html-derivative-CEoRM3ro.js} +1 -1
- package/dist/client/assets/{http-Bli9WHgJ.js → http-DVWDINjr.js} +1 -1
- package/dist/client/assets/{hurl-C45hn_gr.js → hurl-BjNoiLI6.js} +1 -1
- package/dist/client/assets/index-C-uvIS11.css +2 -0
- package/dist/client/assets/index-DrpXuuVn.js +3005 -0
- package/dist/client/assets/{java-BxezylI4.js → java-Cbx3IDuw.js} +1 -1
- package/dist/client/assets/{javascript-CGUJoh42.js → javascript-C1JF7iBX.js} +1 -1
- package/dist/client/assets/{jinja-Dhrq1TMH.js → jinja-BMe2GcVb.js} +1 -1
- package/dist/client/assets/{jison-CK02w6aE.js → jison-BVbXCft7.js} +1 -1
- package/dist/client/assets/{json-DbjMq68q.js → json-CRqSOZnD.js} +1 -1
- package/dist/client/assets/{jsx-BVYjBFnX.js → jsx-DcDmlRfr.js} +1 -1
- package/dist/client/assets/{julia-DUG0dPfw.js → julia-CPcuPmIV.js} +1 -1
- package/dist/client/assets/{just-BzILq64e.js → just-B1zV9CiR.js} +1 -1
- package/dist/client/assets/{latex-DJ-r-_gD.js → latex-Cuj3EXmX.js} +1 -1
- package/dist/client/assets/{liquid-CUeuBds8.js → liquid-C1T4UF1w.js} +1 -1
- package/dist/client/assets/{lua-CUx56wFu.js → lua-46-qG00m.js} +1 -1
- package/dist/client/assets/{marko-zidFMx4y.js → marko-Cc6TCGIc.js} +1 -1
- package/dist/client/assets/{mdc-B8YDeE-G.js → mdc-qnFt69dC.js} +1 -1
- package/dist/client/assets/{nginx-CLmnmibq.js → nginx-7n4L4W73.js} +1 -1
- package/dist/client/assets/{nim-D_ow61_F.js → nim-BLvMDnWX.js} +1 -1
- package/dist/client/assets/{org-D6yFID6y.js → org-C8fiTt0a.js} +1 -1
- package/dist/client/assets/{perl-DlRNqEum.js → perl-x9VdolYn.js} +1 -1
- package/dist/client/assets/{php-CPe77eH-.js → php-mFFqmEsM.js} +1 -1
- package/dist/client/assets/{pug-CzDq8hOp.js → pug-H1OMV43P.js} +1 -1
- package/dist/client/assets/{qml-CmVqwUZS.js → qml-B8D1BoQx.js} +1 -1
- package/dist/client/assets/{r-CZql-PDg.js → r-DAih_iaQ.js} +1 -1
- package/dist/client/assets/{razor-Dp5yql5G.js → razor-BeJgOpe8.js} +1 -1
- package/dist/client/assets/{regexp-DjyDhvtX.js → regexp-CAppT72h.js} +1 -1
- package/dist/client/assets/{rst-B-Q6Ljb6.js → rst-RMbVAAkg.js} +1 -1
- package/dist/client/assets/{ruby-BQD6FS2h.js → ruby-DLm1ZwEN.js} +1 -1
- package/dist/client/assets/{sas-DDGcym6k.js → sas-B7ihlrSP.js} +1 -1
- package/dist/client/assets/{scss-DNPPx0rx.js → scss-3DF1Is0M.js} +1 -1
- package/dist/client/assets/{shellscript-CHWpWo83.js → shellscript-Bq08mUIc.js} +1 -1
- package/dist/client/assets/{shellsession-_KE7LUMO.js → shellsession-DJY5846a.js} +1 -1
- package/dist/client/assets/{soy-B57xVh7W.js → soy-Bkld_lfE.js} +1 -1
- package/dist/client/assets/{sql-8OXlDHCR.js → sql-B35yhKok.js} +1 -1
- package/dist/client/assets/{stata-C452rT5l.js → stata-BgW5egxh.js} +1 -1
- package/dist/client/assets/{surrealql-B5gNeOUk.js → surrealql-B-aUVDek.js} +1 -1
- package/dist/client/assets/{svelte-Cv_BwjgR.js → svelte-C7MrEpG_.js} +1 -1
- package/dist/client/assets/{templ-qZdOs42N.js → templ-CUYQL3Op.js} +1 -1
- package/dist/client/assets/{tex-BK-MJpDM.js → tex-Dbrd9bB3.js} +1 -1
- package/dist/client/assets/{ts-tags-C5Q-Olrs.js → ts-tags-wpbSYL5U.js} +1 -1
- package/dist/client/assets/{tsx-DwPm0lid.js → tsx-B8rF--RV.js} +1 -1
- package/dist/client/assets/{twig-DKj0IG75.js → twig-CpaRBIem.js} +1 -1
- package/dist/client/assets/{typescript-BeWKsdVP.js → typescript-cqbK9R71.js} +1 -1
- package/dist/client/assets/{typst-B7u-3Y0n.js → typst-BUVX7J-L.js} +1 -1
- package/dist/client/assets/{vue-BkC1F40W.js → vue-B3rM_CiH.js} +1 -1
- package/dist/client/assets/{vue-html-BeYXHyr2.js → vue-html-uBxsJ3nV.js} +1 -1
- package/dist/client/assets/{vue-vine-ymsHpgRC.js → vue-vine-CH44uMD8.js} +1 -1
- package/dist/client/assets/{xml-B6M3xnfe.js → xml-BnoqKVtb.js} +1 -1
- package/dist/client/assets/{xsl-CSlbm6tK.js → xsl-r6Si1Xjc.js} +1 -1
- package/dist/client/assets/{yaml-fMwD58zK.js → yaml-tinlnfn7.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/main.js +931 -55
- package/package.json +1 -1
- package/dist/client/assets/index-BugDKSp3.js +0 -2994
- package/dist/client/assets/index-CvgG7WNQ.css +0 -2
package/dist/server/main.js
CHANGED
|
@@ -3952,7 +3952,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
|
|
|
3952
3952
|
}
|
|
3953
3953
|
//#endregion
|
|
3954
3954
|
//#region package.json
|
|
3955
|
-
var version$1 = "0.1
|
|
3955
|
+
var version$1 = "0.2.1";
|
|
3956
3956
|
//#endregion
|
|
3957
3957
|
//#region src/server/comments/anchor.ts
|
|
3958
3958
|
function splitLines(contents) {
|
|
@@ -4109,6 +4109,12 @@ function patchArgs(oldRev) {
|
|
|
4109
4109
|
}
|
|
4110
4110
|
/** An idle `cat-file --batch` is reaped after this long; the next request respawns one. */
|
|
4111
4111
|
const CAT_FILE_IDLE_MS = 2e3;
|
|
4112
|
+
/**
|
|
4113
|
+
* `git range-diff` pairs two commits while the diff between their patches costs less than this
|
|
4114
|
+
* percentage of a patch's size. Git's default of 60 leaves a reworded three-line commit unpaired,
|
|
4115
|
+
* since the message change alone outweighs it; at 100 the message may change as much as the patch.
|
|
4116
|
+
*/
|
|
4117
|
+
const CREATION_FACTOR = 100;
|
|
4112
4118
|
var GitError = class extends Error {
|
|
4113
4119
|
args;
|
|
4114
4120
|
code;
|
|
@@ -4185,6 +4191,16 @@ var GitRepo = class GitRepo {
|
|
|
4185
4191
|
`${rev}^{commit}`
|
|
4186
4192
|
])).trim();
|
|
4187
4193
|
}
|
|
4194
|
+
/** The tree a commit points at. */
|
|
4195
|
+
async tree(rev) {
|
|
4196
|
+
return (await this.text([
|
|
4197
|
+
"rev-parse",
|
|
4198
|
+
"--verify",
|
|
4199
|
+
"--quiet",
|
|
4200
|
+
"--end-of-options",
|
|
4201
|
+
`${rev}^{tree}`
|
|
4202
|
+
])).trim();
|
|
4203
|
+
}
|
|
4188
4204
|
/** Commit messages for two ancestor offsets, pinned to one HEAD. */
|
|
4189
4205
|
async lastCommitsPreview(oldOffset, newOffset) {
|
|
4190
4206
|
const head = await this.commitInfo("HEAD");
|
|
@@ -4300,6 +4316,89 @@ var GitRepo = class GitRepo {
|
|
|
4300
4316
|
b
|
|
4301
4317
|
])).trim();
|
|
4302
4318
|
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Replays what `head` changed since `base` onto `onto`, in memory: the resulting tree, and the
|
|
4321
|
+
* paths the merge could not settle, which the tree holds with conflict markers. `--write-tree`
|
|
4322
|
+
* needs git 2.38. With `-z`, the tree, each conflicted path and the sections end in NUL.
|
|
4323
|
+
*/
|
|
4324
|
+
async replay(base, onto, head) {
|
|
4325
|
+
const [tree = "", ...rest] = (await this.text([
|
|
4326
|
+
"merge-tree",
|
|
4327
|
+
"--write-tree",
|
|
4328
|
+
"-z",
|
|
4329
|
+
"--name-only",
|
|
4330
|
+
`--merge-base=${base}`,
|
|
4331
|
+
"--end-of-options",
|
|
4332
|
+
onto,
|
|
4333
|
+
head
|
|
4334
|
+
], { okCodes: [1] })).split("\0");
|
|
4335
|
+
const conflicts = [];
|
|
4336
|
+
for (const path of rest) {
|
|
4337
|
+
if (path === "") break;
|
|
4338
|
+
conflicts.push(path);
|
|
4339
|
+
}
|
|
4340
|
+
return {
|
|
4341
|
+
tree,
|
|
4342
|
+
conflicts
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
/**
|
|
4346
|
+
* How `git range-diff` pairs the commits of two ranges, in its output order: abbreviated
|
|
4347
|
+
* hashes, null for the side a commit has no counterpart on, and its marker (`=` identical,
|
|
4348
|
+
* `!` changed, `<` only in the first range, `>` only in the second). Summary only, no patches.
|
|
4349
|
+
* Ranges are given as hashes: range-diff learnt `--end-of-options` only in git 2.44.
|
|
4350
|
+
*/
|
|
4351
|
+
async rangeDiff(oldRange, newRange) {
|
|
4352
|
+
const out = await this.text([
|
|
4353
|
+
"range-diff",
|
|
4354
|
+
"-s",
|
|
4355
|
+
"--no-color",
|
|
4356
|
+
`--creation-factor=${CREATION_FACTOR}`,
|
|
4357
|
+
oldRange,
|
|
4358
|
+
newRange
|
|
4359
|
+
]);
|
|
4360
|
+
const pairs = [];
|
|
4361
|
+
for (const line of out.split("\n")) {
|
|
4362
|
+
const m = /^\s*(?:\d+:\s+([0-9a-f]+)|-:\s+-+)\s+([=!<>])\s+(?:\d+:\s+([0-9a-f]+)|-:\s+-+)\s/.exec(line);
|
|
4363
|
+
if (m) pairs.push({
|
|
4364
|
+
old: m[1] ?? null,
|
|
4365
|
+
new: m[3] ?? null,
|
|
4366
|
+
marker: m[2]
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
return pairs;
|
|
4370
|
+
}
|
|
4371
|
+
/** Each commit's stable patch id, so two commits with the same change under different messages compare equal. */
|
|
4372
|
+
async patchIds(shas) {
|
|
4373
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4374
|
+
if (!shas.length) return ids;
|
|
4375
|
+
const patches = await this.exec([
|
|
4376
|
+
"log",
|
|
4377
|
+
"-p",
|
|
4378
|
+
"--no-walk",
|
|
4379
|
+
"--no-show-signature",
|
|
4380
|
+
"--format=%H",
|
|
4381
|
+
"--end-of-options",
|
|
4382
|
+
...shas
|
|
4383
|
+
]);
|
|
4384
|
+
for (const line of (await this.text(["patch-id", "--stable"], { input: patches })).split("\n")) {
|
|
4385
|
+
const [id, sha] = line.split(" ");
|
|
4386
|
+
if (id && sha) ids.set(sha, id);
|
|
4387
|
+
}
|
|
4388
|
+
return ids;
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* Points refs at objects so they survive `gc` and a force-push, or with null deletes them. Refs
|
|
4392
|
+
* must stay inside `refs/diffle/iterations/`: nothing the user owns moves.
|
|
4393
|
+
*/
|
|
4394
|
+
async pin(refs) {
|
|
4395
|
+
const lines = [];
|
|
4396
|
+
for (const [ref, sha] of Object.entries(refs)) {
|
|
4397
|
+
if (!ref.startsWith("refs/diffle/iterations/")) throw new GitError(`refusing to update ${ref}`, ["update-ref"], null, "");
|
|
4398
|
+
lines.push(sha === null ? `delete ${ref}\n` : `update ${ref} ${sha}\n`);
|
|
4399
|
+
}
|
|
4400
|
+
if (lines.length) await this.exec(["update-ref", "--stdin"], { input: lines.join("") });
|
|
4401
|
+
}
|
|
4303
4402
|
/** origin/HEAD → main → master. */
|
|
4304
4403
|
async defaultBranch() {
|
|
4305
4404
|
try {
|
|
@@ -11890,6 +11989,20 @@ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
|
|
|
11890
11989
|
inst.keyType = def.keyType;
|
|
11891
11990
|
inst.valueType = def.valueType;
|
|
11892
11991
|
});
|
|
11992
|
+
function record(keyType, valueType, params) {
|
|
11993
|
+
if (!valueType || !valueType._zod) return new ZodRecord({
|
|
11994
|
+
type: "record",
|
|
11995
|
+
keyType: string(),
|
|
11996
|
+
valueType: keyType,
|
|
11997
|
+
...normalizeParams(valueType)
|
|
11998
|
+
});
|
|
11999
|
+
return new ZodRecord({
|
|
12000
|
+
type: "record",
|
|
12001
|
+
keyType,
|
|
12002
|
+
valueType,
|
|
12003
|
+
...normalizeParams(params)
|
|
12004
|
+
});
|
|
12005
|
+
}
|
|
11893
12006
|
function partialRecord(keyType, valueType, params) {
|
|
11894
12007
|
return new ZodRecord({
|
|
11895
12008
|
type: "record",
|
|
@@ -12165,9 +12278,35 @@ const ModeRequestSchema = discriminatedUnion("kind", [
|
|
|
12165
12278
|
object({
|
|
12166
12279
|
kind: literal("focus"),
|
|
12167
12280
|
commit: string().nullable()
|
|
12281
|
+
}),
|
|
12282
|
+
object({
|
|
12283
|
+
kind: literal("interdiff"),
|
|
12284
|
+
from: number().int().positive(),
|
|
12285
|
+
to: number().int().positive()
|
|
12286
|
+
}),
|
|
12287
|
+
object({
|
|
12288
|
+
kind: literal("range-diff"),
|
|
12289
|
+
args: string().array()
|
|
12290
|
+
}),
|
|
12291
|
+
object({
|
|
12292
|
+
kind: literal("pair"),
|
|
12293
|
+
commit: string().nullable()
|
|
12168
12294
|
})
|
|
12169
12295
|
]);
|
|
12170
12296
|
/**
|
|
12297
|
+
* One state of a reviewed range: its resolved endpoints when a snapshot of it was taken. A new
|
|
12298
|
+
* one is recorded whenever the endpoints differ from the last, so a push, rebase or amend
|
|
12299
|
+
* yields the next iteration; the commits are pinned under `refs/diffle/iterations/`.
|
|
12300
|
+
*/
|
|
12301
|
+
const IterationSchema = object({
|
|
12302
|
+
/** 1-based, in recording order. */
|
|
12303
|
+
n: number(),
|
|
12304
|
+
oldSha: string(),
|
|
12305
|
+
newSha: string(),
|
|
12306
|
+
/** Epoch milliseconds. */
|
|
12307
|
+
recordedAt: number()
|
|
12308
|
+
});
|
|
12309
|
+
/**
|
|
12171
12310
|
* How the old side is derived: `old` itself, merge-base(old, new) with worktree
|
|
12172
12311
|
* taken as HEAD, or new's first parent (the empty tree for a root commit).
|
|
12173
12312
|
*/
|
|
@@ -12189,18 +12328,118 @@ const ComparisonSchema = object({
|
|
|
12189
12328
|
/** Fixed when the comparison is entered; discovery never changes comment storage. */
|
|
12190
12329
|
commentKey: string()
|
|
12191
12330
|
});
|
|
12192
|
-
const
|
|
12193
|
-
|
|
12194
|
-
|
|
12331
|
+
const CommitInfoSchema = object({
|
|
12332
|
+
sha: string(),
|
|
12333
|
+
short: string(),
|
|
12334
|
+
message: string()
|
|
12335
|
+
});
|
|
12336
|
+
/** A single reviewed commit and its neighbours along HEAD's first-parent history, for stepping through it. */
|
|
12337
|
+
const ReviewedCommitSchema = CommitInfoSchema.extend({
|
|
12338
|
+
/** First parent; null for a root commit. */
|
|
12339
|
+
parent: string().nullable(),
|
|
12340
|
+
/** The commit whose first parent this is, toward HEAD; null at HEAD or off HEAD's first-parent line. */
|
|
12341
|
+
child: string().nullable()
|
|
12342
|
+
});
|
|
12343
|
+
/** One commit of the compared range. */
|
|
12344
|
+
const RangeCommitSchema = CommitInfoSchema.extend({
|
|
12345
|
+
author: string(),
|
|
12346
|
+
email: string(),
|
|
12347
|
+
/** Author date, epoch milliseconds. */
|
|
12348
|
+
date: number()
|
|
12349
|
+
});
|
|
12350
|
+
/** How a commit of the older iteration relates to one of the newer, as `git range-diff` pairs them. */
|
|
12351
|
+
const PairStatusSchema = _enum([
|
|
12352
|
+
"identical",
|
|
12353
|
+
"changed",
|
|
12354
|
+
"message",
|
|
12355
|
+
"added",
|
|
12356
|
+
"dropped"
|
|
12357
|
+
]);
|
|
12358
|
+
/**
|
|
12359
|
+
* One row of a range-diff: `identical` and `changed` pairs have both commits, a `message` pair
|
|
12360
|
+
* the same patch under a new message, an `added` commit no old side and a `dropped` one no new side.
|
|
12361
|
+
*/
|
|
12362
|
+
const RangePairSchema = object({
|
|
12363
|
+
old: RangeCommitSchema.nullable(),
|
|
12364
|
+
new: RangeCommitSchema.nullable(),
|
|
12365
|
+
status: PairStatusSchema
|
|
12366
|
+
});
|
|
12367
|
+
/**
|
|
12368
|
+
* One range of a range-diff, `old..new`: the revisions that named it, so moved refs can be
|
|
12369
|
+
* resolved again, and the commits they resolved to. A recorded iteration of the enclosing
|
|
12370
|
+
* range carries its number; a range the reviewer named carries null.
|
|
12371
|
+
*/
|
|
12372
|
+
const RangeSideSchema = object({
|
|
12373
|
+
old: string(),
|
|
12374
|
+
new: string(),
|
|
12375
|
+
oldSha: string(),
|
|
12376
|
+
newSha: string(),
|
|
12377
|
+
iteration: number().nullable()
|
|
12378
|
+
});
|
|
12379
|
+
/**
|
|
12380
|
+
* Two ranges compared: two iterations of one range, or any two the reviewer named. The old
|
|
12381
|
+
* side is `from`'s head replayed onto `to`'s base (a tree, not a commit), so a rebase in
|
|
12382
|
+
* between does not show up as the upstream's changes; the new side is `to`'s head.
|
|
12383
|
+
*/
|
|
12384
|
+
const InterdiffSchema = object({
|
|
12385
|
+
from: RangeSideSchema,
|
|
12386
|
+
to: RangeSideSchema,
|
|
12387
|
+
/** Paths the replay could not merge; they carry conflict markers on the old side. */
|
|
12388
|
+
conflicts: string().array(),
|
|
12389
|
+
/** The two ranges' commits paired, in `git range-diff` order: the newer range's, dropped commits where they were. */
|
|
12390
|
+
pairs: RangePairSchema.array()
|
|
12391
|
+
});
|
|
12392
|
+
/**
|
|
12393
|
+
* One pair of an interdiff shown alone: `new` against `old` replayed onto `new`'s parent, so the
|
|
12394
|
+
* pair's own amendments show without the rebase; an added commit (`old` null) against its parent.
|
|
12395
|
+
*/
|
|
12396
|
+
const PairSpecSchema = object({
|
|
12397
|
+
old: string().nullable(),
|
|
12398
|
+
new: string(),
|
|
12399
|
+
conflicts: string().array()
|
|
12400
|
+
});
|
|
12401
|
+
const ModeSpecSchema = ComparisonSchema.extend({
|
|
12402
|
+
/**
|
|
12403
|
+
* Set while one commit of a range is focused or two ranges are compared: the range, whose commits the
|
|
12404
|
+
* snapshot keeps listing. For a range-diff it is the newer range, where the reviewer continues.
|
|
12405
|
+
*/
|
|
12406
|
+
within: ComparisonSchema.optional(),
|
|
12407
|
+
interdiff: InterdiffSchema.optional(),
|
|
12408
|
+
/** Set with `interdiff` while one of its pairs is shown. */
|
|
12409
|
+
pair: PairSpecSchema.optional()
|
|
12410
|
+
});
|
|
12195
12411
|
/**
|
|
12196
12412
|
* Display comparison endpoints with branch names instead of internal ref namespaces; a single commit as
|
|
12197
|
-
* `<sha>^!`,
|
|
12413
|
+
* `<sha>^!`, as `<range> @ <sha>` when focused within a range, as `<range> #1→#2` for two of its
|
|
12414
|
+
* iterations, as `<a>..<b> → <c>..<d>` for two named ranges, and with ` @ <sha>` for one pair of either.
|
|
12198
12415
|
*/
|
|
12199
12416
|
function comparisonLabel(mode) {
|
|
12417
|
+
if (mode.within && mode.interdiff) {
|
|
12418
|
+
const { from, to } = mode.interdiff;
|
|
12419
|
+
const iterations = shownIterations(mode);
|
|
12420
|
+
const label = iterations ? `${comparisonLabel(mode.within)} #${iterations.from}→#${iterations.to}` : `${rangeLabel(from)} → ${rangeLabel(to)}`;
|
|
12421
|
+
return mode.pair ? `${label} @ ${mode.pair.new.slice(0, 7)}` : label;
|
|
12422
|
+
}
|
|
12200
12423
|
if (mode.within) return `${comparisonLabel(mode.within)} @ ${mode.new.slice(0, 7)}`;
|
|
12201
12424
|
if (mode.base === "parent") return `${mode.new.slice(0, 7)}^!`;
|
|
12202
|
-
|
|
12203
|
-
|
|
12425
|
+
return `${refName(mode.old)}${mode.base === "merge-base" ? "..." : ".."}${refName(mode.new)}`;
|
|
12426
|
+
}
|
|
12427
|
+
/** One side of a range-diff as the reviewer named it, `<old>..<new>`, with full hashes abbreviated. */
|
|
12428
|
+
function rangeLabel(side) {
|
|
12429
|
+
const short = (rev) => /^[0-9a-f]{40}$/.test(rev) ? rev.slice(0, 7) : refName(rev);
|
|
12430
|
+
return `${short(side.old)}..${short(side.new)}`;
|
|
12431
|
+
}
|
|
12432
|
+
/** The iteration numbers an interdiff compares, or null when it compares ranges the reviewer named. */
|
|
12433
|
+
function shownIterations(mode) {
|
|
12434
|
+
const inter = mode.interdiff;
|
|
12435
|
+
return inter && inter.from.iteration != null && inter.to.iteration != null ? {
|
|
12436
|
+
from: inter.from.iteration,
|
|
12437
|
+
to: inter.to.iteration
|
|
12438
|
+
} : null;
|
|
12439
|
+
}
|
|
12440
|
+
/** A comparison endpoint as the user named it: without diffle's PR namespace or `refs/heads/`. */
|
|
12441
|
+
function refName(rev) {
|
|
12442
|
+
return rev.replace(/^refs\/diffle\/[^/]+\/\d+\/(?:base|head)\//, "").replace(/^refs\/(?:heads|remotes)\//, "");
|
|
12204
12443
|
}
|
|
12205
12444
|
const GithubPullRequestSchema = object({
|
|
12206
12445
|
repository: string(),
|
|
@@ -12250,31 +12489,17 @@ const ChangedFileSchema = object({
|
|
|
12250
12489
|
/** A gitlink (mode 160000): `blob` is the recorded commit, the patch shows the commit-id change, and there is no file to open. */
|
|
12251
12490
|
submodule: literal(true).optional()
|
|
12252
12491
|
});
|
|
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
12492
|
/**
|
|
12266
12493
|
* The commits a comparison spans: old..new, with the worktree taken as HEAD, or the one commit under `parent`.
|
|
12267
12494
|
* A focused commit lists its range's.
|
|
12268
12495
|
*/
|
|
12269
12496
|
const RangeCommitsSchema = object({
|
|
12270
12497
|
/** Oldest first; the newest MAX_RANGE_COMMITS when there are more. */
|
|
12271
|
-
list:
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
}).array(),
|
|
12277
|
-
total: number()
|
|
12498
|
+
list: RangeCommitSchema.array(),
|
|
12499
|
+
total: number(),
|
|
12500
|
+
/** The spanned comparison's resolved endpoints: the snapshot's own, or the range's when a commit is focused. */
|
|
12501
|
+
oldSha: string(),
|
|
12502
|
+
newSha: string()
|
|
12278
12503
|
});
|
|
12279
12504
|
object({
|
|
12280
12505
|
root: string(),
|
|
@@ -12293,7 +12518,20 @@ object({
|
|
|
12293
12518
|
tree: string().array(),
|
|
12294
12519
|
/** The reviewed commit when the comparison is a single commit against its parent; otherwise null. */
|
|
12295
12520
|
commit: ReviewedCommitSchema.nullable(),
|
|
12296
|
-
commits: RangeCommitsSchema
|
|
12521
|
+
commits: RangeCommitsSchema,
|
|
12522
|
+
/** The recorded iterations of the range `commits` spans, oldest first; empty for comparisons that are not followed. */
|
|
12523
|
+
iterations: IterationSchema.array()
|
|
12524
|
+
});
|
|
12525
|
+
/**
|
|
12526
|
+
* The compared refs resolve elsewhere than when the shown snapshot was taken. A refs-live
|
|
12527
|
+
* comparison is not recomputed under the reviewer; it reloads on request.
|
|
12528
|
+
*/
|
|
12529
|
+
const MovedSchema = object({
|
|
12530
|
+
/** The snapshot a reload would supersede. */
|
|
12531
|
+
version: number(),
|
|
12532
|
+
/** Where the comparison `Snapshot.commits` spans resolves now. */
|
|
12533
|
+
oldSha: string(),
|
|
12534
|
+
newSha: string()
|
|
12297
12535
|
});
|
|
12298
12536
|
/** Body of `POST /api/patch`: the changed files whose patches to return, concatenated. Unknown paths are skipped. */
|
|
12299
12537
|
const PatchRequestSchema = object({ paths: string().array().max(200) });
|
|
@@ -12425,11 +12663,24 @@ object({
|
|
|
12425
12663
|
matches: SearchMatchSchema.array(),
|
|
12426
12664
|
truncated: boolean()
|
|
12427
12665
|
});
|
|
12666
|
+
/**
|
|
12667
|
+
* How a review between refs reacts when one of them moves. `on`: recompute at once, as a worktree
|
|
12668
|
+
* review follows edits. `off`: announce the move and wait for Reload. `auto`: wait for Reload when an
|
|
12669
|
+
* endpoint names a branch, whose pushes and rebases should not swap the review out under the
|
|
12670
|
+
* reviewer; recompute at once otherwise (a detached HEAD, a tag, an expression).
|
|
12671
|
+
*/
|
|
12672
|
+
const FollowRefsSchema = _enum([
|
|
12673
|
+
"on",
|
|
12674
|
+
"auto",
|
|
12675
|
+
"off"
|
|
12676
|
+
]);
|
|
12428
12677
|
object({
|
|
12429
12678
|
/** Globs (picomatch syntax) for files that start viewed + collapsed. Empty by default. */
|
|
12430
12679
|
autoViewed: string().array(),
|
|
12431
12680
|
/** Unchanged lines shown around each change (git -U). Default 5. */
|
|
12432
12681
|
contextLines: number(),
|
|
12682
|
+
/** When a review between refs recomputes as its refs move; see `FollowRefsSchema`. Default auto. */
|
|
12683
|
+
followRefs: FollowRefsSchema,
|
|
12433
12684
|
/**
|
|
12434
12685
|
* Language server command per language, replacing the built-in candidate for it; an empty
|
|
12435
12686
|
* string disables the language. Read-only over HTTP; set with `diffle config set-lsp`.
|
|
@@ -12439,6 +12690,7 @@ object({
|
|
|
12439
12690
|
const DEFAULT_USER_CONFIG = {
|
|
12440
12691
|
autoViewed: [],
|
|
12441
12692
|
contextLines: 5,
|
|
12693
|
+
followRefs: "auto",
|
|
12442
12694
|
lspCommands: {}
|
|
12443
12695
|
};
|
|
12444
12696
|
/** A point in a new-side file. LSP semantics: the column counts UTF-16 units, like a JS string index. */
|
|
@@ -12509,6 +12761,18 @@ object({
|
|
|
12509
12761
|
object({
|
|
12510
12762
|
/** Null when the position is not inside a token or the server does not classify tokens. */
|
|
12511
12763
|
kind: string().nullable() });
|
|
12764
|
+
object({ occurrences: object({
|
|
12765
|
+
line: number().int().positive(),
|
|
12766
|
+
col: number().int().nonnegative(),
|
|
12767
|
+
endLine: number().int().positive(),
|
|
12768
|
+
endCol: number().int().nonnegative(),
|
|
12769
|
+
/** LSP DocumentHighlightKind: a plain textual match, a read, or a write. */
|
|
12770
|
+
kind: _enum([
|
|
12771
|
+
"text",
|
|
12772
|
+
"read",
|
|
12773
|
+
"write"
|
|
12774
|
+
])
|
|
12775
|
+
}).array() });
|
|
12512
12776
|
object({
|
|
12513
12777
|
name: string(),
|
|
12514
12778
|
/** LSP SymbolKind number; the client maps it to a label. */
|
|
@@ -12531,6 +12795,10 @@ discriminatedUnion("type", [
|
|
|
12531
12795
|
type: literal("snapshot"),
|
|
12532
12796
|
version: number()
|
|
12533
12797
|
}),
|
|
12798
|
+
object({
|
|
12799
|
+
type: literal("moved"),
|
|
12800
|
+
moved: MovedSchema.nullable()
|
|
12801
|
+
}),
|
|
12534
12802
|
object({ type: literal("threads") }),
|
|
12535
12803
|
object({ type: literal("viewed") }),
|
|
12536
12804
|
object({ type: literal("config") }),
|
|
@@ -12624,7 +12892,8 @@ function noServer(lsp, language) {
|
|
|
12624
12892
|
/** Writable HTTP config fields; unknown keys, including CLI-only lspCommands, are stripped. */
|
|
12625
12893
|
const ConfigUpdateSchema = object({
|
|
12626
12894
|
autoViewed: string().array().optional(),
|
|
12627
|
-
contextLines: number().int().min(0).max(1e4).optional()
|
|
12895
|
+
contextLines: number().int().min(0).max(1e4).optional(),
|
|
12896
|
+
followRefs: FollowRefsSchema.optional()
|
|
12628
12897
|
});
|
|
12629
12898
|
const ResolvedRequestSchema = object({ resolved: boolean() });
|
|
12630
12899
|
const ViewedBulkRequestSchema = object({ entries: ViewedEntrySchema.array() });
|
|
@@ -12670,6 +12939,76 @@ const ClearThreadsQuerySchema = object({ stale: literal("1").optional() });
|
|
|
12670
12939
|
const PatchQuerySchema = object({ path: string().optional() });
|
|
12671
12940
|
object({ error: string() });
|
|
12672
12941
|
//#endregion
|
|
12942
|
+
//#region src/shared/fuzzy.ts
|
|
12943
|
+
const MATCH = 1;
|
|
12944
|
+
const WORD_START = 8;
|
|
12945
|
+
const CONSECUTIVE = 4;
|
|
12946
|
+
const GAP = 1;
|
|
12947
|
+
/**
|
|
12948
|
+
* Score `candidate` against `query`, matched case-insensitively as an ordered subsequence;
|
|
12949
|
+
* null when it does not match. Higher is better: matches at word starts (after `_-./`, camelCase
|
|
12950
|
+
* humps, digit runs) and unbroken runs count most, so `AbsBuiFa` finds `AbstractBuilderFactory`
|
|
12951
|
+
* and `evruca` finds `evaluate_run_cases.py`.
|
|
12952
|
+
*/
|
|
12953
|
+
function fuzzyScore(query, candidate) {
|
|
12954
|
+
const q = Array.from(query.toLowerCase());
|
|
12955
|
+
const chars = Array.from(candidate);
|
|
12956
|
+
const n = q.length;
|
|
12957
|
+
const m = chars.length;
|
|
12958
|
+
if (n === 0) return 0;
|
|
12959
|
+
if (n > m) return null;
|
|
12960
|
+
const lower = chars.map((ch) => ch.toLowerCase());
|
|
12961
|
+
const bonus = chars.map((ch, j) => MATCH + wordStart(chars[j - 1], ch));
|
|
12962
|
+
let prev = new Float64Array(m).fill(-Infinity);
|
|
12963
|
+
for (let i = 0; i < n; i++) {
|
|
12964
|
+
const cur = new Float64Array(m).fill(-Infinity);
|
|
12965
|
+
let carry = -Infinity;
|
|
12966
|
+
for (let j = i; j < m; j++) {
|
|
12967
|
+
if (i > 0) carry = Math.max(carry - GAP, prev[j - 1]);
|
|
12968
|
+
if (lower[j] !== q[i]) continue;
|
|
12969
|
+
cur[j] = (i === 0 ? 0 : Math.max(carry, prev[j - 1] + CONSECUTIVE)) + bonus[j];
|
|
12970
|
+
}
|
|
12971
|
+
prev = cur;
|
|
12972
|
+
}
|
|
12973
|
+
const best = Math.max(...prev);
|
|
12974
|
+
return best === -Infinity ? null : best;
|
|
12975
|
+
}
|
|
12976
|
+
function wordStart(before, ch) {
|
|
12977
|
+
if (before === void 0) return 10;
|
|
12978
|
+
if (!ALNUM.test(before)) return WORD_START;
|
|
12979
|
+
if (UPPER.test(ch) && !UPPER.test(before)) return WORD_START;
|
|
12980
|
+
if (DIGIT.test(ch) && !DIGIT.test(before)) return WORD_START;
|
|
12981
|
+
return 0;
|
|
12982
|
+
}
|
|
12983
|
+
const ALNUM = /[\p{L}\p{N}]/u;
|
|
12984
|
+
const UPPER = /\p{Lu}/u;
|
|
12985
|
+
const DIGIT = /\p{N}/u;
|
|
12986
|
+
/**
|
|
12987
|
+
* `items` whose `key` matches `query`, best first; ties prefer the shorter key, then input order.
|
|
12988
|
+
* A blank query keeps every item in order.
|
|
12989
|
+
*/
|
|
12990
|
+
function fuzzyFilter(items, query, key) {
|
|
12991
|
+
const q = query.trim();
|
|
12992
|
+
if (!q) return [...items];
|
|
12993
|
+
const scored = [];
|
|
12994
|
+
for (const item of items) {
|
|
12995
|
+
const k = key(item);
|
|
12996
|
+
const score = fuzzyScore(q, k);
|
|
12997
|
+
if (score !== null) scored.push({
|
|
12998
|
+
item,
|
|
12999
|
+
score,
|
|
13000
|
+
length: k.length
|
|
13001
|
+
});
|
|
13002
|
+
}
|
|
13003
|
+
return scored.sort((a, b) => b.score - a.score || a.length - b.length).map((s) => s.item);
|
|
13004
|
+
}
|
|
13005
|
+
/** `fuzzyFilter`'s order, followed by the items it rejects in input order: re-ranks hits another matcher chose. */
|
|
13006
|
+
function fuzzyRank(items, query, key) {
|
|
13007
|
+
const matched = fuzzyFilter(items, query, key);
|
|
13008
|
+
const kept = new Set(matched);
|
|
13009
|
+
return [...matched, ...items.filter((item) => !kept.has(item))];
|
|
13010
|
+
}
|
|
13011
|
+
//#endregion
|
|
12673
13012
|
//#region src/server/lsp/hover.ts
|
|
12674
13013
|
/**
|
|
12675
13014
|
* One markdown document for the tooltip, or null when the server sent nothing
|
|
@@ -13239,6 +13578,30 @@ var LspBridge = class LspBridge {
|
|
|
13239
13578
|
}
|
|
13240
13579
|
return { kind: null };
|
|
13241
13580
|
}
|
|
13581
|
+
/**
|
|
13582
|
+
* Where else the symbol at `pos` occurs in its file, via `textDocument/documentHighlight`,
|
|
13583
|
+
* so scope and shadowing are the server's call. A server without the method, or a
|
|
13584
|
+
* position holding no symbol, yields no occurrences: the answer is advisory.
|
|
13585
|
+
*/
|
|
13586
|
+
async occurrences(pos) {
|
|
13587
|
+
let result;
|
|
13588
|
+
try {
|
|
13589
|
+
result = await this.query("textDocument/documentHighlight", {
|
|
13590
|
+
textDocument: { uri: this.uri(pos.path) },
|
|
13591
|
+
position: toLsp(pos)
|
|
13592
|
+
}, pos.path);
|
|
13593
|
+
} catch (e) {
|
|
13594
|
+
if (e instanceof LspUnavailableError) throw e;
|
|
13595
|
+
return { occurrences: [] };
|
|
13596
|
+
}
|
|
13597
|
+
return { occurrences: (result ?? []).map(({ range: { start, end }, kind }) => ({
|
|
13598
|
+
line: start.line + 1,
|
|
13599
|
+
col: start.character,
|
|
13600
|
+
endLine: end.line + 1,
|
|
13601
|
+
endCol: end.character,
|
|
13602
|
+
kind: kind === 3 ? "write" : kind === 2 ? "read" : "text"
|
|
13603
|
+
})) };
|
|
13604
|
+
}
|
|
13242
13605
|
async references(pos, opts = {}) {
|
|
13243
13606
|
const result = await this.query("textDocument/references", {
|
|
13244
13607
|
textDocument: { uri: this.uri(pos.path) },
|
|
@@ -13321,7 +13684,7 @@ var LspBridge = class LspBridge {
|
|
|
13321
13684
|
}
|
|
13322
13685
|
/**
|
|
13323
13686
|
* Symbols the server knows for `query`, limited to paths the snapshot
|
|
13324
|
-
* exposes. Membership comes from `has`, never from reading the file: a
|
|
13687
|
+
* exposes, best `fuzzyRank` match first. Membership comes from `has`, never from reading the file: a
|
|
13325
13688
|
* per-keystroke search must not read every hit's text.
|
|
13326
13689
|
*/
|
|
13327
13690
|
async workspaceSymbols(query, limit = 200) {
|
|
@@ -13333,7 +13696,7 @@ var LspBridge = class LspBridge {
|
|
|
13333
13696
|
const seen = /* @__PURE__ */ new Set();
|
|
13334
13697
|
result.forEach((s, i) => {
|
|
13335
13698
|
const path = paths[i];
|
|
13336
|
-
if (path == null || !inside.get(path)
|
|
13699
|
+
if (path == null || !inside.get(path)) return;
|
|
13337
13700
|
const start = "range" in s.location ? s.location.range.start : {
|
|
13338
13701
|
line: 0,
|
|
13339
13702
|
character: 0
|
|
@@ -13352,7 +13715,7 @@ var LspBridge = class LspBridge {
|
|
|
13352
13715
|
col: start.character
|
|
13353
13716
|
});
|
|
13354
13717
|
});
|
|
13355
|
-
return out;
|
|
13718
|
+
return fuzzyRank(out, query, (s) => s.name).slice(0, limit);
|
|
13356
13719
|
}
|
|
13357
13720
|
/**
|
|
13358
13721
|
* Contents of a file outside the snapshot that a recent result named as
|
|
@@ -13466,6 +13829,7 @@ var LspBridge = class LspBridge {
|
|
|
13466
13829
|
definition: { linkSupport: true },
|
|
13467
13830
|
typeDefinition: { linkSupport: true },
|
|
13468
13831
|
references: {},
|
|
13832
|
+
documentHighlight: {},
|
|
13469
13833
|
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
13470
13834
|
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
13471
13835
|
semanticTokens: {
|
|
@@ -14051,11 +14415,14 @@ var LspPool = class {
|
|
|
14051
14415
|
async references(pos) {
|
|
14052
14416
|
return this.bridgeFor(pos.path).references(pos);
|
|
14053
14417
|
}
|
|
14418
|
+
async occurrences(pos) {
|
|
14419
|
+
return this.bridgeFor(pos.path).occurrences(pos);
|
|
14420
|
+
}
|
|
14054
14421
|
async documentSymbols(path) {
|
|
14055
14422
|
return this.bridgeFor(path).documentSymbols(path);
|
|
14056
14423
|
}
|
|
14057
14424
|
/**
|
|
14058
|
-
* Symbols matching `query` from every running server,
|
|
14425
|
+
* Symbols matching `query` from every running server, best `fuzzyRank` match first. One server
|
|
14059
14426
|
* failing drops its share; only an outage everywhere is an error.
|
|
14060
14427
|
*/
|
|
14061
14428
|
async workspaceSymbols(query, limit = 200) {
|
|
@@ -14065,7 +14432,7 @@ var LspPool = class {
|
|
|
14065
14432
|
const out = results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
14066
14433
|
const failed = results.find((r) => r.status === "rejected");
|
|
14067
14434
|
if (!out.length && failed) throw failed.reason;
|
|
14068
|
-
return out.slice(0, limit);
|
|
14435
|
+
return fuzzyRank(out, query, (s) => s.name).slice(0, limit);
|
|
14069
14436
|
}
|
|
14070
14437
|
/**
|
|
14071
14438
|
* Keeps the diff's files open in the server for their language, starting one for each
|
|
@@ -14200,6 +14567,53 @@ function parseRevspec(args) {
|
|
|
14200
14567
|
base: "merge-base"
|
|
14201
14568
|
};
|
|
14202
14569
|
}
|
|
14570
|
+
/**
|
|
14571
|
+
* `git range-diff` style arguments, as two `old..new` ranges:
|
|
14572
|
+
* "a..b" "c..d" → a..b against c..d
|
|
14573
|
+
* "base" "b" "d" → base..b against base..d
|
|
14574
|
+
* "b...d" → d..b against b..d: each tip's commits since their divergence
|
|
14575
|
+
* A range also takes `<rev>^!`, the one commit. Empty sides default to HEAD; the worktree is
|
|
14576
|
+
* not a commit and has no place in a range.
|
|
14577
|
+
*/
|
|
14578
|
+
function parseRangeDiff(args) {
|
|
14579
|
+
if (args.length === 3) {
|
|
14580
|
+
const [base, b, d] = args;
|
|
14581
|
+
for (const arg of args) if (arg.includes("..") || arg.endsWith("^!")) throw new RevspecError("expected <base> <rev1> <rev2> as three plain revisions");
|
|
14582
|
+
return [range(base, b), range(base, d)];
|
|
14583
|
+
}
|
|
14584
|
+
if (args.length === 2) {
|
|
14585
|
+
const [a, b] = args;
|
|
14586
|
+
return [parseRange(a), parseRange(b)];
|
|
14587
|
+
}
|
|
14588
|
+
if (args.length === 1) {
|
|
14589
|
+
const arg = args[0];
|
|
14590
|
+
const three = arg.indexOf("...");
|
|
14591
|
+
if (three === -1 || arg.endsWith("^!")) throw new RevspecError("expected <range1> <range2>, <rev1>...<rev2> or <base> <rev1> <rev2>");
|
|
14592
|
+
const b = arg.slice(0, three) || "HEAD";
|
|
14593
|
+
const d = arg.slice(three + 3) || "HEAD";
|
|
14594
|
+
if (b.includes("..") || d.includes("..")) throw new RevspecError("cannot combine \"...\" with \"..\"");
|
|
14595
|
+
return [range(d, b), range(b, d)];
|
|
14596
|
+
}
|
|
14597
|
+
throw new RevspecError("expected <range1> <range2>, <rev1>...<rev2> or <base> <rev1> <rev2>");
|
|
14598
|
+
}
|
|
14599
|
+
function parseRange(arg) {
|
|
14600
|
+
if (arg.endsWith("^!")) {
|
|
14601
|
+
const commit = arg.slice(0, -2) || "HEAD";
|
|
14602
|
+
if (commit.includes("..")) throw new RevspecError("cannot combine \"^!\" with \"..\"");
|
|
14603
|
+
return range(`${commit}^`, commit);
|
|
14604
|
+
}
|
|
14605
|
+
if (arg.includes("...")) throw new RevspecError(`a range of a range-diff is <old>..<new>, not ${arg}`);
|
|
14606
|
+
const two = arg.indexOf("..");
|
|
14607
|
+
if (two === -1) throw new RevspecError(`not a range: ${arg} (expected <old>..<new>)`);
|
|
14608
|
+
return range(arg.slice(0, two) || "HEAD", arg.slice(two + 2) || "HEAD");
|
|
14609
|
+
}
|
|
14610
|
+
function range(old, next) {
|
|
14611
|
+
for (const rev of [old, next]) if (rev === WORKTREE || rev === `${WORKTREE}^`) throw new RevspecError("the worktree is not a commit");
|
|
14612
|
+
return {
|
|
14613
|
+
old,
|
|
14614
|
+
new: next
|
|
14615
|
+
};
|
|
14616
|
+
}
|
|
14203
14617
|
var RevspecError = class extends Error {};
|
|
14204
14618
|
//#endregion
|
|
14205
14619
|
//#region node_modules/@hono/node-server/dist/index.mjs
|
|
@@ -17536,6 +17950,14 @@ function createApi(deps) {
|
|
|
17536
17950
|
await next();
|
|
17537
17951
|
});
|
|
17538
17952
|
app.get("/api/snapshot", async (c) => c.json(await session.snapshotter.current()));
|
|
17953
|
+
app.get("/api/moved", (c) => c.json(session.moved));
|
|
17954
|
+
app.post("/api/reload", async (c) => c.json(await session.reload()));
|
|
17955
|
+
app.delete("/api/iterations", async (c) => c.json(await session.clearIterations()));
|
|
17956
|
+
app.delete("/api/iterations/:n", async (c) => {
|
|
17957
|
+
const n = Number(c.req.param("n"));
|
|
17958
|
+
if (!Number.isInteger(n) || n < 1) throw new RevspecError(`not an iteration number: ${c.req.param("n")}`);
|
|
17959
|
+
return c.json(await session.deleteIteration(n));
|
|
17960
|
+
});
|
|
17539
17961
|
app.post("/api/mode", async (c) => {
|
|
17540
17962
|
const req = ModeRequestSchema.parse(await readJson(c));
|
|
17541
17963
|
return c.json(await session.switchMode(req));
|
|
@@ -17731,6 +18153,12 @@ function createApi(deps) {
|
|
|
17731
18153
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
17732
18154
|
return c.json(await r.lsp.references(pos));
|
|
17733
18155
|
});
|
|
18156
|
+
app.post("/api/lsp/occurrences", async (c) => {
|
|
18157
|
+
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18158
|
+
const r = await lspFor(pos.path);
|
|
18159
|
+
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18160
|
+
return c.json(await r.lsp.occurrences(pos));
|
|
18161
|
+
});
|
|
17734
18162
|
app.get("/api/lsp/symbols", async (c) => {
|
|
17735
18163
|
const { path, q: query } = SymbolsQuerySchema.parse(c.req.query());
|
|
17736
18164
|
const r = await lspFor(path);
|
|
@@ -17743,6 +18171,7 @@ function createApi(deps) {
|
|
|
17743
18171
|
await deps.config.set(body);
|
|
17744
18172
|
hub.broadcast({ type: "config" });
|
|
17745
18173
|
if (body.contextLines != null) await session.setContext(deps.config.get().contextLines);
|
|
18174
|
+
if (body.followRefs != null) session.setFollowRefs(deps.config.get().followRefs);
|
|
17746
18175
|
return c.json(effectiveConfig(deps));
|
|
17747
18176
|
});
|
|
17748
18177
|
return app;
|
|
@@ -18422,6 +18851,163 @@ function describe(skipped) {
|
|
|
18422
18851
|
for (const s of skipped) counts.set(s.reason, (counts.get(s.reason) ?? 0) + 1);
|
|
18423
18852
|
return [...counts].map(([reason, n]) => `${n} ${reason}`).join(", ");
|
|
18424
18853
|
}
|
|
18854
|
+
const FileSchema = object({
|
|
18855
|
+
version: literal(1),
|
|
18856
|
+
ranges: record(string(), IterationSchema.array())
|
|
18857
|
+
});
|
|
18858
|
+
/**
|
|
18859
|
+
* The iterations of one range, under its comment key in `<gitDir>/diffle/iterations.json`.
|
|
18860
|
+
* Like the comment store, nothing is cached: every mutation is a read-modify-write of fresh
|
|
18861
|
+
* disk state under the file lock, so two servers on one repository keep each other's records.
|
|
18862
|
+
* Each iteration's commits are pinned under `refs/diffle/iterations/<key hash>/<n>/{old,new}`.
|
|
18863
|
+
*/
|
|
18864
|
+
var IterationStore = class {
|
|
18865
|
+
repo;
|
|
18866
|
+
key;
|
|
18867
|
+
file;
|
|
18868
|
+
refs;
|
|
18869
|
+
constructor(repo, key) {
|
|
18870
|
+
this.repo = repo;
|
|
18871
|
+
this.key = key;
|
|
18872
|
+
this.file = join(repo.gitDir, "diffle", "iterations.json");
|
|
18873
|
+
this.refs = `refs/diffle/iterations/${createHash("sha1").update(key).digest("hex").slice(0, 16)}`;
|
|
18874
|
+
}
|
|
18875
|
+
/** Oldest first. */
|
|
18876
|
+
async list() {
|
|
18877
|
+
return (await read(this.file)).ranges[this.key] ?? [];
|
|
18878
|
+
}
|
|
18879
|
+
/** The iteration numbered `n`, or null. */
|
|
18880
|
+
async get(n) {
|
|
18881
|
+
return (await this.list()).find((it) => it.n === n) ?? null;
|
|
18882
|
+
}
|
|
18883
|
+
/**
|
|
18884
|
+
* Records the endpoints as the next iteration, unless the latest already has them. Returns
|
|
18885
|
+
* the list afterwards. Pins the commits first, so a recorded iteration is always reachable.
|
|
18886
|
+
*/
|
|
18887
|
+
async record(oldSha, newSha) {
|
|
18888
|
+
return withFileLock(this.file, async () => {
|
|
18889
|
+
const all = await read(this.file);
|
|
18890
|
+
const list = all.ranges[this.key] ?? [];
|
|
18891
|
+
const last = list.at(-1);
|
|
18892
|
+
if (last && last.oldSha === oldSha && last.newSha === newSha) return list;
|
|
18893
|
+
const n = (last?.n ?? 0) + 1;
|
|
18894
|
+
const next = [...list, {
|
|
18895
|
+
n,
|
|
18896
|
+
oldSha,
|
|
18897
|
+
newSha,
|
|
18898
|
+
recordedAt: Date.now()
|
|
18899
|
+
}];
|
|
18900
|
+
const dropped = next.splice(0, Math.max(0, next.length - 50));
|
|
18901
|
+
await this.repo.pin({
|
|
18902
|
+
[`${this.refs}/${n}/old`]: oldSha,
|
|
18903
|
+
[`${this.refs}/${n}/new`]: newSha,
|
|
18904
|
+
...Object.fromEntries(dropped.flatMap((it) => [[`${this.refs}/${it.n}/old`, null], [`${this.refs}/${it.n}/new`, null]]))
|
|
18905
|
+
});
|
|
18906
|
+
all.ranges[this.key] = next;
|
|
18907
|
+
await writeFileAtomic(this.file, JSON.stringify(all, null, 2) + "\n");
|
|
18908
|
+
return next;
|
|
18909
|
+
});
|
|
18910
|
+
}
|
|
18911
|
+
/** Forgets the iteration numbered `n`, if recorded, and unpins its commits. The rest keep their numbers. */
|
|
18912
|
+
async remove(n) {
|
|
18913
|
+
return withFileLock(this.file, async () => {
|
|
18914
|
+
const all = await read(this.file);
|
|
18915
|
+
const list = all.ranges[this.key] ?? [];
|
|
18916
|
+
if (!list.some((it) => it.n === n)) return list;
|
|
18917
|
+
await this.repo.pin({
|
|
18918
|
+
[`${this.refs}/${n}/old`]: null,
|
|
18919
|
+
[`${this.refs}/${n}/new`]: null
|
|
18920
|
+
});
|
|
18921
|
+
const next = list.filter((it) => it.n !== n);
|
|
18922
|
+
if (next.length) all.ranges[this.key] = next;
|
|
18923
|
+
else delete all.ranges[this.key];
|
|
18924
|
+
await writeFileAtomic(this.file, JSON.stringify(all, null, 2) + "\n");
|
|
18925
|
+
return next;
|
|
18926
|
+
});
|
|
18927
|
+
}
|
|
18928
|
+
/** Forgets every iteration of this range and unpins their commits. */
|
|
18929
|
+
async clear() {
|
|
18930
|
+
await withFileLock(this.file, async () => {
|
|
18931
|
+
const all = await read(this.file);
|
|
18932
|
+
const list = all.ranges[this.key] ?? [];
|
|
18933
|
+
await this.repo.pin(Object.fromEntries(list.flatMap((it) => [[`${this.refs}/${it.n}/old`, null], [`${this.refs}/${it.n}/new`, null]])));
|
|
18934
|
+
delete all.ranges[this.key];
|
|
18935
|
+
await writeFileAtomic(this.file, JSON.stringify(all, null, 2) + "\n");
|
|
18936
|
+
});
|
|
18937
|
+
}
|
|
18938
|
+
};
|
|
18939
|
+
async function read(file) {
|
|
18940
|
+
try {
|
|
18941
|
+
return FileSchema.parse(JSON.parse(await readFile(file, "utf8")));
|
|
18942
|
+
} catch (e) {
|
|
18943
|
+
if (e.code === "ENOENT") return {
|
|
18944
|
+
version: 1,
|
|
18945
|
+
ranges: {}
|
|
18946
|
+
};
|
|
18947
|
+
throw e;
|
|
18948
|
+
}
|
|
18949
|
+
}
|
|
18950
|
+
//#endregion
|
|
18951
|
+
//#region src/server/rangediff.ts
|
|
18952
|
+
/**
|
|
18953
|
+
* The mode that compares `older` with `newer`: `older`'s head replayed onto `newer`'s base against
|
|
18954
|
+
* `newer`'s head, so only what the series itself changed in between shows, with the two ranges'
|
|
18955
|
+
* commits paired. `range` is where the reviewer continues afterwards; its refs keep being watched,
|
|
18956
|
+
* so the comparison is pinned under them. Comments are off in it: the old side is a tree no
|
|
18957
|
+
* revision names.
|
|
18958
|
+
*/
|
|
18959
|
+
async function compareRanges(repo, range, older, newer) {
|
|
18960
|
+
const commentKey = older.iteration != null && newer.iteration != null ? `${range.commentKey}:interdiff:${older.iteration}-${newer.iteration}` : `range-diff:${older.oldSha}..${older.newSha}:${newer.oldSha}..${newer.newSha}`;
|
|
18961
|
+
const [{ tree, conflicts }, pairs] = await Promise.all([older.oldSha === newer.oldSha ? repo.tree(older.newSha).then((tree) => ({
|
|
18962
|
+
tree,
|
|
18963
|
+
conflicts: []
|
|
18964
|
+
})) : repo.replay(older.oldSha, newer.oldSha, older.newSha), pairRanges(repo, older, newer)]);
|
|
18965
|
+
return {
|
|
18966
|
+
old: tree,
|
|
18967
|
+
new: newer.newSha,
|
|
18968
|
+
base: "direct",
|
|
18969
|
+
live: range.live === "none" ? "none" : "refs",
|
|
18970
|
+
commentKey,
|
|
18971
|
+
within: range,
|
|
18972
|
+
interdiff: {
|
|
18973
|
+
from: older,
|
|
18974
|
+
to: newer,
|
|
18975
|
+
conflicts,
|
|
18976
|
+
pairs
|
|
18977
|
+
}
|
|
18978
|
+
};
|
|
18979
|
+
}
|
|
18980
|
+
/**
|
|
18981
|
+
* Pairs the two ranges' commits as `git range-diff` does and tells a pair whose patch is the
|
|
18982
|
+
* same (a reworded commit) from an amended one by patch id. range-diff compares every commit
|
|
18983
|
+
* with every other, so a range past MAX_RANGE_COMMITS is refused rather than left running.
|
|
18984
|
+
*/
|
|
18985
|
+
async function pairRanges(repo, older, newer) {
|
|
18986
|
+
const oldRange = `${older.oldSha}..${older.newSha}`;
|
|
18987
|
+
const newRange = `${newer.oldSha}..${newer.newSha}`;
|
|
18988
|
+
const [oldCommits, newCommits] = await Promise.all([repo.rangeCommits(oldRange, 250), repo.rangeCommits(newRange, 250)]);
|
|
18989
|
+
for (const [side, commits] of [[older, oldCommits], [newer, newCommits]]) if (commits.total > 250) throw new RevspecError(`${comparisonLabel({
|
|
18990
|
+
...side,
|
|
18991
|
+
base: "direct"
|
|
18992
|
+
})} spans ${commits.total} commits; a range-diff takes at most 250`);
|
|
18993
|
+
const rows = await repo.rangeDiff(oldRange, newRange);
|
|
18994
|
+
const find = async (list, abbrev) => {
|
|
18995
|
+
if (abbrev === null) return null;
|
|
18996
|
+
return list.find((c) => c.sha.startsWith(abbrev)) ?? (await repo.rangeCommits(`${abbrev}^!`, 1)).list[0] ?? null;
|
|
18997
|
+
};
|
|
18998
|
+
const paired = await Promise.all(rows.map(async (row) => ({
|
|
18999
|
+
old: await find(oldCommits.list, row.old),
|
|
19000
|
+
new: await find(newCommits.list, row.new),
|
|
19001
|
+
marker: row.marker
|
|
19002
|
+
})));
|
|
19003
|
+
const changed = paired.filter((p) => p.marker === "!" && p.old && p.new);
|
|
19004
|
+
const ids = await repo.patchIds(changed.flatMap((p) => [p.old.sha, p.new.sha]));
|
|
19005
|
+
return paired.map(({ old, new: next, marker }) => ({
|
|
19006
|
+
old,
|
|
19007
|
+
new: next,
|
|
19008
|
+
status: marker === "=" ? "identical" : marker === "<" ? "dropped" : marker === ">" ? "added" : old && next && ids.get(old.sha) !== void 0 && ids.get(old.sha) === ids.get(next.sha) ? "message" : "changed"
|
|
19009
|
+
}));
|
|
19010
|
+
}
|
|
18425
19011
|
//#endregion
|
|
18426
19012
|
//#region src/server/mode.ts
|
|
18427
19013
|
/**
|
|
@@ -18451,6 +19037,11 @@ function describe(skipped) {
|
|
|
18451
19037
|
* the empty tree. Invalid revisions or missing merge bases throw RevspecError.
|
|
18452
19038
|
*/
|
|
18453
19039
|
async function resolveComparison(repo, comparison) {
|
|
19040
|
+
if (comparison.interdiff && comparison.base !== "parent") return {
|
|
19041
|
+
oldSha: comparison.old,
|
|
19042
|
+
newSha: comparison.new,
|
|
19043
|
+
live: "none"
|
|
19044
|
+
};
|
|
18454
19045
|
const endpoints = [comparison.old, comparison.new];
|
|
18455
19046
|
const resolve = async (rev) => {
|
|
18456
19047
|
if (rev === "worktree") return rev;
|
|
@@ -18487,9 +19078,15 @@ async function resolveComparison(repo, comparison) {
|
|
|
18487
19078
|
live: endpoints.includes("worktree") ? "worktree" : pinned(comparison.old, old) && pinned(comparison.new, next) ? "none" : "refs"
|
|
18488
19079
|
};
|
|
18489
19080
|
}
|
|
19081
|
+
/** Whether either endpoint names a local or remote branch, HEAD on one included. */
|
|
19082
|
+
async function namesBranch(repo, comparison) {
|
|
19083
|
+
const [old, next] = await Promise.all([repo.branchRef(comparison.old), repo.branchRef(comparison.new)]);
|
|
19084
|
+
return old != null || next != null;
|
|
19085
|
+
}
|
|
18490
19086
|
/** Resolves an input command into a comparison and optional explicit PR identity. */
|
|
18491
19087
|
async function resolveReview(req, repo, github) {
|
|
18492
19088
|
if (req.kind === "pr") return resolvePr(req, repo, github);
|
|
19089
|
+
if (req.kind === "range-diff") return rangeDiffReview(repo, parseRangeDiff(req.args));
|
|
18493
19090
|
const parsed = req.kind === "working" ? {
|
|
18494
19091
|
old: "HEAD",
|
|
18495
19092
|
new: "worktree",
|
|
@@ -18505,12 +19102,48 @@ async function resolveReview(req, repo, github) {
|
|
|
18505
19102
|
commentKey: `commit:${sha}`
|
|
18506
19103
|
} };
|
|
18507
19104
|
}
|
|
19105
|
+
return { mode: await resolveRange(repo, parsed, req.kind === "working" ? "working" : void 0) };
|
|
19106
|
+
}
|
|
19107
|
+
/** A range as a comparison: resolved for its liveness and keyed for its comments, unless `key` names them. */
|
|
19108
|
+
async function resolveRange(repo, parsed, key) {
|
|
18508
19109
|
const { oldSha, newSha, live } = await resolveComparison(repo, parsed);
|
|
18509
|
-
return {
|
|
19110
|
+
return {
|
|
18510
19111
|
...parsed,
|
|
18511
19112
|
live,
|
|
18512
|
-
commentKey:
|
|
18513
|
-
}
|
|
19113
|
+
commentKey: key ?? await commentKey(repo, parsed, oldSha, newSha)
|
|
19114
|
+
};
|
|
19115
|
+
}
|
|
19116
|
+
/**
|
|
19117
|
+
* Two ranges the reviewer named, compared. Both resolve now and the comparison is pinned; the
|
|
19118
|
+
* newer range is where "All changes" leads and whose refs are watched. Resolving again, as a
|
|
19119
|
+
* reload does, follows the revisions either range was named by.
|
|
19120
|
+
*/
|
|
19121
|
+
async function rangeDiffReview(repo, [older, newer], prUrl) {
|
|
19122
|
+
const resolveSide = async (parsed) => {
|
|
19123
|
+
const { oldSha, newSha } = await resolveComparison(repo, {
|
|
19124
|
+
...parsed,
|
|
19125
|
+
base: "direct"
|
|
19126
|
+
});
|
|
19127
|
+
return {
|
|
19128
|
+
old: parsed.old,
|
|
19129
|
+
new: parsed.new,
|
|
19130
|
+
oldSha,
|
|
19131
|
+
newSha,
|
|
19132
|
+
iteration: null
|
|
19133
|
+
};
|
|
19134
|
+
};
|
|
19135
|
+
const [range, from, to] = await Promise.all([
|
|
19136
|
+
resolveRange(repo, {
|
|
19137
|
+
...newer,
|
|
19138
|
+
base: "direct"
|
|
19139
|
+
}),
|
|
19140
|
+
resolveSide(older),
|
|
19141
|
+
resolveSide(newer)
|
|
19142
|
+
]);
|
|
19143
|
+
return {
|
|
19144
|
+
prUrl,
|
|
19145
|
+
mode: await compareRanges(repo, range, from, to)
|
|
19146
|
+
};
|
|
18514
19147
|
}
|
|
18515
19148
|
/**
|
|
18516
19149
|
* Branch identities keep review state stable across pushes and local branch aliases.
|
|
@@ -18647,16 +19280,18 @@ var Snapshotter = class {
|
|
|
18647
19280
|
mode;
|
|
18648
19281
|
version;
|
|
18649
19282
|
context;
|
|
19283
|
+
iterations;
|
|
18650
19284
|
cache = null;
|
|
18651
19285
|
patches = /* @__PURE__ */ new Map();
|
|
18652
19286
|
all = null;
|
|
18653
19287
|
/** Content-sniff verdict by blob sha. A blob is immutable, so a refresh re-reads only new content. */
|
|
18654
19288
|
sniffed = /* @__PURE__ */ new Map();
|
|
18655
|
-
constructor(repo, mode, version, context) {
|
|
19289
|
+
constructor(repo, mode, version, context, iterations = null) {
|
|
18656
19290
|
this.repo = repo;
|
|
18657
19291
|
this.mode = mode;
|
|
18658
19292
|
this.version = version;
|
|
18659
19293
|
this.context = context;
|
|
19294
|
+
this.iterations = iterations;
|
|
18660
19295
|
}
|
|
18661
19296
|
/** Change context lines: drops cached patches; the next snapshot carries the new value. */
|
|
18662
19297
|
setContext(context, version) {
|
|
@@ -18728,7 +19363,7 @@ var Snapshotter = class {
|
|
|
18728
19363
|
this.mode.base === "parent" ? this.reviewedCommit(newSha) : null,
|
|
18729
19364
|
this.rangeCommits(oldSha, newSha, headSha)
|
|
18730
19365
|
]);
|
|
18731
|
-
await this.fillGenerated(changed, newSha);
|
|
19366
|
+
const [iterations] = await Promise.all([this.recordIteration(commits), this.fillGenerated(changed, newSha)]);
|
|
18732
19367
|
const tree = new Set(tracked);
|
|
18733
19368
|
for (const f of changed) {
|
|
18734
19369
|
if (f.status === "D") tree.delete(f.path);
|
|
@@ -18746,11 +19381,17 @@ var Snapshotter = class {
|
|
|
18746
19381
|
changed,
|
|
18747
19382
|
tree: [...tree].sort(),
|
|
18748
19383
|
commit,
|
|
18749
|
-
commits
|
|
19384
|
+
commits,
|
|
19385
|
+
iterations
|
|
18750
19386
|
};
|
|
18751
19387
|
for (const f of changed.slice(0, PREWARM)) this.patchFor(snap, f).catch(() => {});
|
|
18752
19388
|
return snap;
|
|
18753
19389
|
}
|
|
19390
|
+
/** The snapshot taken is the range's current iteration; the worktree has no commit to record. */
|
|
19391
|
+
async recordIteration(commits) {
|
|
19392
|
+
if (!this.iterations || commits.oldSha === "worktree" || commits.newSha === "worktree") return [];
|
|
19393
|
+
return this.iterations.record(commits.oldSha, commits.newSha);
|
|
19394
|
+
}
|
|
18754
19395
|
async reviewedCommit(sha) {
|
|
18755
19396
|
const [info, neighbours] = await Promise.all([this.repo.commitInfo(sha), this.repo.commitNeighbours(sha)]);
|
|
18756
19397
|
return info && {
|
|
@@ -18763,16 +19404,23 @@ var Snapshotter = class {
|
|
|
18763
19404
|
const range = await resolveComparison(this.repo, this.mode.within);
|
|
18764
19405
|
return this.spanned(range.oldSha, range.newSha, headSha);
|
|
18765
19406
|
}
|
|
18766
|
-
if (this.mode.base === "parent") return
|
|
19407
|
+
if (this.mode.base === "parent") return {
|
|
19408
|
+
...await this.repo.rangeCommits(`${newSha}^!`, 250),
|
|
19409
|
+
oldSha,
|
|
19410
|
+
newSha
|
|
19411
|
+
};
|
|
18767
19412
|
return this.spanned(oldSha, newSha, headSha);
|
|
18768
19413
|
}
|
|
18769
|
-
spanned(oldSha, newSha, headSha) {
|
|
19414
|
+
async spanned(oldSha, newSha, headSha) {
|
|
18770
19415
|
const [from, to] = [oldSha, newSha].map((sha) => sha === "worktree" ? headSha : sha);
|
|
18771
|
-
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
|
|
18775
|
-
|
|
19416
|
+
return {
|
|
19417
|
+
...!from || !to || from === to ? {
|
|
19418
|
+
list: [],
|
|
19419
|
+
total: 0
|
|
19420
|
+
} : await this.repo.rangeCommits(`${from}..${to}`, 250),
|
|
19421
|
+
oldSha,
|
|
19422
|
+
newSha
|
|
19423
|
+
};
|
|
18776
19424
|
}
|
|
18777
19425
|
/**
|
|
18778
19426
|
* Sets `generated` on each non-binary changed file. A `linguist-generated`
|
|
@@ -18822,6 +19470,10 @@ var Snapshotter = class {
|
|
|
18822
19470
|
* while its task holds the queue, so published versions are monotonic and the
|
|
18823
19471
|
* active snapshotter always carries the session's context. Requests that arrive
|
|
18824
19472
|
* before the first mode resolves await `ready()`.
|
|
19473
|
+
*
|
|
19474
|
+
* A worktree-live comparison recomputes on every change. A refs-live one only
|
|
19475
|
+
* announces that its refs moved (`moved`) and recomputes on `reload()`, so a
|
|
19476
|
+
* push or rebase does not swap the review out from under the reviewer.
|
|
18825
19477
|
*/
|
|
18826
19478
|
var Session = class {
|
|
18827
19479
|
repo;
|
|
@@ -18836,6 +19488,8 @@ var Session = class {
|
|
|
18836
19488
|
queue = Promise.resolve();
|
|
18837
19489
|
/** The refresh waiting for the queue, if any. Later requests join it. */
|
|
18838
19490
|
queuedRefresh = null;
|
|
19491
|
+
/** The refs check waiting for the queue, if any. Later signals join it. */
|
|
19492
|
+
queuedCheck = null;
|
|
18839
19493
|
readable = /* @__PURE__ */ new WeakMap();
|
|
18840
19494
|
snapshotListeners = /* @__PURE__ */ new Set();
|
|
18841
19495
|
constructor(repo, hub, opts) {
|
|
@@ -18857,6 +19511,24 @@ var Session = class {
|
|
|
18857
19511
|
get snapshotter() {
|
|
18858
19512
|
return this.require().snapshotter;
|
|
18859
19513
|
}
|
|
19514
|
+
/** The pending reload of a refs-live comparison, for a client that connects after the push. */
|
|
19515
|
+
get moved() {
|
|
19516
|
+
return this.require().moved;
|
|
19517
|
+
}
|
|
19518
|
+
/** Whether moved refs recompute the review, as worktree edits always do, or only announce themselves. */
|
|
19519
|
+
get follows() {
|
|
19520
|
+
const a = this.require();
|
|
19521
|
+
if (a.mode.live === "worktree") return true;
|
|
19522
|
+
switch (this.opts.followRefs) {
|
|
19523
|
+
case "on": return true;
|
|
19524
|
+
case "off": return false;
|
|
19525
|
+
case "auto": return !a.branches;
|
|
19526
|
+
}
|
|
19527
|
+
}
|
|
19528
|
+
/** Change the user's choice for the running session; a pending notice stands until a reload picks the refs up. */
|
|
19529
|
+
setFollowRefs(followRefs) {
|
|
19530
|
+
this.opts.followRefs = followRefs;
|
|
19531
|
+
}
|
|
18860
19532
|
/** Cached independently of snapshot construction; callers never hold the transition queue. */
|
|
18861
19533
|
github(snap) {
|
|
18862
19534
|
const active = this.require();
|
|
@@ -18944,7 +19616,7 @@ var Session = class {
|
|
|
18944
19616
|
}
|
|
18945
19617
|
/** Switch modes at runtime. Broadcasts a snapshot bump on success. */
|
|
18946
19618
|
async switchMode(req) {
|
|
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)));
|
|
19619
|
+
const snap = await this.run(async () => this.activate(req.kind === "focus" ? await this.focus(req.commit) : req.kind === "interdiff" ? await this.interdiff(req.from, req.to) : req.kind === "pair" ? await this.pair(req.commit) : await resolveReview(req, this.repo, this.opts.github)));
|
|
18948
19620
|
this.hub.broadcast({
|
|
18949
19621
|
type: "snapshot",
|
|
18950
19622
|
version: snap.version
|
|
@@ -18979,6 +19651,87 @@ var Session = class {
|
|
|
18979
19651
|
};
|
|
18980
19652
|
}
|
|
18981
19653
|
/**
|
|
19654
|
+
* Compares two recorded iterations of the active range, so only what the branch itself changed
|
|
19655
|
+
* in between shows. The interdiff keeps its own comments and the range's PR identity; its refs
|
|
19656
|
+
* keep being watched.
|
|
19657
|
+
*/
|
|
19658
|
+
async interdiff(from, to) {
|
|
19659
|
+
const a = this.require();
|
|
19660
|
+
const range = a.mode.within ?? a.mode;
|
|
19661
|
+
if (range.base === "parent") throw new RevspecError("a single commit has no iterations");
|
|
19662
|
+
if (from >= to) throw new RevspecError("compare an older iteration with a newer one");
|
|
19663
|
+
const store = new IterationStore(this.repo, range.commentKey);
|
|
19664
|
+
const [older, newer] = await Promise.all([store.get(from), store.get(to)]);
|
|
19665
|
+
if (!older || !newer) throw new RevspecError(`no iteration #${older ? to : from} of ${comparisonLabel(range)}`);
|
|
19666
|
+
const side = (it) => ({
|
|
19667
|
+
old: it.oldSha,
|
|
19668
|
+
new: it.newSha,
|
|
19669
|
+
oldSha: it.oldSha,
|
|
19670
|
+
newSha: it.newSha,
|
|
19671
|
+
iteration: it.n
|
|
19672
|
+
});
|
|
19673
|
+
return {
|
|
19674
|
+
prUrl: a.prUrl,
|
|
19675
|
+
mode: await compareRanges(this.repo, range, side(older), side(newer))
|
|
19676
|
+
};
|
|
19677
|
+
}
|
|
19678
|
+
/**
|
|
19679
|
+
* Shows one pair of the active interdiff by its new-side commit: an added commit against its
|
|
19680
|
+
* parent, an amended or reworded one against its old self replayed onto its parent. With null,
|
|
19681
|
+
* the whole interdiff again. Identical pairs have nothing to show; dropped ones have no new side.
|
|
19682
|
+
*/
|
|
19683
|
+
async pair(commit) {
|
|
19684
|
+
const a = this.require();
|
|
19685
|
+
const { interdiff, within: range } = a.mode;
|
|
19686
|
+
if (!interdiff || !range) throw new RevspecError("no interdiff to pick a pair from");
|
|
19687
|
+
if (commit === null) return {
|
|
19688
|
+
prUrl: a.prUrl,
|
|
19689
|
+
mode: await compareRanges(this.repo, range, interdiff.from, interdiff.to)
|
|
19690
|
+
};
|
|
19691
|
+
const found = interdiff.pairs.find((p) => p.new && (p.new.sha === commit || p.new.short === commit));
|
|
19692
|
+
if (!found?.new) throw new RevspecError(`not a commit of ${comparisonLabel(a.mode)}: ${commit}`);
|
|
19693
|
+
if (found.status === "identical") throw new RevspecError(`${found.new.short} is identical in both ranges`);
|
|
19694
|
+
const base = {
|
|
19695
|
+
live: range.live === "none" ? "none" : "refs",
|
|
19696
|
+
within: range,
|
|
19697
|
+
interdiff
|
|
19698
|
+
};
|
|
19699
|
+
if (!found.old) {
|
|
19700
|
+
const sha = found.new.sha;
|
|
19701
|
+
return {
|
|
19702
|
+
prUrl: a.prUrl,
|
|
19703
|
+
mode: {
|
|
19704
|
+
...base,
|
|
19705
|
+
old: sha,
|
|
19706
|
+
new: sha,
|
|
19707
|
+
base: "parent",
|
|
19708
|
+
commentKey: `commit:${sha}`,
|
|
19709
|
+
pair: {
|
|
19710
|
+
old: null,
|
|
19711
|
+
new: sha,
|
|
19712
|
+
conflicts: []
|
|
19713
|
+
}
|
|
19714
|
+
}
|
|
19715
|
+
};
|
|
19716
|
+
}
|
|
19717
|
+
const { tree, conflicts } = await this.repo.replay(`${found.old.sha}^`, `${found.new.sha}^`, found.old.sha);
|
|
19718
|
+
return {
|
|
19719
|
+
prUrl: a.prUrl,
|
|
19720
|
+
mode: {
|
|
19721
|
+
...base,
|
|
19722
|
+
old: tree,
|
|
19723
|
+
new: found.new.sha,
|
|
19724
|
+
base: "direct",
|
|
19725
|
+
commentKey: `${range.commentKey}:pair:${found.old.sha}-${found.new.sha}`,
|
|
19726
|
+
pair: {
|
|
19727
|
+
old: found.old.sha,
|
|
19728
|
+
new: found.new.sha,
|
|
19729
|
+
conflicts
|
|
19730
|
+
}
|
|
19731
|
+
}
|
|
19732
|
+
};
|
|
19733
|
+
}
|
|
19734
|
+
/**
|
|
18982
19735
|
* Runs `fn` after every earlier mutation settled. The only way to touch
|
|
18983
19736
|
* `active`, `version` or `opts.context`.
|
|
18984
19737
|
*/
|
|
@@ -18988,14 +19741,22 @@ var Session = class {
|
|
|
18988
19741
|
return r;
|
|
18989
19742
|
}
|
|
18990
19743
|
async activate({ mode, prUrl }) {
|
|
18991
|
-
const
|
|
18992
|
-
const
|
|
19744
|
+
const range = mode.within ?? mode;
|
|
19745
|
+
const followed = range.base !== "parent" && (range.live === "refs" || prUrl != null);
|
|
19746
|
+
const snapshotter = new Snapshotter(this.repo, mode, ++this.version, this.opts.context, followed ? new IterationStore(this.repo, range.commentKey) : null);
|
|
19747
|
+
const [comments, snap, branches] = await Promise.all([
|
|
19748
|
+
CommentStore.open(this.repo.gitDir, mode.commentKey),
|
|
19749
|
+
snapshotter.current(),
|
|
19750
|
+
range.live === "refs" && namesBranch(this.repo, range)
|
|
19751
|
+
]);
|
|
18993
19752
|
const next = {
|
|
18994
19753
|
mode,
|
|
18995
19754
|
prUrl,
|
|
18996
19755
|
snapshotter,
|
|
18997
19756
|
comments,
|
|
18998
|
-
watcher: null
|
|
19757
|
+
watcher: null,
|
|
19758
|
+
moved: null,
|
|
19759
|
+
branches
|
|
18999
19760
|
};
|
|
19000
19761
|
await this.relocateComments(next, snap);
|
|
19001
19762
|
if (this.opts.watch && mode.live !== "none") next.watcher = await this.startWatcher(next);
|
|
@@ -19025,13 +19786,71 @@ var Session = class {
|
|
|
19025
19786
|
if (this.queuedRefresh) return this.queuedRefresh;
|
|
19026
19787
|
const r = this.run(async () => {
|
|
19027
19788
|
this.queuedRefresh = null;
|
|
19028
|
-
|
|
19029
|
-
a.snapshotter.invalidate(++this.version);
|
|
19030
|
-
await this.publish(a);
|
|
19789
|
+
await this.recompute();
|
|
19031
19790
|
}).catch((e) => console.error("[diffle] refresh failed:", e));
|
|
19032
19791
|
this.queuedRefresh = r;
|
|
19033
19792
|
return r;
|
|
19034
19793
|
}
|
|
19794
|
+
/** The reviewer asks for the moved refs: recompute now. Unlike `refresh`, a failure is the caller's. */
|
|
19795
|
+
reload() {
|
|
19796
|
+
return this.run(async () => {
|
|
19797
|
+
const a = this.require();
|
|
19798
|
+
if (a.mode.within && shownIterations(a.mode)) return this.leaveInterdiff(a);
|
|
19799
|
+
return this.recompute();
|
|
19800
|
+
});
|
|
19801
|
+
}
|
|
19802
|
+
/** Forgets the range's recorded iterations and unpins their commits; its current state is recorded anew as #1. */
|
|
19803
|
+
clearIterations() {
|
|
19804
|
+
return this.run(async () => {
|
|
19805
|
+
const a = this.require();
|
|
19806
|
+
const range = a.mode.within ?? a.mode;
|
|
19807
|
+
await new IterationStore(this.repo, range.commentKey).clear();
|
|
19808
|
+
return shownIterations(a.mode) ? this.leaveInterdiff(a) : this.recompute();
|
|
19809
|
+
});
|
|
19810
|
+
}
|
|
19811
|
+
/**
|
|
19812
|
+
* Forgets one recorded iteration and unpins its commits. The latest stays: it is the range's
|
|
19813
|
+
* current state and would be recorded again at once.
|
|
19814
|
+
*/
|
|
19815
|
+
deleteIteration(n) {
|
|
19816
|
+
return this.run(async () => {
|
|
19817
|
+
const a = this.require();
|
|
19818
|
+
const range = a.mode.within ?? a.mode;
|
|
19819
|
+
const store = new IterationStore(this.repo, range.commentKey);
|
|
19820
|
+
const list = await store.list();
|
|
19821
|
+
if (!list.some((it) => it.n === n)) throw new RevspecError(`no iteration #${n}`);
|
|
19822
|
+
if (list.at(-1).n === n) throw new RevspecError(`#${n} is the latest iteration, the range's current state`);
|
|
19823
|
+
await store.remove(n);
|
|
19824
|
+
const shown = shownIterations(a.mode);
|
|
19825
|
+
return shown && (shown.from === n || shown.to === n) ? this.leaveInterdiff(a) : this.recompute();
|
|
19826
|
+
});
|
|
19827
|
+
}
|
|
19828
|
+
leaveInterdiff(a) {
|
|
19829
|
+
return this.enter({
|
|
19830
|
+
mode: a.mode.within,
|
|
19831
|
+
prUrl: a.prUrl
|
|
19832
|
+
});
|
|
19833
|
+
}
|
|
19834
|
+
/** Activates a review in place of the current one, as a switch does, and tells clients. */
|
|
19835
|
+
async enter(review) {
|
|
19836
|
+
const snap = await this.activate(review);
|
|
19837
|
+
this.hub.broadcast({
|
|
19838
|
+
type: "snapshot",
|
|
19839
|
+
version: snap.version
|
|
19840
|
+
});
|
|
19841
|
+
return snap;
|
|
19842
|
+
}
|
|
19843
|
+
/**
|
|
19844
|
+
* Recomputes the active snapshot. A range-diff of two named ranges resolves them again
|
|
19845
|
+
* instead, since its sides are pinned trees: that is how it follows the refs it was named by.
|
|
19846
|
+
*/
|
|
19847
|
+
recompute() {
|
|
19848
|
+
const a = this.require();
|
|
19849
|
+
const inter = a.mode.interdiff;
|
|
19850
|
+
if (inter && !shownIterations(a.mode) && !a.mode.pair) return rangeDiffReview(this.repo, [inter.from, inter.to], a.prUrl).then((review) => this.enter(review));
|
|
19851
|
+
a.snapshotter.invalidate(++this.version);
|
|
19852
|
+
return this.publish(a);
|
|
19853
|
+
}
|
|
19035
19854
|
/**
|
|
19036
19855
|
* Computes `a`'s current snapshot, re-anchors its threads (hunks may have
|
|
19037
19856
|
* grown, shrunk or moved), then broadcasts and announces it.
|
|
@@ -19039,11 +19858,40 @@ var Session = class {
|
|
|
19039
19858
|
async publish(a) {
|
|
19040
19859
|
const snap = await a.snapshotter.current();
|
|
19041
19860
|
await this.relocateComments(a, snap);
|
|
19861
|
+
a.moved = null;
|
|
19042
19862
|
this.hub.broadcast({
|
|
19043
19863
|
type: "snapshot",
|
|
19044
19864
|
version: snap.version
|
|
19045
19865
|
});
|
|
19046
19866
|
this.announce(snap);
|
|
19867
|
+
return snap;
|
|
19868
|
+
}
|
|
19869
|
+
/**
|
|
19870
|
+
* Resolves the refs the live comparison follows and tells clients when they
|
|
19871
|
+
* left the snapshot behind, or came back to it. At most one check waits
|
|
19872
|
+
* behind the running one, as with `refresh`.
|
|
19873
|
+
*/
|
|
19874
|
+
checkMoved(a) {
|
|
19875
|
+
if (this.queuedCheck) return this.queuedCheck;
|
|
19876
|
+
const r = this.run(async () => {
|
|
19877
|
+
this.queuedCheck = null;
|
|
19878
|
+
if (this.active !== a) return;
|
|
19879
|
+
const snap = await a.snapshotter.current();
|
|
19880
|
+
const { oldSha, newSha } = await resolveComparison(this.repo, a.mode.within ?? a.mode);
|
|
19881
|
+
const moved = oldSha === snap.commits.oldSha && newSha === snap.commits.newSha ? null : {
|
|
19882
|
+
version: snap.version,
|
|
19883
|
+
oldSha,
|
|
19884
|
+
newSha
|
|
19885
|
+
};
|
|
19886
|
+
if (moved?.oldSha === a.moved?.oldSha && moved?.newSha === a.moved?.newSha) return;
|
|
19887
|
+
a.moved = moved;
|
|
19888
|
+
this.hub.broadcast({
|
|
19889
|
+
type: "moved",
|
|
19890
|
+
moved
|
|
19891
|
+
});
|
|
19892
|
+
}).catch((e) => console.error("[diffle] refs check failed:", e));
|
|
19893
|
+
this.queuedCheck = r;
|
|
19894
|
+
return r;
|
|
19047
19895
|
}
|
|
19048
19896
|
/**
|
|
19049
19897
|
* Re-anchors threads against what `snap` shows. A changed file shows its
|
|
@@ -19107,6 +19955,7 @@ var Session = class {
|
|
|
19107
19955
|
* review. Goes through `readSide`, so the allowlist applies.
|
|
19108
19956
|
*/
|
|
19109
19957
|
anchorSource() {
|
|
19958
|
+
if (this.mode.interdiff) throw new RevspecError(shownIterations(this.mode) ? "comments are off while comparing iterations" : "comments are off in a range-diff");
|
|
19110
19959
|
return {
|
|
19111
19960
|
quote: async (path, side, startLine, endLine) => {
|
|
19112
19961
|
const snap = await this.snapshotter.current();
|
|
@@ -19147,6 +19996,10 @@ var Session = class {
|
|
|
19147
19996
|
});
|
|
19148
19997
|
watcher.on("dirty", () => {
|
|
19149
19998
|
if (this.active !== a) return;
|
|
19999
|
+
if (!this.follows) {
|
|
20000
|
+
this.checkMoved(a);
|
|
20001
|
+
return;
|
|
20002
|
+
}
|
|
19150
20003
|
this.refresh();
|
|
19151
20004
|
this.run(async () => {
|
|
19152
20005
|
if (this.active === a && a.mode.live === "worktree") await refreshIgnored();
|
|
@@ -19237,6 +20090,7 @@ function normalize(c) {
|
|
|
19237
20090
|
return {
|
|
19238
20091
|
autoViewed: Array.isArray(c.autoViewed) ? c.autoViewed.filter((p) => typeof p === "string").map((p) => p.trim()).filter(Boolean) : DEFAULT_USER_CONFIG.autoViewed,
|
|
19239
20092
|
contextLines: typeof c.contextLines === "number" && Number.isFinite(c.contextLines) ? Math.max(0, Math.min(1e4, Math.floor(c.contextLines))) : DEFAULT_USER_CONFIG.contextLines,
|
|
20093
|
+
followRefs: FollowRefsSchema.safeParse(c.followRefs).data ?? DEFAULT_USER_CONFIG.followRefs,
|
|
19240
20094
|
lspCommands: lspCommands(c)
|
|
19241
20095
|
};
|
|
19242
20096
|
}
|
|
@@ -22944,6 +23798,12 @@ function parseLspOverride(raw) {
|
|
|
22944
23798
|
function collectLspOverride(raw, prev) {
|
|
22945
23799
|
return [...Array.isArray(prev) ? prev : [], parseLspOverride(raw)];
|
|
22946
23800
|
}
|
|
23801
|
+
/** A `set-follow-refs` value. */
|
|
23802
|
+
function parseFollowRefs(value) {
|
|
23803
|
+
const parsed = FollowRefsSchema.safeParse(value);
|
|
23804
|
+
if (!parsed.success) throw new InvalidArgumentError("expected on, auto or off");
|
|
23805
|
+
return parsed.data;
|
|
23806
|
+
}
|
|
22947
23807
|
//#endregion
|
|
22948
23808
|
//#region src/cli/browserLifetime.ts
|
|
22949
23809
|
/**
|
|
@@ -23089,13 +23949,19 @@ const c = {
|
|
|
23089
23949
|
};
|
|
23090
23950
|
const program = new Command().exitOverride().name("diffle").description("Review a git diff in the browser and export line comments as an agent prompt.").version(version$1, "-v, --version", "print the version and exit").addOption(completionHint(new Option("-C <path>", "run as if started in <path> (any directory inside a git worktree)"), { kind: "directory" })).option("-p, --port <port>", `port to listen on (default: ${DEFAULT_PORT}, or the next free one; 0 = random)`, parsePort).option("-H, --host <host>", "address to bind; use 0.0.0.0 to expose on the network", "127.0.0.1").option("--allowed-origin <origin>", "trusted public HTTP(S) origin behind a reverse proxy (without a path)", parseAllowedOrigin).option("--no-open", "do not open a browser").option("--keep-alive", "keep the server running after all browser tabs close").option("--no-watch", "do not watch for changes").addOption(new Option("--auto-viewed <glob>", "mark matching files viewed for this session (repeatable)").argParser(collect).default([], "none")).option("-U, --context <n>", "context lines around changes for this session (default: config, 5)", parseContext).addOption(new Option("--lsp <language=command>", "language server command for one language, started for this run (repeatable); by default the diff's languages are served by whatever is on PATH").argParser(collectLspOverride).default(true, "the diff's languages, resolved on PATH")).option("--no-lsp", "do not start any language server").option("--timing", "print startup phase timings to stderr").option("--dev", "serve the client through Vite (development)", process.env.DIFFLE_DEV === "1" ? true : void 0).addArgument(completionHint(new Argument("[revs...]", "git-diff style revisions: <rev> | <a>..<b> | <a>...<b> | <a> <b>"), {
|
|
23091
23951
|
kind: "choices",
|
|
23092
|
-
values: [
|
|
23952
|
+
values: [
|
|
23953
|
+
"working",
|
|
23954
|
+
"pr",
|
|
23955
|
+
"range-diff"
|
|
23956
|
+
]
|
|
23093
23957
|
})).addHelpText("after", `
|
|
23094
23958
|
Shorthands (in place of <revs>):
|
|
23095
23959
|
working same as HEAD..worktree: uncommitted changes, staged and untracked included
|
|
23096
23960
|
pr [number|url] a GitHub pull request: its base...head, fetched if needed
|
|
23097
23961
|
(without an argument: the pull request for this branch)
|
|
23098
23962
|
show [rev] same as <rev>^!: one commit against its first parent, pinned (default: HEAD)
|
|
23963
|
+
range-diff <ranges...> how one series of commits became another, like git range-diff:
|
|
23964
|
+
<a>..<b> <c>..<d> | <base> <b> <d> | <b>...<d>
|
|
23099
23965
|
|
|
23100
23966
|
Revisions follow git diff, except that a lone one compares from the merge base:
|
|
23101
23967
|
diffle main same as main...HEAD: what this branch added since it left main
|
|
@@ -23124,6 +23990,10 @@ program.command("pr", { hidden: true }).argument("[pr]", "pull request number or
|
|
|
23124
23990
|
kind: "pr",
|
|
23125
23991
|
pr
|
|
23126
23992
|
}, cmd.optsWithGlobals()));
|
|
23993
|
+
program.command("range-diff", { hidden: true }).argument("<ranges...>", "two ranges: <a>..<b> <c>..<d>, <base> <b> <d>, or <b>...<d>").summary("two series of commits").description("How one series of commits became another, as git range-diff shows it: the older range replayed onto the newer one’s base against the newer range, with the commits paired.").action(async (ranges, _o, cmd) => run({
|
|
23994
|
+
kind: "range-diff",
|
|
23995
|
+
args: ranges
|
|
23996
|
+
}, cmd.optsWithGlobals()));
|
|
23127
23997
|
const config = program.command("config").description("show or edit the user config (same settings as the UI dialog)");
|
|
23128
23998
|
config.command("show", { isDefault: true }).description("print the config file path and contents").action(async () => {
|
|
23129
23999
|
const store = await UserConfigStore.open();
|
|
@@ -23140,6 +24010,11 @@ config.command("set-context").description("set context lines around changes").ar
|
|
|
23140
24010
|
await store.set({ contextLines: n });
|
|
23141
24011
|
console.log(store.get().contextLines);
|
|
23142
24012
|
});
|
|
24013
|
+
config.command("set-follow-refs").description("follow moved refs: on, auto (not branches), off").argument("<mode>", "on: reload at once; auto: so unless a branch is compared; off: offer a Reload button", parseFollowRefs).action(async (followRefs) => {
|
|
24014
|
+
const store = await UserConfigStore.open();
|
|
24015
|
+
await store.set({ followRefs });
|
|
24016
|
+
console.log(store.get().followRefs);
|
|
24017
|
+
});
|
|
23143
24018
|
config.command("set-lsp").description("set the language-server command for one language").addArgument(languageArgument("<language>", `one of: ${LANGUAGE_IDS.join(", ")}`).argParser(parseLanguage)).argument("<command>", "shell command that starts a stdio language server, e.g. \"pyrefly lsp\"; \"\" to turn it off").action(async (language, command) => {
|
|
23144
24019
|
const store = await UserConfigStore.open();
|
|
23145
24020
|
await store.set({ lspCommands: {
|
|
@@ -23215,6 +24090,7 @@ async function serve(req, opts, repo, closeRepo, config, github, timing) {
|
|
|
23215
24090
|
const session = new Session(repo, hub, {
|
|
23216
24091
|
watch: opts.watch,
|
|
23217
24092
|
context: opts.context ?? config.get().contextLines,
|
|
24093
|
+
followRefs: config.get().followRefs,
|
|
23218
24094
|
github: github.client ?? void 0
|
|
23219
24095
|
});
|
|
23220
24096
|
const named = Array.isArray(opts.lsp) ? opts.lsp : [];
|
|
@@ -23277,7 +24153,7 @@ async function serve(req, opts, repo, closeRepo, config, github, timing) {
|
|
|
23277
24153
|
console.error(`🔍 ${c.dim("comparing")} ${c.bold(comparisonLabel(snap.mode))}`);
|
|
23278
24154
|
if (n === 0) console.error(`${c.yellow("!")} No differences. Open any file from the tree to comment on it.`);
|
|
23279
24155
|
else console.error(`📝 ${n} changed file${n === 1 ? "" : "s"} ${c.green(`+${adds}`)} ${c.red(`−${dels}`)}`);
|
|
23280
|
-
if (snap.mode.live !== "none") console.error(`👀 ${c.dim(snap.mode.live === "worktree" ? "watching the worktree" : "watching refs")}${opts.watch ? "" : c.dim(" (disabled with --no-watch)")}`);
|
|
24156
|
+
if (snap.mode.live !== "none") console.error(`👀 ${c.dim(snap.mode.live === "worktree" ? "watching the worktree" : session.follows ? "watching refs" : "watching refs; moved refs show a Reload button")}${opts.watch ? "" : c.dim(" (disabled with --no-watch)")}`);
|
|
23281
24157
|
if (lsp) {
|
|
23282
24158
|
const { servers, missing } = lsp.status();
|
|
23283
24159
|
const names = servers.map((s) => `${s.name} ${c.dim(`(${s.languages.join(", ")})`)}`).join(", ");
|