@moritzwilksch/diffle 0.1.4 → 0.1.5
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 +21 -5
- package/dist/client/assets/{angular-html-DCs801AK.js → angular-html-DmrG9JtS.js} +1 -1
- package/dist/client/assets/{angular-ts-BWhN0w8_.js → angular-ts-ViWJivwt.js} +1 -1
- package/dist/client/assets/{apl-Djz0ErNW.js → apl-C5EQbFuR.js} +1 -1
- package/dist/client/assets/{astro-MWC3rLUN.js → astro-Dn81Z0Ad.js} +1 -1
- package/dist/client/assets/{blade-B8pycf1w.js → blade-Bw99tw7z.js} +1 -1
- package/dist/client/assets/{c-B-Jcxs--.js → c-BUkR06RC.js} +1 -1
- package/dist/client/assets/{chapel-CuJCeKLf.js → chapel-BkX4HVjh.js} +1 -1
- package/dist/client/assets/{cobol-DgquORVs.js → cobol-DHYNHdYd.js} +1 -1
- package/dist/client/assets/{coffee-ECOaIhT5.js → coffee-CwXH1Vh3.js} +1 -1
- package/dist/client/assets/{cpp-CqnSivHO.js → cpp-CH4TrKz5.js} +1 -1
- package/dist/client/assets/{crystal-Cs7ZYBfN.js → crystal-DKLVzvRx.js} +1 -1
- package/dist/client/assets/{css-p0qk9Lci.js → css-Nq8Uu_QZ.js} +1 -1
- package/dist/client/assets/{edge-mMULBvUs.js → edge-CJRhOJkj.js} +1 -1
- package/dist/client/assets/{elixir-VIosceup.js → elixir-B6z04L_L.js} +1 -1
- package/dist/client/assets/{elm-C3kw87lA.js → elm-CylqdfmR.js} +1 -1
- package/dist/client/assets/{erb-DZjmloke.js → erb--LFUnTAk.js} +1 -1
- package/dist/client/assets/{git-rebase-qAMhScBw.js → git-rebase-CJezoI_U.js} +1 -1
- package/dist/client/assets/{glimmer-js-DJdA7CEB.js → glimmer-js-CVs-5jG6.js} +1 -1
- package/dist/client/assets/{glimmer-ts-6xtV4JQb.js → glimmer-ts-C8GkuLHj.js} +1 -1
- package/dist/client/assets/{glsl-BRqSJ3dN.js → glsl-Cre-e57h.js} +1 -1
- package/dist/client/assets/{graphql-nm_X3Hg7.js → graphql-Bflp-kge.js} +1 -1
- package/dist/client/assets/{hack-CQ7j-SLC.js → hack-BVXbQcT2.js} +1 -1
- package/dist/client/assets/{haml-nw30ZCFe.js → haml-De4W5AvJ.js} +1 -1
- package/dist/client/assets/{handlebars-CbbUPaYB.js → handlebars-DrAV6XbB.js} +1 -1
- package/dist/client/assets/{html-C-E44L0y.js → html-DWrE5ZtW.js} +1 -1
- package/dist/client/assets/{html-derivative-DTCnXXkR.js → html-derivative-4y8Ldk_X.js} +1 -1
- package/dist/client/assets/{http-Cg4Jw-kz.js → http-BlF3sH0P.js} +1 -1
- package/dist/client/assets/{hurl-CITbwYVL.js → hurl-8Yd7qaL-.js} +1 -1
- package/dist/client/assets/{index-BdA8MWLx.js → index-B5j0g-YF.js} +90 -90
- package/dist/client/assets/{java-DNT_EPVr.js → java-Da6TVA-D.js} +1 -1
- package/dist/client/assets/{javascript-DBrMK690.js → javascript-9H4TWodB.js} +1 -1
- package/dist/client/assets/{jinja-DqPnwJo-.js → jinja-CB5h6IWK.js} +1 -1
- package/dist/client/assets/{jison-A5ieRkCJ.js → jison-DdpPjkKm.js} +1 -1
- package/dist/client/assets/{json-BolWLdCd.js → json-lKrYnKUy.js} +1 -1
- package/dist/client/assets/{jsx-CkIysgAJ.js → jsx-Cq_s8bb1.js} +1 -1
- package/dist/client/assets/{julia-jBzm267c.js → julia-B2fUm1la.js} +1 -1
- package/dist/client/assets/{just-cr8UASbW.js → just-EU9poDOS.js} +1 -1
- package/dist/client/assets/{latex-DcdC0vak.js → latex-D94S8ANd.js} +1 -1
- package/dist/client/assets/{liquid-BBR6eAOA.js → liquid-Dq3Y6XOW.js} +1 -1
- package/dist/client/assets/{lua-CnUVAA9w.js → lua-Dl3Y8EmE.js} +1 -1
- package/dist/client/assets/{marko-BxH5DxB_.js → marko-BUvlzVCc.js} +1 -1
- package/dist/client/assets/{mdc-CVc0_tcy.js → mdc-5MteepiP.js} +1 -1
- package/dist/client/assets/{nginx-B1DQmN_N.js → nginx-DPAEohPR.js} +1 -1
- package/dist/client/assets/{nim-C3rsdIhh.js → nim-Dy-zxDpe.js} +1 -1
- package/dist/client/assets/{org-DICsbmv1.js → org-a4b5Lf2G.js} +1 -1
- package/dist/client/assets/{perl-B-6tgIaz.js → perl-Ba0EQGS8.js} +1 -1
- package/dist/client/assets/{php-CSZf1YiM.js → php-BwWPnF9y.js} +1 -1
- package/dist/client/assets/{pug-BvQ97HKe.js → pug-DiduM0zv.js} +1 -1
- package/dist/client/assets/{qml-B_gSWBeZ.js → qml-Zm_NS-qG.js} +1 -1
- package/dist/client/assets/{r-Dbh43Ti8.js → r-Dq0UKcOZ.js} +1 -1
- package/dist/client/assets/{razor-Dks3_W39.js → razor-BOvDDN47.js} +1 -1
- package/dist/client/assets/{regexp-cZE9On7Z.js → regexp-D3YxDQ6s.js} +1 -1
- package/dist/client/assets/{rst-Cgn-1Zks.js → rst-DHbzSb6z.js} +1 -1
- package/dist/client/assets/{ruby-DkGM-R-6.js → ruby-CvI-mn5Q.js} +1 -1
- package/dist/client/assets/{sas-D6c_PGbn.js → sas-CWQFfwiE.js} +1 -1
- package/dist/client/assets/{scss-BKxWJNFh.js → scss-CCpN0AyS.js} +1 -1
- package/dist/client/assets/{shellscript-Bcg5lXvK.js → shellscript-BfpoO4g6.js} +1 -1
- package/dist/client/assets/{shellsession-Bf400BZo.js → shellsession-agjx723h.js} +1 -1
- package/dist/client/assets/{soy-VDZj_scj.js → soy-pEVWulWp.js} +1 -1
- package/dist/client/assets/{sql-ushyJdRf.js → sql-DS0UAPd6.js} +1 -1
- package/dist/client/assets/{stata-87RYcwm-.js → stata-D8L-Tcxs.js} +1 -1
- package/dist/client/assets/{surrealql-DiJkNDgM.js → surrealql-DOkpssDf.js} +1 -1
- package/dist/client/assets/{svelte-B6zEQCqX.js → svelte-B4ieXAtz.js} +1 -1
- package/dist/client/assets/{templ-BbUkdxgW.js → templ-BdVEAAo4.js} +1 -1
- package/dist/client/assets/{tex-8DUlt0ja.js → tex-CYjNIefm.js} +1 -1
- package/dist/client/assets/{ts-tags-B9TjJt8W.js → ts-tags-BFD-4qTD.js} +1 -1
- package/dist/client/assets/{tsx-KtVv-1tC.js → tsx-CZLDU0bf.js} +1 -1
- package/dist/client/assets/{twig-SRZcBkws.js → twig-DYcKmD5x.js} +1 -1
- package/dist/client/assets/{typescript-XxBZhs1X.js → typescript-d_JvksIp.js} +1 -1
- package/dist/client/assets/{typst-zdrwuFs3.js → typst-B0Uo-CKu.js} +1 -1
- package/dist/client/assets/{vue-CIJzu_ou.js → vue-B3Iiop2I.js} +1 -1
- package/dist/client/assets/{vue-html-Cbh4Xclf.js → vue-html-B9s0WJG6.js} +1 -1
- package/dist/client/assets/{vue-vine-B3rP3Gia.js → vue-vine-CIMF412S.js} +1 -1
- package/dist/client/assets/{xml-B8FuGGpH.js → xml-BzXG-icR.js} +1 -1
- package/dist/client/assets/{xsl-DGb4ZSQs.js → xsl-Dp1sXze8.js} +1 -1
- package/dist/client/assets/{yaml-DgFlGzOM.js → yaml-BW2dXlES.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/{Watcher-C_X03LRa.js → Watcher-CKwvGuPB.js} +5 -4
- package/dist/server/main.js +291 -120
- package/package.json +1 -1
package/dist/server/main.js
CHANGED
|
@@ -3951,7 +3951,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
|
|
|
3951
3951
|
}
|
|
3952
3952
|
//#endregion
|
|
3953
3953
|
//#region package.json
|
|
3954
|
-
var version$1 = "0.1.
|
|
3954
|
+
var version$1 = "0.1.5";
|
|
3955
3955
|
//#endregion
|
|
3956
3956
|
//#region src/server/comments/anchor.ts
|
|
3957
3957
|
function splitLines(contents) {
|
|
@@ -3996,10 +3996,15 @@ function quoteRange(contents, startLine, endLine) {
|
|
|
3996
3996
|
if (startLine < 1 || endLine < startLine || endLine > lines.length) return null;
|
|
3997
3997
|
return lines.slice(startLine - 1, endLine).join("\n");
|
|
3998
3998
|
}
|
|
3999
|
-
/**
|
|
3999
|
+
/** The line a thread sorts under: a file thread comes before every line of its file. */
|
|
4000
|
+
function anchorLine(anchor) {
|
|
4001
|
+
return anchor.kind === "line" ? anchor.startLine : 0;
|
|
4002
|
+
}
|
|
4003
|
+
/** Review order: by path, then line (file threads first), then when the thread was opened. */
|
|
4000
4004
|
function compareThreads(a, b) {
|
|
4001
4005
|
if (a.anchor.path !== b.anchor.path) return a.anchor.path < b.anchor.path ? -1 : 1;
|
|
4002
|
-
|
|
4006
|
+
const line = anchorLine(a.anchor) - anchorLine(b.anchor);
|
|
4007
|
+
if (line !== 0) return line;
|
|
4003
4008
|
return (a.messages[0]?.createdAt ?? 0) - (b.messages[0]?.createdAt ?? 0);
|
|
4004
4009
|
}
|
|
4005
4010
|
//#endregion
|
|
@@ -4007,21 +4012,23 @@ function compareThreads(a, b) {
|
|
|
4007
4012
|
/**
|
|
4008
4013
|
* Agent-prompt format, one block per thread:
|
|
4009
4014
|
*
|
|
4010
|
-
* path:line (or path:start-end)
|
|
4015
|
+
* path:line (or path:start-end; `(file) path` for a thread on the whole file)
|
|
4011
4016
|
*
|
|
4012
|
-
* > quoted line
|
|
4017
|
+
* > quoted line (absent for a file thread)
|
|
4013
4018
|
*
|
|
4014
4019
|
* body (a thread's messages joined by a blank line)
|
|
4015
4020
|
*
|
|
4016
4021
|
* ---
|
|
4017
4022
|
*
|
|
4018
|
-
* A ```suggestion fence in a body expands to ORIGINAL / SUGGESTED fences
|
|
4023
|
+
* A ```suggestion fence in a body expands to ORIGINAL / SUGGESTED fences; a file
|
|
4024
|
+
* thread has no original, so its fences stay as written.
|
|
4019
4025
|
*/
|
|
4020
4026
|
function formatPrompt(threads) {
|
|
4021
4027
|
return [...threads].sort(compareThreads).map(formatOne).join("\n");
|
|
4022
4028
|
}
|
|
4023
4029
|
function formatOne(t) {
|
|
4024
4030
|
const { anchor } = t;
|
|
4031
|
+
if (anchor.kind === "file") return `${`(file) ${t.stale ? "(stale) " : ""}${anchor.path}`}\n\n${t.messages.map((m) => m.body.trim()).join("\n\n")}\n\n---\n`;
|
|
4025
4032
|
const range = anchor.startLine === anchor.endLine ? `${anchor.startLine}` : `${anchor.startLine}-${anchor.endLine}`;
|
|
4026
4033
|
const prefixes = [];
|
|
4027
4034
|
if (anchor.side === "old") prefixes.push("(removed)");
|
|
@@ -4720,6 +4727,31 @@ var GitRepo = class GitRepo {
|
|
|
4720
4727
|
await fh?.close();
|
|
4721
4728
|
}
|
|
4722
4729
|
}
|
|
4730
|
+
/**
|
|
4731
|
+
* Boolean value of a gitattribute per path: `attr` or `attr=true` reads
|
|
4732
|
+
* true, `-attr` or `attr=false` false; unspecified paths are absent. A
|
|
4733
|
+
* revision reads `.gitattributes` from that tree, the worktree from the
|
|
4734
|
+
* checkout. `--source` needs git 2.40.
|
|
4735
|
+
*/
|
|
4736
|
+
async attr(name, paths, rev) {
|
|
4737
|
+
const out = /* @__PURE__ */ new Map();
|
|
4738
|
+
if (paths.length === 0) return out;
|
|
4739
|
+
const source = rev === "worktree" ? [] : [`--source=${rev}`];
|
|
4740
|
+
const fields = splitZ(await this.exec([
|
|
4741
|
+
"check-attr",
|
|
4742
|
+
"-z",
|
|
4743
|
+
"--stdin",
|
|
4744
|
+
...source,
|
|
4745
|
+
name,
|
|
4746
|
+
"--"
|
|
4747
|
+
], { input: paths.join("\0") }));
|
|
4748
|
+
for (let i = 0; i + 2 < fields.length; i += 3) {
|
|
4749
|
+
const value = fields[i + 2];
|
|
4750
|
+
if (value === "set" || value === "true") out.set(fields[i], true);
|
|
4751
|
+
else if (value === "unset" || value === "false") out.set(fields[i], false);
|
|
4752
|
+
}
|
|
4753
|
+
return out;
|
|
4754
|
+
}
|
|
4723
4755
|
/** Paths git ignores, from a candidate list. */
|
|
4724
4756
|
async ignored(paths) {
|
|
4725
4757
|
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -5076,11 +5108,15 @@ var GithubError = class extends Error {
|
|
|
5076
5108
|
this.status = status;
|
|
5077
5109
|
}
|
|
5078
5110
|
};
|
|
5111
|
+
function isFileComment(c) {
|
|
5112
|
+
return "subject_type" in c;
|
|
5113
|
+
}
|
|
5079
5114
|
/**
|
|
5080
5115
|
* Threads → the comments of one pending GitHub review, one per thread. Stale threads are skipped:
|
|
5081
5116
|
* their lines no longer sit in the diff, and GitHub would refuse them anyway. With
|
|
5082
5117
|
* `threadIds`, only those (resolved included, the user asked for them by hand);
|
|
5083
|
-
* without, every unresolved thread.
|
|
5118
|
+
* without, every unresolved thread. `review.comments` holds the line comments REST can
|
|
5119
|
+
* create; the file comments are appended afterwards, but `ids` counts both, in review order.
|
|
5084
5120
|
*/
|
|
5085
5121
|
function buildReview(threads, commitId, threadIds) {
|
|
5086
5122
|
const skipped = [];
|
|
@@ -5113,20 +5149,27 @@ function buildReview(threads, commitId, threadIds) {
|
|
|
5113
5149
|
return {
|
|
5114
5150
|
review: {
|
|
5115
5151
|
commit_id: commitId,
|
|
5116
|
-
comments
|
|
5152
|
+
comments: comments.filter((c) => !isFileComment(c))
|
|
5117
5153
|
},
|
|
5154
|
+
comments,
|
|
5118
5155
|
ids,
|
|
5119
5156
|
skipped
|
|
5120
5157
|
};
|
|
5121
5158
|
}
|
|
5122
5159
|
function toReviewComment(t) {
|
|
5123
5160
|
const { anchor } = t;
|
|
5161
|
+
const body = formatBody(t.messages);
|
|
5162
|
+
if (anchor.kind === "file") return {
|
|
5163
|
+
path: anchor.path,
|
|
5164
|
+
subject_type: "file",
|
|
5165
|
+
body
|
|
5166
|
+
};
|
|
5124
5167
|
const side = anchor.side === "old" ? "LEFT" : "RIGHT";
|
|
5125
5168
|
const c = {
|
|
5126
5169
|
path: anchor.path,
|
|
5127
5170
|
line: anchor.endLine,
|
|
5128
5171
|
side,
|
|
5129
|
-
body
|
|
5172
|
+
body
|
|
5130
5173
|
};
|
|
5131
5174
|
if (anchor.startLine !== anchor.endLine) {
|
|
5132
5175
|
c.start_line = anchor.startLine;
|
|
@@ -5171,15 +5214,25 @@ var GithubExporter = class {
|
|
|
5171
5214
|
*/
|
|
5172
5215
|
async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, run = runGh }) {
|
|
5173
5216
|
if (snap.newSha === "worktree") throw new GithubError("GitHub cannot anchor comments to the worktree");
|
|
5174
|
-
const { review, ids, skipped } = buildReview(threads, snap.newSha, threadIds);
|
|
5175
|
-
if (
|
|
5176
|
-
for (const [i, comment] of
|
|
5217
|
+
const { review, comments, ids, skipped } = buildReview(threads, snap.newSha, threadIds);
|
|
5218
|
+
if (comments.length === 0) throw new GithubError(skipped.length ? `nothing to post: ${describe(skipped)}` : "nothing to post", 400);
|
|
5219
|
+
for (const [i, comment] of comments.entries()) comment.body += `\n\n${threadMarker(ids[i])}`;
|
|
5177
5220
|
const pending = await findPendingReview(run, snap.root, pr);
|
|
5178
5221
|
if (!pending) {
|
|
5179
|
-
await createPendingReview(run, snap.root, pr.repository, pr.number, review);
|
|
5222
|
+
const created = await createPendingReview(run, snap.root, pr.repository, pr.number, review);
|
|
5223
|
+
let appended = 0;
|
|
5224
|
+
try {
|
|
5225
|
+
for (const c of comments.filter(isFileComment)) {
|
|
5226
|
+
await addToPendingReview(run, snap.root, created.id, c);
|
|
5227
|
+
appended++;
|
|
5228
|
+
}
|
|
5229
|
+
} catch (e) {
|
|
5230
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
5231
|
+
throw new GithubError(`created the pending review with ${review.comments.length + appended} comments, then ${detail}`, 502);
|
|
5232
|
+
}
|
|
5180
5233
|
return {
|
|
5181
5234
|
url: pr.url,
|
|
5182
|
-
posted:
|
|
5235
|
+
posted: comments.length,
|
|
5183
5236
|
updated: 0,
|
|
5184
5237
|
review: "created",
|
|
5185
5238
|
skipped
|
|
@@ -5190,7 +5243,7 @@ async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, run =
|
|
|
5190
5243
|
let updated = 0;
|
|
5191
5244
|
let posted = 0;
|
|
5192
5245
|
try {
|
|
5193
|
-
for (const [i, c] of
|
|
5246
|
+
for (const [i, c] of comments.entries()) {
|
|
5194
5247
|
const id = ids[i];
|
|
5195
5248
|
const at = existing.get(anchorKey(c));
|
|
5196
5249
|
const index = at?.findIndex((comment) => comment.body.trimEnd().endsWith(threadMarker(id))) ?? -1;
|
|
@@ -5234,6 +5287,7 @@ function threadMarker(id) {
|
|
|
5234
5287
|
}
|
|
5235
5288
|
/** The anchor must still match before we rewrite an exported thread. */
|
|
5236
5289
|
function anchorKey(c) {
|
|
5290
|
+
if (isFileComment(c)) return [c.path, "FILE"].join("\0");
|
|
5237
5291
|
return [
|
|
5238
5292
|
c.path,
|
|
5239
5293
|
c.side,
|
|
@@ -5264,7 +5318,7 @@ async function findPendingReview(run, cwd, pr) {
|
|
|
5264
5318
|
const THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){
|
|
5265
5319
|
repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){
|
|
5266
5320
|
pageInfo{hasNextPage endCursor}
|
|
5267
|
-
nodes{path line startLine diffSide comments(first:1){nodes{id body pullRequestReview{id}}}}
|
|
5321
|
+
nodes{path line startLine diffSide subjectType comments(first:1){nodes{id body pullRequestReview{id}}}}
|
|
5268
5322
|
}}}
|
|
5269
5323
|
}`;
|
|
5270
5324
|
/**
|
|
@@ -5288,15 +5342,8 @@ async function pendingComments(run, cwd, pr, reviewId) {
|
|
|
5288
5342
|
for (const t of threads.nodes) {
|
|
5289
5343
|
const c = t?.comments?.nodes?.[0];
|
|
5290
5344
|
if (!t?.path || !c?.id || c.pullRequestReview?.id !== reviewId) continue;
|
|
5291
|
-
const
|
|
5292
|
-
if (
|
|
5293
|
-
const side = t.diffSide === "LEFT" ? "LEFT" : "RIGHT";
|
|
5294
|
-
const key = anchorKey({
|
|
5295
|
-
path: t.path,
|
|
5296
|
-
side,
|
|
5297
|
-
line,
|
|
5298
|
-
start_line: t.startLine ?? void 0
|
|
5299
|
-
});
|
|
5345
|
+
const key = pendingKey(t);
|
|
5346
|
+
if (key == null) continue;
|
|
5300
5347
|
const at = by.get(key);
|
|
5301
5348
|
if (at) at.push({
|
|
5302
5349
|
nodeId: c.id,
|
|
@@ -5313,10 +5360,30 @@ async function pendingComments(run, cwd, pr, reviewId) {
|
|
|
5313
5360
|
}
|
|
5314
5361
|
throw new GithubError("pending review exceeds the 1000-thread lookup limit; export stopped without changes", 502);
|
|
5315
5362
|
}
|
|
5363
|
+
/** The anchor key of a thread already in the review, or null for one GitHub reports without a position. */
|
|
5364
|
+
function pendingKey(t) {
|
|
5365
|
+
const path = t.path;
|
|
5366
|
+
if (t.subjectType === "FILE") return anchorKey({
|
|
5367
|
+
path,
|
|
5368
|
+
subject_type: "file",
|
|
5369
|
+
body: ""
|
|
5370
|
+
});
|
|
5371
|
+
if (t.line == null) return null;
|
|
5372
|
+
return anchorKey({
|
|
5373
|
+
path,
|
|
5374
|
+
side: t.diffSide === "LEFT" ? "LEFT" : "RIGHT",
|
|
5375
|
+
line: t.line,
|
|
5376
|
+
start_line: t.startLine ?? void 0,
|
|
5377
|
+
body: ""
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5316
5380
|
const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
|
|
5317
|
-
/**
|
|
5381
|
+
/**
|
|
5382
|
+
* No `event` in the body, so GitHub keeps the new review pending with all of its comments.
|
|
5383
|
+
* Resolves the review's node id, for the comments REST cannot carry.
|
|
5384
|
+
*/
|
|
5318
5385
|
async function createPendingReview(run, cwd, repository, number, review) {
|
|
5319
|
-
await run([
|
|
5386
|
+
const out = await run([
|
|
5320
5387
|
"api",
|
|
5321
5388
|
"--method",
|
|
5322
5389
|
"POST",
|
|
@@ -5327,20 +5394,31 @@ async function createPendingReview(run, cwd, repository, number, review) {
|
|
|
5327
5394
|
cwd,
|
|
5328
5395
|
input: JSON.stringify(review)
|
|
5329
5396
|
});
|
|
5397
|
+
let id;
|
|
5398
|
+
try {
|
|
5399
|
+
id = JSON.parse(out).node_id;
|
|
5400
|
+
} catch {
|
|
5401
|
+
throw new GithubError(`unexpected output from gh api: ${out.slice(0, 200)}`, 502);
|
|
5402
|
+
}
|
|
5403
|
+
if (typeof id !== "string") throw new GithubError(`unexpected output from gh api: ${out.slice(0, 200)}`, 502);
|
|
5404
|
+
return { id };
|
|
5330
5405
|
}
|
|
5331
5406
|
const ADD_THREAD = `mutation($input:AddPullRequestReviewThreadInput!){addPullRequestReviewThread(input:$input){thread{id}}}`;
|
|
5332
|
-
/** Appends one thread through GraphQL; REST cannot extend a pending review. */
|
|
5407
|
+
/** Appends one thread through GraphQL; REST cannot extend a pending review, nor place a file comment. */
|
|
5333
5408
|
async function addToPendingReview(run, cwd, reviewId, c) {
|
|
5334
5409
|
const input = {
|
|
5335
5410
|
pullRequestReviewId: reviewId,
|
|
5336
5411
|
path: c.path,
|
|
5337
|
-
line: c.line,
|
|
5338
|
-
side: c.side,
|
|
5339
5412
|
body: c.body
|
|
5340
5413
|
};
|
|
5341
|
-
if (c.
|
|
5342
|
-
|
|
5343
|
-
input.
|
|
5414
|
+
if (isFileComment(c)) input.subjectType = "FILE";
|
|
5415
|
+
else {
|
|
5416
|
+
input.line = c.line;
|
|
5417
|
+
input.side = c.side;
|
|
5418
|
+
if (c.start_line != null) {
|
|
5419
|
+
input.startLine = c.start_line;
|
|
5420
|
+
input.startSide = c.start_side;
|
|
5421
|
+
}
|
|
5344
5422
|
}
|
|
5345
5423
|
await graphql(run, cwd, ADD_THREAD, { input });
|
|
5346
5424
|
}
|
|
@@ -11164,7 +11242,7 @@ const ChangedFileSchema = object({
|
|
|
11164
11242
|
binary: boolean(),
|
|
11165
11243
|
/** New-side blob sha. '' for deletions. Keys the viewed state. */
|
|
11166
11244
|
blob: string(),
|
|
11167
|
-
/**
|
|
11245
|
+
/** The `linguist-generated` gitattribute, a path pattern or a content sniff said this file is generated. Feeds auto-collapse and risk ranking. */
|
|
11168
11246
|
generated: boolean(),
|
|
11169
11247
|
/** A gitlink (mode 160000): `blob` is the recorded commit, the patch shows the commit-id change, and there is no file to open. */
|
|
11170
11248
|
submodule: literal(true).optional()
|
|
@@ -11192,7 +11270,8 @@ object({
|
|
|
11192
11270
|
contents: string(),
|
|
11193
11271
|
binary: boolean()
|
|
11194
11272
|
});
|
|
11195
|
-
const CommentAnchorSchema = object({
|
|
11273
|
+
const CommentAnchorSchema = discriminatedUnion("kind", [object({
|
|
11274
|
+
kind: literal("line"),
|
|
11196
11275
|
path: string(),
|
|
11197
11276
|
/** Column the user selected in a diff; 'new' for file items. */
|
|
11198
11277
|
side: SideSchema,
|
|
@@ -11200,7 +11279,11 @@ const CommentAnchorSchema = object({
|
|
|
11200
11279
|
endLine: number(),
|
|
11201
11280
|
/** Exact text of [startLine, endLine] at creation. */
|
|
11202
11281
|
quoted: string()
|
|
11203
|
-
})
|
|
11282
|
+
}), object({
|
|
11283
|
+
kind: literal("file"),
|
|
11284
|
+
/** The changed file's path (its new path for a rename), or any tree path. */
|
|
11285
|
+
path: string()
|
|
11286
|
+
})]);
|
|
11204
11287
|
const CommentMessageSchema = object({
|
|
11205
11288
|
id: string(),
|
|
11206
11289
|
body: string(),
|
|
@@ -11214,24 +11297,31 @@ object({
|
|
|
11214
11297
|
messages: CommentMessageSchema.array().min(1),
|
|
11215
11298
|
resolved: boolean(),
|
|
11216
11299
|
resolvedAt: number().optional(),
|
|
11217
|
-
/** Relocation failed after a snapshot refresh. */
|
|
11300
|
+
/** Relocation failed after a snapshot refresh: the lines left the diff, or the file left the review. */
|
|
11218
11301
|
stale: boolean(),
|
|
11219
|
-
/** Original startLine, shown in export when stale. */
|
|
11302
|
+
/** Original startLine of a line thread, shown in export when stale. */
|
|
11220
11303
|
staleFromLine: number().optional()
|
|
11221
11304
|
});
|
|
11222
|
-
/**
|
|
11305
|
+
/**
|
|
11306
|
+
* Create payload. Without `startLine` the thread is on the file as a whole; then `side`,
|
|
11307
|
+
* `endLine` and `quoted` are refused rather than ignored, so a payload that meant a line
|
|
11308
|
+
* never lands on the file. `quoted` is optional: the server quotes the range from the snapshot.
|
|
11309
|
+
*/
|
|
11223
11310
|
const ThreadCreateSchema = object({
|
|
11224
11311
|
path: string().min(1),
|
|
11225
11312
|
/** Default 'new'. */
|
|
11226
11313
|
side: SideSchema.optional(),
|
|
11227
|
-
startLine: number().int().positive(),
|
|
11314
|
+
startLine: number().int().positive().optional(),
|
|
11228
11315
|
/** Default startLine. */
|
|
11229
11316
|
endLine: number().int().positive().optional(),
|
|
11230
11317
|
body: string().refine((body) => body.trim().length > 0, "body required"),
|
|
11231
11318
|
quoted: string().optional()
|
|
11232
|
-
}).refine((t) => t.endLine == null || t.endLine >= t.startLine, {
|
|
11319
|
+
}).refine((t) => t.startLine == null || t.endLine == null || t.endLine >= t.startLine, {
|
|
11233
11320
|
path: ["endLine"],
|
|
11234
11321
|
message: "endLine must be ≥ startLine"
|
|
11322
|
+
}).refine((t) => t.startLine != null || t.side == null && t.endLine == null && t.quoted == null, {
|
|
11323
|
+
path: ["startLine"],
|
|
11324
|
+
message: "side, endLine and quoted need a startLine; leave all four out for a comment on the whole file"
|
|
11235
11325
|
});
|
|
11236
11326
|
const ReplyCreateSchema = object({ body: string().refine((body) => body.trim().length > 0, "body required") });
|
|
11237
11327
|
const ThreadStateSchema = _enum([
|
|
@@ -15780,6 +15870,7 @@ const LOOPBACK = /* @__PURE__ */ new Set([
|
|
|
15780
15870
|
"::1",
|
|
15781
15871
|
"localhost"
|
|
15782
15872
|
]);
|
|
15873
|
+
const HINT = "if a reverse proxy sent it, trust that origin with --allowed-origin";
|
|
15783
15874
|
/**
|
|
15784
15875
|
* Restricts API and WebSocket requests to local hosts and the configured public origin.
|
|
15785
15876
|
* Proxies may preserve the public Host or rewrite it to an allowed upstream Host.
|
|
@@ -15794,20 +15885,24 @@ function requestGuard(bindHost, names = ownNames(), allowedOrigin) {
|
|
|
15794
15885
|
]);
|
|
15795
15886
|
const publicHost = allowedOrigin == null ? void 0 : new URL(allowedOrigin).host;
|
|
15796
15887
|
return ({ host, origin }) => {
|
|
15797
|
-
if (!host) return
|
|
15888
|
+
if (!host) return "missing Host header";
|
|
15798
15889
|
const name = hostnameOf(host).toLowerCase();
|
|
15799
15890
|
const localHost = loopback ? LOOPBACK.has(name) : isIP(name) !== 0 || allowed.has(name);
|
|
15800
15891
|
const proxyHost = publicHost != null && host.toLowerCase() === publicHost;
|
|
15801
|
-
if (!localHost && !proxyHost) return
|
|
15802
|
-
if (origin == null || allowedOrigin != null && origin === allowedOrigin) return
|
|
15803
|
-
if (proxyHost) return
|
|
15804
|
-
|
|
15805
|
-
return new URL(origin).host === host;
|
|
15806
|
-
} catch {
|
|
15807
|
-
return false;
|
|
15808
|
-
}
|
|
15892
|
+
if (!localHost && !proxyHost) return `forbidden Host "${host}"; ${HINT}`;
|
|
15893
|
+
if (origin == null || allowedOrigin != null && origin === allowedOrigin) return null;
|
|
15894
|
+
if (!proxyHost && parseHost(origin) === host) return null;
|
|
15895
|
+
return `forbidden Origin "${origin}" for Host "${host}"; ${HINT}`;
|
|
15809
15896
|
};
|
|
15810
15897
|
}
|
|
15898
|
+
/** The host of a syntactically valid origin; undefined for `null` and other non-URLs. */
|
|
15899
|
+
function parseHost(origin) {
|
|
15900
|
+
try {
|
|
15901
|
+
return new URL(origin).host;
|
|
15902
|
+
} catch {
|
|
15903
|
+
return;
|
|
15904
|
+
}
|
|
15905
|
+
}
|
|
15811
15906
|
/** The machine's hostname, its short form, and its mDNS name, so LAN visitors can use any of them. */
|
|
15812
15907
|
function ownNames() {
|
|
15813
15908
|
const full = hostname();
|
|
@@ -15877,15 +15972,17 @@ function toThreadCreate(entry, where) {
|
|
|
15877
15972
|
}
|
|
15878
15973
|
/** True when `t` duplicates an open thread at the same anchor whose opening message has the same body. */
|
|
15879
15974
|
function isDuplicate(existing, t) {
|
|
15975
|
+
const body = t.body.trim();
|
|
15976
|
+
const open = existing.filter((e) => !e.resolved && e.anchor.path === t.path && e.messages[0]?.body.trim() === body);
|
|
15977
|
+
if (t.startLine == null) return open.some((e) => e.anchor.kind === "file");
|
|
15880
15978
|
const side = t.side ?? "new";
|
|
15881
15979
|
const endLine = t.endLine ?? t.startLine;
|
|
15882
|
-
|
|
15883
|
-
return existing.some((e) => !e.resolved && e.anchor.path === t.path && e.anchor.side === side && e.anchor.startLine === t.startLine && e.anchor.endLine === endLine && e.messages[0]?.body.trim() === body);
|
|
15980
|
+
return open.some((e) => e.anchor.kind === "line" && e.anchor.side === side && e.anchor.startLine === t.startLine && e.anchor.endLine === endLine);
|
|
15884
15981
|
}
|
|
15885
15982
|
//#endregion
|
|
15886
15983
|
//#region src/server/comments/CommentStore.ts
|
|
15887
15984
|
var NotFoundError = class extends Error {};
|
|
15888
|
-
/**
|
|
15985
|
+
/** An anchor the snapshot cannot place: path absent on that side, or lines past the end. */
|
|
15889
15986
|
var UnquotableError = class extends Error {};
|
|
15890
15987
|
/**
|
|
15891
15988
|
* Owns this mode's set in `<gitDir>/diffle/comments.json`: threads and viewed
|
|
@@ -15902,14 +15999,14 @@ var CommentStore = class CommentStore {
|
|
|
15902
15999
|
this.file = file;
|
|
15903
16000
|
this.key = key;
|
|
15904
16001
|
}
|
|
15905
|
-
/** Opens the store;
|
|
16002
|
+
/** Opens the store; an older file is backed up to `comments.v<n>.bak` and rewritten in the current format first. */
|
|
15906
16003
|
static async open(gitDir, modeKey) {
|
|
15907
16004
|
const file = join(gitDir, "diffle", "comments.json");
|
|
15908
16005
|
await withFileLock(file, async () => {
|
|
15909
16006
|
const all = await readStoreFile(file);
|
|
15910
|
-
if (all.version
|
|
15911
|
-
await copyFile(file, join(gitDir, "diffle",
|
|
15912
|
-
await writeStoreFile(file,
|
|
16007
|
+
if (all.version === 3) return;
|
|
16008
|
+
await copyFile(file, join(gitDir, "diffle", `comments.v${all.version}.bak`));
|
|
16009
|
+
await writeStoreFile(file, upgrade(all));
|
|
15913
16010
|
});
|
|
15914
16011
|
return new CommentStore(file, modeKey);
|
|
15915
16012
|
}
|
|
@@ -15985,12 +16082,13 @@ var CommentStore = class CommentStore {
|
|
|
15985
16082
|
});
|
|
15986
16083
|
}
|
|
15987
16084
|
/**
|
|
15988
|
-
* Adds each import that is not already an open duplicate. Every
|
|
15989
|
-
* checked against the snapshot through `
|
|
15990
|
-
* text the browser showed
|
|
15991
|
-
*
|
|
16085
|
+
* Adds each import that is not already an open duplicate. Every anchor is
|
|
16086
|
+
* checked against the snapshot through `source`: a range is quoted (a payload's
|
|
16087
|
+
* own `quoted`, the text the browser showed, is kept once the range is known to
|
|
16088
|
+
* exist) and a file thread's path must be in the review. Throws UnquotableError,
|
|
16089
|
+
* adding nothing, when any anchor cannot be placed.
|
|
15992
16090
|
*/
|
|
15993
|
-
importThreads(imports,
|
|
16091
|
+
importThreads(imports, source) {
|
|
15994
16092
|
return this.mutate(async (set) => {
|
|
15995
16093
|
const added = [];
|
|
15996
16094
|
const pending = [];
|
|
@@ -16001,18 +16099,7 @@ var CommentStore = class CommentStore {
|
|
|
16001
16099
|
skipped++;
|
|
16002
16100
|
continue;
|
|
16003
16101
|
}
|
|
16004
|
-
const
|
|
16005
|
-
const startLine = t.startLine;
|
|
16006
|
-
const endLine = t.endLine ?? startLine;
|
|
16007
|
-
const fromSnapshot = await quote(t.path, side, startLine, endLine);
|
|
16008
|
-
if (fromSnapshot == null) throw new UnquotableError(`cannot quote ${t.path}:${startLine}${endLine !== startLine ? `-${endLine}` : ""} on the ${side} side`);
|
|
16009
|
-
const anchor = {
|
|
16010
|
-
path: t.path,
|
|
16011
|
-
side,
|
|
16012
|
-
startLine,
|
|
16013
|
-
endLine,
|
|
16014
|
-
quoted: t.quoted ?? fromSnapshot
|
|
16015
|
-
};
|
|
16102
|
+
const anchor = await placeImport(t, source);
|
|
16016
16103
|
pending.push({
|
|
16017
16104
|
anchor,
|
|
16018
16105
|
t
|
|
@@ -16093,17 +16180,18 @@ var CommentStore = class CommentStore {
|
|
|
16093
16180
|
});
|
|
16094
16181
|
}
|
|
16095
16182
|
/**
|
|
16096
|
-
* Re-anchor every thread against the current review. A thread is stale
|
|
16097
|
-
* its side is gone, its text is not found, or the relocated range falls
|
|
16098
|
-
* outside what the review shows
|
|
16183
|
+
* Re-anchor every thread against the current review. A line thread is stale
|
|
16184
|
+
* when its side is gone, its text is not found, or the relocated range falls
|
|
16185
|
+
* outside what the review shows; a file thread when its file left the review.
|
|
16186
|
+
* Returns true if anything changed.
|
|
16099
16187
|
*/
|
|
16100
|
-
async relocateAll(
|
|
16188
|
+
async relocateAll(review) {
|
|
16101
16189
|
const cache = /* @__PURE__ */ new Map();
|
|
16102
16190
|
const load = (path, side) => {
|
|
16103
16191
|
const k = `${side}:${path}`;
|
|
16104
16192
|
let p = cache.get(k);
|
|
16105
16193
|
if (!p) {
|
|
16106
|
-
p =
|
|
16194
|
+
p = review.side(path, side);
|
|
16107
16195
|
cache.set(k, p);
|
|
16108
16196
|
}
|
|
16109
16197
|
return p;
|
|
@@ -16111,6 +16199,14 @@ var CommentStore = class CommentStore {
|
|
|
16111
16199
|
return this.mutate(async (set) => {
|
|
16112
16200
|
let changed = false;
|
|
16113
16201
|
for (const t of set.threads) {
|
|
16202
|
+
if (t.anchor.kind === "file") {
|
|
16203
|
+
const stale = !review.hasFile(t.anchor.path);
|
|
16204
|
+
if (stale !== t.stale) {
|
|
16205
|
+
t.stale = stale;
|
|
16206
|
+
changed = true;
|
|
16207
|
+
}
|
|
16208
|
+
continue;
|
|
16209
|
+
}
|
|
16114
16210
|
const v = await load(t.anchor.path, t.anchor.side);
|
|
16115
16211
|
let moved = v == null ? null : relocate(t.anchor, v.contents);
|
|
16116
16212
|
if (moved && v?.shown && !isShown(v.shown, moved.startLine, moved.endLine)) moved = null;
|
|
@@ -16147,7 +16243,7 @@ var CommentStore = class CommentStore {
|
|
|
16147
16243
|
viewed: []
|
|
16148
16244
|
};
|
|
16149
16245
|
}
|
|
16150
|
-
return ownSet(
|
|
16246
|
+
return ownSet(upgrade(parsed), this.key);
|
|
16151
16247
|
}
|
|
16152
16248
|
/**
|
|
16153
16249
|
* Locked read-modify-write: `fn` edits this key's set as read from disk under
|
|
@@ -16157,12 +16253,11 @@ var CommentStore = class CommentStore {
|
|
|
16157
16253
|
*/
|
|
16158
16254
|
mutate(fn, changed = () => true) {
|
|
16159
16255
|
return withFileLock(this.file, async () => {
|
|
16160
|
-
const
|
|
16161
|
-
const all = onDisk.version === 2 ? onDisk : migrateV1(onDisk);
|
|
16256
|
+
const all = upgrade(await readStoreFile(this.file));
|
|
16162
16257
|
const set = ownSet(all, this.key);
|
|
16163
16258
|
const r = await fn(set);
|
|
16164
16259
|
if (changed(r)) await writeStoreFile(this.file, {
|
|
16165
|
-
version:
|
|
16260
|
+
version: 3,
|
|
16166
16261
|
sets: {
|
|
16167
16262
|
...all.sets,
|
|
16168
16263
|
[this.key]: set
|
|
@@ -16184,7 +16279,33 @@ function findThread(set, id) {
|
|
|
16184
16279
|
if (!t) throw new NotFoundError(id);
|
|
16185
16280
|
return t;
|
|
16186
16281
|
}
|
|
16187
|
-
/**
|
|
16282
|
+
/**
|
|
16283
|
+
* The anchor an import lands on, checked against the snapshot. Throws
|
|
16284
|
+
* UnquotableError for a range that cannot be quoted or a file the review lacks.
|
|
16285
|
+
*/
|
|
16286
|
+
async function placeImport(t, source) {
|
|
16287
|
+
if (t.startLine == null) {
|
|
16288
|
+
if (!await source.hasFile(t.path)) throw new UnquotableError(`${t.path} is not in the review`);
|
|
16289
|
+
return {
|
|
16290
|
+
kind: "file",
|
|
16291
|
+
path: t.path
|
|
16292
|
+
};
|
|
16293
|
+
}
|
|
16294
|
+
const side = t.side ?? "new";
|
|
16295
|
+
const startLine = t.startLine;
|
|
16296
|
+
const endLine = t.endLine ?? startLine;
|
|
16297
|
+
const fromSnapshot = await source.quote(t.path, side, startLine, endLine);
|
|
16298
|
+
if (fromSnapshot == null) throw new UnquotableError(`cannot quote ${t.path}:${startLine}${endLine !== startLine ? `-${endLine}` : ""} on the ${side} side`);
|
|
16299
|
+
return {
|
|
16300
|
+
kind: "line",
|
|
16301
|
+
path: t.path,
|
|
16302
|
+
side,
|
|
16303
|
+
startLine,
|
|
16304
|
+
endLine,
|
|
16305
|
+
quoted: t.quoted ?? fromSnapshot
|
|
16306
|
+
};
|
|
16307
|
+
}
|
|
16308
|
+
/** The file as stored, or an empty store when absent. Malformed JSON propagates. */
|
|
16188
16309
|
async function readStoreFile(file) {
|
|
16189
16310
|
try {
|
|
16190
16311
|
return parseStoreFile(await readFile(file, "utf8"));
|
|
@@ -16192,20 +16313,26 @@ async function readStoreFile(file) {
|
|
|
16192
16313
|
if (e.code !== "ENOENT") throw e;
|
|
16193
16314
|
}
|
|
16194
16315
|
return {
|
|
16195
|
-
version:
|
|
16316
|
+
version: 3,
|
|
16196
16317
|
sets: {}
|
|
16197
16318
|
};
|
|
16198
16319
|
}
|
|
16199
|
-
/** Unknown shapes read as an empty
|
|
16320
|
+
/** Unknown shapes read as an empty store. */
|
|
16200
16321
|
function parseStoreFile(text) {
|
|
16201
16322
|
const parsed = JSON.parse(text);
|
|
16323
|
+
if (parsed.version === 3 && parsed.sets) return parsed;
|
|
16202
16324
|
if (parsed.version === 2 && parsed.sets) return parsed;
|
|
16203
16325
|
if (parsed.version === 1 && parsed.sets) return parsed;
|
|
16204
16326
|
return {
|
|
16205
|
-
version:
|
|
16327
|
+
version: 3,
|
|
16206
16328
|
sets: {}
|
|
16207
16329
|
};
|
|
16208
16330
|
}
|
|
16331
|
+
/** Any stored format as the current one. */
|
|
16332
|
+
function upgrade(all) {
|
|
16333
|
+
if (all.version === 3) return all;
|
|
16334
|
+
return migrateV2(all.version === 2 ? all : migrateV1(all));
|
|
16335
|
+
}
|
|
16209
16336
|
/** Atomic write. Quoted source lines: owner-only. */
|
|
16210
16337
|
function writeStoreFile(file, data) {
|
|
16211
16338
|
return writeFileAtomic(file, JSON.stringify(data, null, 2), 384);
|
|
@@ -16219,6 +16346,24 @@ function newMessage(msg) {
|
|
|
16219
16346
|
updatedAt: now
|
|
16220
16347
|
};
|
|
16221
16348
|
}
|
|
16349
|
+
/** Every v2 anchor is a line range; v3 says so. */
|
|
16350
|
+
function migrateV2(v2) {
|
|
16351
|
+
const sets = {};
|
|
16352
|
+
for (const [key, set] of Object.entries(v2.sets)) sets[key] = {
|
|
16353
|
+
threads: (set.threads ?? []).map((t) => ({
|
|
16354
|
+
...t,
|
|
16355
|
+
anchor: {
|
|
16356
|
+
kind: "line",
|
|
16357
|
+
...t.anchor
|
|
16358
|
+
}
|
|
16359
|
+
})),
|
|
16360
|
+
viewed: set.viewed ?? []
|
|
16361
|
+
};
|
|
16362
|
+
return {
|
|
16363
|
+
version: 3,
|
|
16364
|
+
sets
|
|
16365
|
+
};
|
|
16366
|
+
}
|
|
16222
16367
|
/** Each v1 comment becomes a single-message, unresolved thread. */
|
|
16223
16368
|
function migrateV1(v1) {
|
|
16224
16369
|
const sets = {};
|
|
@@ -16349,7 +16494,7 @@ function createApi(deps) {
|
|
|
16349
16494
|
});
|
|
16350
16495
|
app.post("/api/threads", async (c) => {
|
|
16351
16496
|
const imports = parseImports(await readJson(c));
|
|
16352
|
-
const { added } = await session.comments.importThreads(imports, session.
|
|
16497
|
+
const { added } = await session.comments.importThreads(imports, session.anchorSource());
|
|
16353
16498
|
if (added.length) hub.broadcast({ type: "threads" });
|
|
16354
16499
|
return c.json(added, 201);
|
|
16355
16500
|
});
|
|
@@ -16499,10 +16644,11 @@ var Server = class {
|
|
|
16499
16644
|
const app = new Hono();
|
|
16500
16645
|
const guard = requestGuard(this.opts.host, void 0, this.opts.allowedOrigin);
|
|
16501
16646
|
app.use("/api/*", async (c, next) => {
|
|
16502
|
-
|
|
16647
|
+
const rejected = guard({
|
|
16503
16648
|
host: c.req.header("host"),
|
|
16504
16649
|
origin: c.req.header("origin")
|
|
16505
|
-
})
|
|
16650
|
+
});
|
|
16651
|
+
if (rejected != null) return c.json({ error: rejected }, 403);
|
|
16506
16652
|
await next();
|
|
16507
16653
|
});
|
|
16508
16654
|
app.use("/api/*", async (c, next) => {
|
|
@@ -16892,6 +17038,8 @@ function looksGenerated(path, head) {
|
|
|
16892
17038
|
//#endregion
|
|
16893
17039
|
//#region src/server/Snapshotter.ts
|
|
16894
17040
|
const PREWARM = 3;
|
|
17041
|
+
/** The gitattribute GitHub reads to collapse generated files; `=false` opts a file out of the heuristics. */
|
|
17042
|
+
const GENERATED_ATTR = "linguist-generated";
|
|
16895
17043
|
/** Concurrent worktree reads while flagging generated files. */
|
|
16896
17044
|
const SNIFF_CONCURRENCY = 8;
|
|
16897
17045
|
/** Sniff verdicts remembered by blob sha; the map is emptied, not evicted, past this. */
|
|
@@ -17000,14 +17148,23 @@ var Snapshotter = class {
|
|
|
17000
17148
|
return snap;
|
|
17001
17149
|
}
|
|
17002
17150
|
/**
|
|
17003
|
-
* Sets `generated` on each non-binary changed file
|
|
17004
|
-
*
|
|
17005
|
-
*
|
|
17006
|
-
*
|
|
17151
|
+
* Sets `generated` on each non-binary changed file. A `linguist-generated`
|
|
17152
|
+
* gitattribute on the new side decides outright, as on GitHub; otherwise a
|
|
17153
|
+
* path convention, then a SNIFF_BYTES content sniff. Blobs already sniffed
|
|
17154
|
+
* are not read again; commit-side blobs go through one `cat-file --batch`,
|
|
17155
|
+
* worktree files through a bounded read each. Never fails the snapshot: a
|
|
17156
|
+
* failed attribute lookup or read means the next heuristic, then false.
|
|
17007
17157
|
* Submodules have no contents to sniff.
|
|
17008
17158
|
*/
|
|
17009
17159
|
async fillGenerated(files, newSha) {
|
|
17010
|
-
const
|
|
17160
|
+
const textual = files.filter((f) => f.status !== "D" && !f.binary && !f.submodule);
|
|
17161
|
+
const marked = await this.repo.attr(GENERATED_ATTR, textual.map((f) => f.path), newSha).catch(() => /* @__PURE__ */ new Map());
|
|
17162
|
+
const sniff = [];
|
|
17163
|
+
for (const f of textual) {
|
|
17164
|
+
const attr = marked.get(f.path);
|
|
17165
|
+
if (attr != null) f.generated = attr;
|
|
17166
|
+
else if (!(f.generated = looksGenerated(f.path, null))) sniff.push(f);
|
|
17167
|
+
}
|
|
17011
17168
|
const unknown = [];
|
|
17012
17169
|
for (const f of sniff) {
|
|
17013
17170
|
const known = f.blob ? this.sniffed.get(f.blob) : void 0;
|
|
@@ -17232,21 +17389,24 @@ var Session = class {
|
|
|
17232
17389
|
/**
|
|
17233
17390
|
* Re-anchors threads against what `snap` shows. A changed file shows its
|
|
17234
17391
|
* hunks; an unchanged tree file shows its new side whole (the file view) and
|
|
17235
|
-
* nothing of its old side.
|
|
17392
|
+
* nothing of its old side. A file thread only needs its file in the review.
|
|
17236
17393
|
*/
|
|
17237
17394
|
async relocateComments(a, snap) {
|
|
17238
|
-
await a.comments.relocateAll(
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17395
|
+
await a.comments.relocateAll({
|
|
17396
|
+
side: async (path, side) => {
|
|
17397
|
+
const buf = await this.readSide(snap, path, side);
|
|
17398
|
+
if (buf == null) return null;
|
|
17399
|
+
const contents = buf.toString("utf8");
|
|
17400
|
+
if (!snap.changed.some((f) => f.path === path)) return side === "new" ? {
|
|
17401
|
+
contents,
|
|
17402
|
+
shown: null
|
|
17403
|
+
} : null;
|
|
17404
|
+
return {
|
|
17405
|
+
contents,
|
|
17406
|
+
shown: shownRanges(await a.snapshotter.patch(path) ?? "", side)
|
|
17407
|
+
};
|
|
17408
|
+
},
|
|
17409
|
+
hasFile: (path) => this.hasFile(snap, path)
|
|
17250
17410
|
});
|
|
17251
17411
|
}
|
|
17252
17412
|
/**
|
|
@@ -17265,15 +17425,24 @@ var Session = class {
|
|
|
17265
17425
|
hasSide(snap, path, side) {
|
|
17266
17426
|
return sidePath(this.readablePaths(snap), path, side) != null;
|
|
17267
17427
|
}
|
|
17428
|
+
/** Whether `path` is a file of the review: a changed file (by its new path) or a tree path. A rename's old path is not. */
|
|
17429
|
+
hasFile(snap, path) {
|
|
17430
|
+
const r = this.readablePaths(snap);
|
|
17431
|
+
return r.tree.has(path) || r.changed.has(path);
|
|
17432
|
+
}
|
|
17268
17433
|
/**
|
|
17269
|
-
*
|
|
17270
|
-
*
|
|
17434
|
+
* Places imported anchors against the current snapshot: quotes a line range for
|
|
17435
|
+
* imports that carry no `quoted`, and says whether a file thread's file is in the
|
|
17436
|
+
* review. Goes through `readSide`, so the allowlist applies.
|
|
17271
17437
|
*/
|
|
17272
|
-
|
|
17273
|
-
return
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17438
|
+
anchorSource() {
|
|
17439
|
+
return {
|
|
17440
|
+
quote: async (path, side, startLine, endLine) => {
|
|
17441
|
+
const snap = await this.snapshotter.current();
|
|
17442
|
+
const buf = await this.readSide(snap, path, side);
|
|
17443
|
+
return buf == null ? null : quoteRange(buf.toString("utf8"), startLine, endLine);
|
|
17444
|
+
},
|
|
17445
|
+
hasFile: async (path) => this.hasFile(await this.snapshotter.current(), path)
|
|
17277
17446
|
};
|
|
17278
17447
|
}
|
|
17279
17448
|
readablePaths(snap) {
|
|
@@ -17325,7 +17494,7 @@ var Session = class {
|
|
|
17325
17494
|
}
|
|
17326
17495
|
};
|
|
17327
17496
|
async function defaultWatcherFactory() {
|
|
17328
|
-
const { Watcher } = await import("./Watcher-
|
|
17497
|
+
const { Watcher } = await import("./Watcher-CKwvGuPB.js");
|
|
17329
17498
|
return (target) => new Watcher(target);
|
|
17330
17499
|
}
|
|
17331
17500
|
function readablePaths(snap) {
|
|
@@ -21013,14 +21182,16 @@ var WsHub = class {
|
|
|
21013
21182
|
maxPayload: MAX_PAYLOAD_BYTES
|
|
21014
21183
|
});
|
|
21015
21184
|
clientListeners = /* @__PURE__ */ new Set();
|
|
21016
|
-
attach(server, guard = () =>
|
|
21185
|
+
attach(server, guard = () => null) {
|
|
21017
21186
|
server.on("upgrade", (req, socket, head) => {
|
|
21018
21187
|
if (new URL(req.url ?? "/", "http://localhost").pathname !== "/ws") return;
|
|
21019
|
-
|
|
21188
|
+
const rejected = guard({
|
|
21020
21189
|
host: req.headers.host,
|
|
21021
21190
|
origin: req.headers.origin
|
|
21022
|
-
})
|
|
21023
|
-
|
|
21191
|
+
});
|
|
21192
|
+
if (rejected != null) {
|
|
21193
|
+
socket.once("finish", () => socket.destroy());
|
|
21194
|
+
socket.end(`HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n${rejected}\n`);
|
|
21024
21195
|
return;
|
|
21025
21196
|
}
|
|
21026
21197
|
this.wss.handleUpgrade(req, socket, head, (ws) => {
|