@shiyuphan/follow-me 0.5.0 → 0.6.0
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 +6 -3
- package/dist/src/cli.js +35 -11
- package/dist/src/comments.js +62 -9
- package/dist/src/definition.js +226 -0
- package/dist/src/evolve.js +145 -29
- package/dist/src/git.js +69 -13
- package/dist/src/host.js +12 -3
- package/dist/src/plan.js +26 -1
- package/dist/src/prompt.js +122 -12
- package/dist/src/server.js +120 -28
- package/dist/src/session.js +23 -0
- package/dist/src/types.js +2 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-BXkLs7mV.js → abnfDiagram-WEYOIA3C-fYSYLdMl.js} +1 -1
- package/dist/ui/dist/assets/{arc-DNzjw7Ie.js → arc-DGuGSnwa.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-D66kgJJt.js → architecture-WOLXFQ4H-VFox_hV1.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-D8UWuhQr.js → architectureDiagram-ZHVYKT53-CBVK3nHZ.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BLkmr62i.js → blockDiagram-RJAE7QA3-BFdsIWEr.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-3rZPYwOc.js → c4Diagram-XDPPPE54-BGntcPId.js} +1 -1
- package/dist/ui/dist/assets/channel-gz6cS9Z1.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-MMnvAgpB.js → chunk-2E7WHVMD-CNMuK9zj.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-D9FGWA3l.js → chunk-4EMNKR2T-ByeTTJWS.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-DuOUaV8F.js → chunk-55XCU76K-99xE7cLH.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-p3YWPv0m.js → chunk-7S6XSTFO-ZVmT7eMZ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-DWfKYjwk.js → chunk-AQDTSKZB-B981JORL.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-C3CIwpv0.js → chunk-DSNLSHJZ-B2z7FX4h.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-Bwfl9gme.js → chunk-HAGFOTED-CHFbBS-4.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-0_oeU0le.js → chunk-HNE4Q36P-rq11xv9X.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-z_LPPPx5.js → chunk-NQOKRVIF-BYmg9kSX.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-BH28V2yu.js → chunk-UHW7PFBP-JvL9IXWg.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-BXDajDf7.js → chunk-VQ5W62QI-CYMQaAKn.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-BphBlfKj.js → chunk-WQQL4CDK-CXnR0PHT.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-BiCT5NwV.js → chunk-XXDRQBXY-CDUWJCXi.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-Bg5Pu8BX.js → chunk-YZO67HFG-mZfXIx0h.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-C_Tgd8tB.js → chunk-ZAUX2LTX-DBxVTCHX.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-MBRcMi8-.js → chunk-ZCHMQSOR-DrWA4IBo.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-1wyeCe5y.js → classDiagram-v2-4MYWR3UA-Beah5uxA.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BtOYclF6.js → cose-bilkent-JH36ORCC-UaE-jnHk.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-dJmoOG8g.js → cynefin-EF2NZ3EQ-DKfGaCR8.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-AloyqbEN.js → cynefinDiagram-WF24UDRP-tTUzTs3D.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-NCZhdnfb.js → dagre-NK4GIZVX-jSn35_T7.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-D8w7x5Gb.js → diagram-CBK3MWTC-7W2uw8Zc.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-BUFnCGlp.js → diagram-I5XIIV2D-CcfyQVh9.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-BULmHKaW.js → diagram-ODIL2UCW-BjGvaLis.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-B5MqFo0r.js → diagram-PFBEJUXE-jvdCrt8B.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-BIkDLL8E.js → diagram-RJA3I4VV-D65_Vpi_.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-Ca4gsQV8.js → diagram-TYUNFSNR-C52roQBE.js} +1 -1
- package/dist/ui/dist/assets/{dist-G85JKY5k.js → dist-BJd4jp9Y.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-rVjcFFtb.js → ebnfDiagram-V4P3TTPT-DXysoIBq.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BWPdvULh.js → elk-QONTBCTQ-BVZcsgV3.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BPxJJkRl.js → erDiagram-KYQIGQAA-BJnT4fVU.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BSB9WYV3.js → eventmodeling-K75KTNOO-C5MzxCMY.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BqUgZSCz.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-DKps530K.js → ganttDiagram-R7GPNTJY-CZRrnuah.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-D1d4UAFV.js → gitGraph-VSP46ZUC-l58aGSkh.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-N55yl6hc.js → gitGraphDiagram-PDYL7XHY-D4ks1WO-.js} +1 -1
- package/dist/ui/dist/assets/index-BW95UA5q.css +1 -0
- package/dist/ui/dist/assets/index-DOGXgoHF.js +134 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-CgxE3gf9.js → info-OHQRW6UA-BSqfzkdk.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-DOnUrnMf.js → infoDiagram-MVDRKAX5-B6NgVF_N.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-BIY0fFHB.js → ishikawaDiagram-HPEHV2GE-DvtfssEZ.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-CqA2Ern0.js → journeyDiagram-BOSHW7O7-DM64gT5v.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-vGrpQKIF.js → kanban-definition-FNSA4HC5-AliyRJHy.js} +1 -1
- package/dist/ui/dist/assets/{line-Brzv3qA6.js → line-CHncXbKO.js} +1 -1
- package/dist/ui/dist/assets/{linear-D9uIlqyh.js → linear-BWEWydI0.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-Bz9b-Thu.js → mermaid-parser.core-DGpOP8AM.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-BaZipu3b.js → mermaid.core-BJtBFbJx.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-D6aie7uH.js → mindmap-definition-6OY57UG3-NjavnWBi.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-28r3jVFY.js → packet-JDAUHWVQ-zuhpVsME.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-CbGvD4qP.js → pegDiagram-ECIUJNBX-sL4grM97.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-DHpcGnZd.js → pie-XZMESJXO-C9l1Mp09.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-DuhauwnY.js → pieDiagram-GNMPY74T-rlRBr2U4.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-CbZMOvrb.js → prism-core-DmuST30i.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BfyNmVca.js → quadrantDiagram-D57K4MC3-4oQMip2Q.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-Xp1TYwaj.js → radar-ABXABTNO-DvGg96L7.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-c5FY4wwa.js → railroad-I3PHUGI6-8wdFKMZp.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-RDyWXqcL.js → railroad-abnf-LNEFI6M7-DUtOO0gn.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-CLD4jVou.js → railroad-ebnf-DWYD2UWJ-5YCGY1_d.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-17EYTA-w.js → railroad-peg-JR7BVNR7-Bwywxjhs.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-C4p2wtKe.js → railroadDiagram-Y72NRBL7-CwO6HSNQ.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTYIFN8T.js → requirementDiagram-EQYSW5RA-Cp0IbfNr.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-YDYhRyJU.js → sankeyDiagram-SY24JHO5-DeQKsO68.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-Crvy6IlU.js → sequenceDiagram-BLEHNHQU-DTDalCxF.js} +1 -1
- package/dist/ui/dist/assets/{src-BPod-Ype.js → src-XnrqevnL.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-BWr70YOQ.js → stateDiagram-v2-A6IESKWZ-2DZ-tKo9.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-DvbG4I9a.js → swimlanes-B3ENRKFQ-66yZJeRU.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-rbX6bYG4.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-CR950Bzb.js → timeline-definition-JXYKGLGB-Dm1xGU-d.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DSDDXGrk.js → treeView-D4JQ5SDB-BvgsbSot.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-Ct5w2_RR.js → treemap-SAJKECNS-C3lnbG1a.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-_eP0371y.js → usecaseDiagram-WPOLBNNX-B6iPNIhZ.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-Cu-IA57L.js → vennDiagram-Q3Y3IDCW-D5dgflg9.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-r8yd6frN.js → wardley-7MLQ67FV-Dn9mLIB4.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-CBUUiRQm.js → wardleyDiagram-IVAIDKWA-W1EwzN_X.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-DEVQjvgG.js → xychartDiagram-4URNHDU4-CKRzMUvp.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +3 -1
- package/dist/ui/dist/assets/channel-B9JVdOBZ.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-N-Spyk2o.js +0 -1
- package/dist/ui/dist/assets/index-B8TYcxF8.css +0 -1
- package/dist/ui/dist/assets/index-BKtWBS7Z.js +0 -134
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-D2wU-jW6.js +0 -8
package/README.md
CHANGED
|
@@ -69,8 +69,11 @@ If the hashes differ, `apply` exits with code 1. The commits are signed when you
|
|
|
69
69
|
|
|
70
70
|
The red blob in the party hat is your branch tip. Nothing follow-me does moves it.
|
|
71
71
|
|
|
72
|
-

|
|
73
|
+
*The review page. The index groups the commits the way they read: the story first, then the steps it calls, then the details. Pick any line, changed or not, and Ask or Flag right under it; the agent answers inline.*
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
*Cmd-click a name to jump to where it is defined, even in a later commit, and Alt+Left to come back. A definition the branch does not change opens as a peek.*
|
|
74
77
|
|
|
75
78
|

|
|
76
79
|
*Diagrams sit full width under the commit or reply they explain, and open full size on click.*
|
|
@@ -78,7 +81,7 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
78
81
|

|
|
79
82
|
*Submit review. Each item carries the original branch's path and line, so your agent can post it to the merge request as is.*
|
|
80
83
|
|
|
81
|
-
Keys: `j` and `k` move between commits, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
84
|
+
Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
82
85
|
|
|
83
86
|
### Reviewing someone else's branch
|
|
84
87
|
|
package/dist/src/cli.js
CHANGED
|
@@ -3,15 +3,16 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { definitionIndex, isIdentifier } from "./definition.js";
|
|
6
7
|
import { DiffParseError, formatUnitRef, isUnitId } from "./diff.js";
|
|
7
8
|
import { BranchRefusedError, branchHead, GitError, isOwnWork, signing, treeOf, writeBranch, } from "./git.js";
|
|
8
9
|
import { fetchCarried, fetchComments, fetchReport, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
|
|
9
10
|
import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
|
|
10
11
|
import { naiveDraft } from "./naive.js";
|
|
11
12
|
import { isCommentId, isConcernId, isFields, serializePlanFile, validatePlan, } from "./plan.js";
|
|
12
|
-
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownUnit, reportItems, } from "./prompt.js";
|
|
13
|
+
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
|
|
13
14
|
import { RegistrationError, sessionIdOf } from "./server.js";
|
|
14
|
-
import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sinceLastPlan, } from "./session.js";
|
|
15
|
+
import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
|
|
15
16
|
import { checkSyntax } from "./syntax.js";
|
|
16
17
|
class UsageError extends Error {
|
|
17
18
|
name = "UsageError";
|
|
@@ -30,9 +31,10 @@ Other commands:
|
|
|
30
31
|
Mermaid source
|
|
31
32
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
32
33
|
start a thread on the page without being asked
|
|
33
|
-
show <h_…|c_…|k_…|path:line[@n|@base]>
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
show <h_…|c_…|k_…|path:line[@n|@base]|name>
|
|
35
|
+
print a unit, a comment's thread, a concern, the
|
|
36
|
+
lines around path:line in commit n, or where a name
|
|
37
|
+
is defined, instead of reading files
|
|
36
38
|
report [--json] [--markdown] print the report of the latest review; --json lists
|
|
37
39
|
the items to post to a merge request
|
|
38
40
|
|
|
@@ -85,11 +87,13 @@ Interactive loop (while the review page is open):
|
|
|
85
87
|
--lines are change lines of the unit, commits count
|
|
86
88
|
from 1; with no target it is about the whole plan.
|
|
87
89
|
watch leaves these out like your replies
|
|
88
|
-
show <h_…|c_…|k_…|path:line[@n|@base]> [--branch <name>]
|
|
90
|
+
show <h_…|c_…|k_…|path:line[@n|@base]|name> [--branch <name>]
|
|
89
91
|
print a unit with the commits that take it, a
|
|
90
92
|
comment with its rows and thread, a concern with its
|
|
91
|
-
lines,
|
|
92
|
-
commit n of the plan, or in the base
|
|
93
|
+
lines, the lines around path:line at the tip, in
|
|
94
|
+
commit n of the plan, or in the base, or each place
|
|
95
|
+
a name is defined at the tip with the commit it
|
|
96
|
+
lands in
|
|
93
97
|
report [--json] [--markdown] [--branch <name>]
|
|
94
98
|
print the report of the open review, or of the last
|
|
95
99
|
one that ended. Markdown lists issues by file and
|
|
@@ -909,7 +913,9 @@ function finished(session, stored) {
|
|
|
909
913
|
const text = renderStored(session, stored);
|
|
910
914
|
if (text === null)
|
|
911
915
|
return 1;
|
|
912
|
-
|
|
916
|
+
// A review that swapped versions holds what its earlier versions said too.
|
|
917
|
+
const said = stored.review.comments.length + (stored.review.carried?.length ?? 0);
|
|
918
|
+
say(`review v${stored.version} ended with ${plural(said, "comment")}`);
|
|
913
919
|
process.stdout.write(text);
|
|
914
920
|
say();
|
|
915
921
|
next("done; the user can port it with follow-me report --json");
|
|
@@ -1240,7 +1246,19 @@ function renderStored(session, stored, format = "markdown") {
|
|
|
1240
1246
|
.plans()
|
|
1241
1247
|
.filter((p) => p.version > stored.version)
|
|
1242
1248
|
.flatMap((p) => p.file.responses);
|
|
1243
|
-
const
|
|
1249
|
+
const { carried } = stored.review;
|
|
1250
|
+
const said = reviewsOf(session)
|
|
1251
|
+
.filter((r) => r.version <= stored.version)
|
|
1252
|
+
.map((r) => r.review);
|
|
1253
|
+
const earlier = carried === undefined
|
|
1254
|
+
? {}
|
|
1255
|
+
: {
|
|
1256
|
+
earlier: {
|
|
1257
|
+
comments: sessionComments(carried, said),
|
|
1258
|
+
concerns: stored.review.carriedConcerns ?? [],
|
|
1259
|
+
},
|
|
1260
|
+
};
|
|
1261
|
+
const opts = { branch: session.anchor.branch, responses, ...earlier };
|
|
1244
1262
|
if (format === "json") {
|
|
1245
1263
|
const items = reportItems(stored.review, verdict.plan.commits, session.diff.units, opts);
|
|
1246
1264
|
return `${JSON.stringify(items, null, 2)}\n`;
|
|
@@ -1502,7 +1520,7 @@ async function explain(cwd, args) {
|
|
|
1502
1520
|
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1503
1521
|
return 0;
|
|
1504
1522
|
}
|
|
1505
|
-
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]>";
|
|
1523
|
+
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]|name>";
|
|
1506
1524
|
/** Every comment the session has, with the plan version it was made on: live ones first. */
|
|
1507
1525
|
async function knownComments(session) {
|
|
1508
1526
|
const open = await openOnServer(session, null);
|
|
@@ -1615,6 +1633,12 @@ async function showRef(cwd, args) {
|
|
|
1615
1633
|
}
|
|
1616
1634
|
throw new UsageError(`no concern ${ref} in a plan that fits the branch`);
|
|
1617
1635
|
}
|
|
1636
|
+
if (isIdentifier(ref)) {
|
|
1637
|
+
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1638
|
+
const index = definitionIndex(session.root, session.diff.tip, units, commits);
|
|
1639
|
+
process.stdout.write(renderShownDefinitions(ref, await index.lookup(ref, null)));
|
|
1640
|
+
return 0;
|
|
1641
|
+
}
|
|
1618
1642
|
process.stdout.write(showLines(session, ref));
|
|
1619
1643
|
return 0;
|
|
1620
1644
|
}
|
package/dist/src/comments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRepoPath, isUnitId } from "./diff.js";
|
|
2
|
-
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, } from "./plan.js";
|
|
2
|
+
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
3
3
|
/**
|
|
4
4
|
* The wire formats of review state, parsed once at the boundary: comments, replies, concern
|
|
5
5
|
* decisions, the review file, its autosaved draft and the events a hosted session logs. Every
|
|
@@ -253,13 +253,7 @@ function parseEarlierReplies(v) {
|
|
|
253
253
|
function isLine(v) {
|
|
254
254
|
return typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
if (!isFields(v)) {
|
|
258
|
-
return "not an object";
|
|
259
|
-
}
|
|
260
|
-
const comment = parseComment(v.comment);
|
|
261
|
-
if (typeof comment === "string")
|
|
262
|
-
return comment;
|
|
256
|
+
function parsePlacement(v) {
|
|
263
257
|
const { unit, offsets, changed, path, newLine } = v;
|
|
264
258
|
if (unit !== null && !(typeof unit === "string" && isUnitId(unit)))
|
|
265
259
|
return "unit is not a unit id";
|
|
@@ -273,7 +267,6 @@ export function parseCarried(v) {
|
|
|
273
267
|
if (newLine !== null && !isLine(newLine))
|
|
274
268
|
return "newLine is not null or a line";
|
|
275
269
|
return {
|
|
276
|
-
comment,
|
|
277
270
|
unit,
|
|
278
271
|
offsets: offsets === null ? null : offsets.filter(isIndex),
|
|
279
272
|
changed,
|
|
@@ -281,6 +274,56 @@ export function parseCarried(v) {
|
|
|
281
274
|
newLine,
|
|
282
275
|
};
|
|
283
276
|
}
|
|
277
|
+
export function parseCarried(v) {
|
|
278
|
+
if (!isFields(v))
|
|
279
|
+
return "not an object";
|
|
280
|
+
const comment = parseComment(v.comment);
|
|
281
|
+
if (typeof comment === "string")
|
|
282
|
+
return comment;
|
|
283
|
+
const placement = parsePlacement(v);
|
|
284
|
+
return typeof placement === "string" ? placement : { comment, ...placement };
|
|
285
|
+
}
|
|
286
|
+
function parseSince(v) {
|
|
287
|
+
if (v === null)
|
|
288
|
+
return null;
|
|
289
|
+
return isPlanVersion(v) ? v : "since is not null or a plan version";
|
|
290
|
+
}
|
|
291
|
+
function parseCarriedRef(v) {
|
|
292
|
+
if (!isFields(v))
|
|
293
|
+
return "not an object";
|
|
294
|
+
const comment = asCommentId(v.comment);
|
|
295
|
+
if (comment === null)
|
|
296
|
+
return "comment is not a comment id";
|
|
297
|
+
if (!isPlanVersion(v.version))
|
|
298
|
+
return "version is not a plan version";
|
|
299
|
+
const since = parseSince(v.since);
|
|
300
|
+
if (typeof since === "string")
|
|
301
|
+
return since;
|
|
302
|
+
const placement = parsePlacement(v);
|
|
303
|
+
if (typeof placement === "string")
|
|
304
|
+
return placement;
|
|
305
|
+
return { ...placement, comment, version: v.version, since };
|
|
306
|
+
}
|
|
307
|
+
function parseCarriedConcern(v) {
|
|
308
|
+
if (!isFields(v)) {
|
|
309
|
+
return "not an object";
|
|
310
|
+
}
|
|
311
|
+
const concern = parsePlacedConcern(v.concern);
|
|
312
|
+
if (typeof concern === "string")
|
|
313
|
+
return `concern: ${concern}`;
|
|
314
|
+
if (!isIndex(v.commit))
|
|
315
|
+
return "commit is not a commit index";
|
|
316
|
+
const decision = parseDecision(v.decision);
|
|
317
|
+
if (typeof decision === "string")
|
|
318
|
+
return `decision: ${decision}`;
|
|
319
|
+
const since = parseSince(v.since);
|
|
320
|
+
if (typeof since === "string")
|
|
321
|
+
return since;
|
|
322
|
+
const placement = parsePlacement(v);
|
|
323
|
+
if (typeof placement === "string")
|
|
324
|
+
return placement;
|
|
325
|
+
return { ...placement, concern, commit: v.commit, decision, since };
|
|
326
|
+
}
|
|
284
327
|
/** Boundary parse of reviews/vN.json. Files written before walk mode existed have no mode. */
|
|
285
328
|
export function parseReview(v) {
|
|
286
329
|
if (!isFields(v)) {
|
|
@@ -314,6 +357,12 @@ export function parseReview(v) {
|
|
|
314
357
|
const earlier = parseList(v.earlierReplies, parseEarlierReplies, "earlierReplies");
|
|
315
358
|
if (typeof earlier === "string")
|
|
316
359
|
return earlier;
|
|
360
|
+
const carried = v.carried === undefined ? null : parseList(v.carried, parseCarriedRef, "carried");
|
|
361
|
+
if (typeof carried === "string")
|
|
362
|
+
return carried;
|
|
363
|
+
const concernsCarried = parseList(v.carriedConcerns, parseCarriedConcern, "carriedConcerns");
|
|
364
|
+
if (typeof concernsCarried === "string")
|
|
365
|
+
return concernsCarried;
|
|
317
366
|
return {
|
|
318
367
|
schema: v.schema,
|
|
319
368
|
mode,
|
|
@@ -325,6 +374,8 @@ export function parseReview(v) {
|
|
|
325
374
|
...(resolved.length === 0 ? {} : { resolved }),
|
|
326
375
|
...(anyExcluded ? { excluded } : {}),
|
|
327
376
|
...(earlier.length === 0 ? {} : { earlierReplies: earlier }),
|
|
377
|
+
...(carried === null ? {} : { carried }),
|
|
378
|
+
...(concernsCarried.length === 0 ? {} : { carriedConcerns: concernsCarried }),
|
|
328
379
|
};
|
|
329
380
|
}
|
|
330
381
|
function lenient(items, parse) {
|
|
@@ -350,6 +401,8 @@ export function parseDraft(v) {
|
|
|
350
401
|
resolved: lenientIds(v.resolved, isCommentIdValue),
|
|
351
402
|
excluded: typeof excluded === "string" ? { comments: [], concerns: [] } : excluded,
|
|
352
403
|
earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
|
|
404
|
+
carried: v.carried === undefined ? null : lenient(v.carried, parseCarriedRef),
|
|
405
|
+
carriedConcerns: lenient(v.carriedConcerns, parseCarriedConcern),
|
|
353
406
|
};
|
|
354
407
|
}
|
|
355
408
|
export function parseEvent(v) {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { splitLines, tipStartOf } from "./diff.js";
|
|
3
|
+
import { blobsAt, grepTip } from "./git.js";
|
|
4
|
+
export const MAX_CANDIDATES = 10;
|
|
5
|
+
const PREVIEW_LINES = 8;
|
|
6
|
+
/** An ASCII identifier as JS, Python, Go, Rust, JVM languages, Ruby and C spell one. */
|
|
7
|
+
export function isIdentifier(s) {
|
|
8
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(s);
|
|
9
|
+
}
|
|
10
|
+
const JS_MODS = "((static|async|public|private|protected|readonly|override|get|set)\\s+)*";
|
|
11
|
+
const RUST_VIS = "(pub([(][^)]*[)])?\\s+)?";
|
|
12
|
+
const JAVA_MODS = "((public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\\s+)*";
|
|
13
|
+
const JVM_CLASS_MODS = "((public|private|protected|static|final|abstract|sealed|data|open|internal|inner|enum|annotation)\\s+)*";
|
|
14
|
+
const KOTLIN_MODS = "((public|private|protected|internal|override|open|abstract|suspend|inline|operator|infix|tailrec|external|actual|expect)\\s+)*";
|
|
15
|
+
const STATEMENT = /^\s*(return|new|throw|else|case|yield|await|typedef|delete|goto)\b/;
|
|
16
|
+
const LANGUAGES = [
|
|
17
|
+
{
|
|
18
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
|
19
|
+
patterns: [
|
|
20
|
+
{
|
|
21
|
+
kind: "function",
|
|
22
|
+
source: "^\\s*(export\\s+)?(default\\s+)?(async\\s+)?function\\s*[*]?\\s*NAME\\s*[(<]",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: "const",
|
|
26
|
+
source: "^\\s*(export\\s+)?(declare\\s+)?(const|let|var)\\s+NAME\\s*(:[^=]*)?=",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
kind: "class",
|
|
30
|
+
source: "^\\s*(export\\s+)?(default\\s+)?(declare\\s+)?(abstract\\s+)?class\\s+NAMEEND",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: "type",
|
|
34
|
+
source: "^\\s*(export\\s+)?(declare\\s+)?(interface|type|(const\\s+)?enum)\\s+NAMEEND",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
kind: "method",
|
|
38
|
+
source: `^\\s+${JS_MODS}[*]?NAME\\s*(<[^>]*>)?[(].*[)]\\s*(:[^={;]+)?[{]\\s*$`,
|
|
39
|
+
reject: /^\s*(if|for|while|switch|catch|return)\b/,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
extensions: [".py", ".pyi"],
|
|
45
|
+
patterns: [
|
|
46
|
+
{ kind: "function", source: "^(async\\s+)?def\\s+NAME\\s*[(]" },
|
|
47
|
+
{ kind: "method", source: "^\\s+(async\\s+)?def\\s+NAME\\s*[(]" },
|
|
48
|
+
{ kind: "class", source: "^\\s*class\\s+NAME\\s*[(:]" },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
extensions: [".go"],
|
|
53
|
+
patterns: [
|
|
54
|
+
{ kind: "function", source: "^func\\s+NAME\\s*[[(]" },
|
|
55
|
+
{ kind: "method", source: "^func\\s*[(][^)]*[)]\\s*NAME\\s*[[(]" },
|
|
56
|
+
{ kind: "type", source: "^\\s*type\\s+NAMEEND" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
extensions: [".rs"],
|
|
61
|
+
patterns: [
|
|
62
|
+
{
|
|
63
|
+
kind: "function",
|
|
64
|
+
source: `^${RUST_VIS}((const|async|unsafe|extern)\\s+("[^"]*"\\s+)?)*fn\\s+NAME\\s*[<(]`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: "method",
|
|
68
|
+
source: `^\\s+${RUST_VIS}((const|async|unsafe|extern)\\s+("[^"]*"\\s+)?)*fn\\s+NAME\\s*[<(]`,
|
|
69
|
+
},
|
|
70
|
+
{ kind: "class", source: `^\\s*${RUST_VIS}struct\\s+NAMEEND` },
|
|
71
|
+
{ kind: "type", source: `^\\s*${RUST_VIS}(enum|trait|type)\\s+NAMEEND` },
|
|
72
|
+
{ kind: "other", source: "^\\s*impl(<[^>]*>)?\\s+([A-Za-z0-9_:<>, ]+\\s+for\\s+)?NAMEEND" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
extensions: [".java", ".kt", ".kts"],
|
|
77
|
+
patterns: [
|
|
78
|
+
{ kind: "class", source: `^\\s*${JVM_CLASS_MODS}(class|object|record)\\s+NAMEEND` },
|
|
79
|
+
{ kind: "type", source: `^\\s*${JVM_CLASS_MODS}(interface|enum|@interface)\\s+NAMEEND` },
|
|
80
|
+
{
|
|
81
|
+
kind: "function",
|
|
82
|
+
source: `^\\s*${KOTLIN_MODS}fun\\s+(<[^>]*>\\s*)?([A-Za-z0-9_.<>]+[.])?NAME\\s*[(]`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: "method",
|
|
86
|
+
source: `^\\s*${JAVA_MODS}(<[^>]*>\\s+)?[A-Za-z_][A-Za-z0-9_<>,.?]*(\\[\\])*\\s+NAME\\s*[(][^;]*$`,
|
|
87
|
+
reject: STATEMENT,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
extensions: [".rb"],
|
|
93
|
+
patterns: [
|
|
94
|
+
{ kind: "method", source: "^\\s*def\\s+(self[.])?NAMEEND" },
|
|
95
|
+
{ kind: "class", source: "^\\s*class\\s+NAMEEND" },
|
|
96
|
+
{ kind: "other", source: "^\\s*module\\s+NAMEEND" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx"],
|
|
101
|
+
patterns: [
|
|
102
|
+
{
|
|
103
|
+
kind: "function",
|
|
104
|
+
source: "^[A-Za-z_][A-Za-z0-9_ *&<>,:~]*[ *&]([A-Za-z_][A-Za-z0-9_]*::)*NAME\\s*[(][^;]*$",
|
|
105
|
+
reject: STATEMENT,
|
|
106
|
+
},
|
|
107
|
+
{ kind: "function", source: "^NAME\\s*[(][^;]*$" },
|
|
108
|
+
{ kind: "class", source: "^\\s*(typedef\\s+)?(struct|class|union)\\s+NAME\\s*([{:]|$)" },
|
|
109
|
+
{ kind: "type", source: "^\\s*(typedef\\s+)?enum\\s+(class\\s+)?NAME\\s*([{:]|$)" },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const END = "([^A-Za-z0-9_$]|$)";
|
|
114
|
+
function spell(source, name) {
|
|
115
|
+
return source
|
|
116
|
+
.replaceAll("NAMEEND", `${name.replaceAll("$", "[$]")}${END}`)
|
|
117
|
+
.replaceAll("NAME", name.replaceAll("$", "[$]"));
|
|
118
|
+
}
|
|
119
|
+
function toEre(source) {
|
|
120
|
+
return source.replaceAll("\\s", "[[:space:]]");
|
|
121
|
+
}
|
|
122
|
+
function languageOf(path) {
|
|
123
|
+
const ext = posix.extname(path).toLowerCase();
|
|
124
|
+
return LANGUAGES.find((l) => l.extensions.includes(ext)) ?? null;
|
|
125
|
+
}
|
|
126
|
+
/** What the match defines by the patterns of its file's language, or null when it is no definition. */
|
|
127
|
+
function kindOf(match, name) {
|
|
128
|
+
const language = languageOf(match.path);
|
|
129
|
+
if (language === null)
|
|
130
|
+
return null;
|
|
131
|
+
const hit = language.patterns.find((p) => new RegExp(spell(p.source, name)).test(match.text) && !p.reject?.test(match.text));
|
|
132
|
+
return hit?.kind ?? null;
|
|
133
|
+
}
|
|
134
|
+
/** Same file as `from` first, then the same directory, then the rest, each in path and line order. */
|
|
135
|
+
export function rank(found, from) {
|
|
136
|
+
const tier = (path) => {
|
|
137
|
+
if (from === null)
|
|
138
|
+
return 2;
|
|
139
|
+
if (path === from)
|
|
140
|
+
return 0;
|
|
141
|
+
return posix.dirname(path) === posix.dirname(from) ? 1 : 2;
|
|
142
|
+
};
|
|
143
|
+
return found
|
|
144
|
+
.map((f, i) => ({ f, i, t: tier(f.path) }))
|
|
145
|
+
.sort((a, b) => a.t - b.t || a.i - b.i)
|
|
146
|
+
.map(({ f }) => f);
|
|
147
|
+
}
|
|
148
|
+
const NOWHERE = { unit: null, row: null, commit: null };
|
|
149
|
+
/**
|
|
150
|
+
* The hunk row that holds tip line `line` of `path`, and the commit that writes it: the owner of a
|
|
151
|
+
* change row, or the first commit taking the unit for a context row.
|
|
152
|
+
*/
|
|
153
|
+
export function place(units, commits, path, line) {
|
|
154
|
+
for (const unit of units.values()) {
|
|
155
|
+
if (unit.kind !== "hunk" || unit.path !== path)
|
|
156
|
+
continue;
|
|
157
|
+
const row = rowAt(unit, tipStartOf(unit, units), line);
|
|
158
|
+
if (row === null)
|
|
159
|
+
continue;
|
|
160
|
+
const body = unit.body[row];
|
|
161
|
+
const change = body !== undefined && body.op !== " " ? body.change : null;
|
|
162
|
+
const takes = (c) => c.refs.some((r) => r.unit === unit.id &&
|
|
163
|
+
(change === null || r.select.kind === "rest" || r.select.changes.includes(change)));
|
|
164
|
+
const k = commits.findIndex(takes);
|
|
165
|
+
return { unit: unit.id, row, commit: k === -1 ? null : k };
|
|
166
|
+
}
|
|
167
|
+
return NOWHERE;
|
|
168
|
+
}
|
|
169
|
+
function rowAt(hunk, tipStart, line) {
|
|
170
|
+
let tipLine = tipStart;
|
|
171
|
+
for (const [row, body] of hunk.body.entries()) {
|
|
172
|
+
if (body.op === "-")
|
|
173
|
+
continue;
|
|
174
|
+
if (tipLine === line)
|
|
175
|
+
return row;
|
|
176
|
+
tipLine += 1;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function stripEol(line) {
|
|
181
|
+
return line.replace(/\r?\n$/, "");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Where names are defined at one branch tip and where each lands in the plan. Built per hosted
|
|
185
|
+
* version, so a swap starts a fresh one; matches are cached per name and file lines per path.
|
|
186
|
+
*/
|
|
187
|
+
export function definitionIndex(repo, tip, units, commits) {
|
|
188
|
+
const foundByName = new Map();
|
|
189
|
+
const linesByPath = new Map();
|
|
190
|
+
const find = (name) => {
|
|
191
|
+
const cached = foundByName.get(name);
|
|
192
|
+
if (cached !== undefined)
|
|
193
|
+
return cached;
|
|
194
|
+
const patterns = [
|
|
195
|
+
...new Set(LANGUAGES.flatMap((l) => l.patterns.map((p) => toEre(spell(p.source, name))))),
|
|
196
|
+
];
|
|
197
|
+
const pathspecs = LANGUAGES.flatMap((l) => l.extensions.map((e) => `*${e}`));
|
|
198
|
+
const found = grepTip(repo, tip, patterns, pathspecs).then((matches) => matches.flatMap((m) => {
|
|
199
|
+
const kind = kindOf(m, name);
|
|
200
|
+
return kind === null ? [] : [{ path: m.path, newLine: m.line, kind }];
|
|
201
|
+
}));
|
|
202
|
+
foundByName.set(name, found);
|
|
203
|
+
// A failed grep is not an answer; the next request tries again.
|
|
204
|
+
found.catch(() => foundByName.delete(name));
|
|
205
|
+
return found;
|
|
206
|
+
};
|
|
207
|
+
const load = async (paths) => {
|
|
208
|
+
const missing = [...new Set(paths)].filter((p) => !linesByPath.has(p));
|
|
209
|
+
if (missing.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
for (const [path, bytes] of await blobsAt(repo, tip, missing)) {
|
|
212
|
+
linesByPath.set(path, splitLines(bytes).map(stripEol));
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
lookup: async (name, from) => {
|
|
217
|
+
const ranked = rank(await find(name), from).slice(0, MAX_CANDIDATES);
|
|
218
|
+
await load(ranked.map((f) => f.path));
|
|
219
|
+
return ranked.map((f) => ({
|
|
220
|
+
...f,
|
|
221
|
+
...place(units, commits, f.path, f.newLine),
|
|
222
|
+
preview: (linesByPath.get(f.path) ?? []).slice(f.newLine - 1, f.newLine - 1 + PREVIEW_LINES),
|
|
223
|
+
}));
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
package/dist/src/evolve.js
CHANGED
|
@@ -78,50 +78,166 @@ function distance(hunk, units, line) {
|
|
|
78
78
|
const [from, to] = span(hunk, units);
|
|
79
79
|
return line < from ? from - line : line > to ? line - to : 0;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
/** The body row whose tip line is `line`; a kept row before a removal at the same line. */
|
|
82
|
+
function rowAt(hunk, units, line) {
|
|
83
|
+
if (line === null)
|
|
84
|
+
return 0;
|
|
85
|
+
const lines = tipLines(hunk, units);
|
|
86
|
+
const row = hunk.body.findIndex((r, i) => r.op !== "-" && lines[i] === line);
|
|
87
|
+
return row === -1 ? 0 : row;
|
|
87
88
|
}
|
|
88
|
-
function
|
|
89
|
+
function pinnedComment(comment) {
|
|
89
90
|
const { anchor } = comment;
|
|
90
|
-
const nowhere = { comment, unit: null, offsets: null, newLine: null };
|
|
91
|
-
if (anchor.kind !== "hunk")
|
|
92
|
-
return { ...nowhere, changed: false, path: comment.context.path };
|
|
93
91
|
// A comment on a whole hunk has no line; inventing one moves its card off the hunk's end.
|
|
94
|
-
const whole = anchor.changes === null && anchor.offsets === undefined;
|
|
95
|
-
|
|
92
|
+
const whole = anchor.kind === "hunk" && anchor.changes === null && anchor.offsets === undefined;
|
|
93
|
+
return { whole, row: (hunk) => firstRow(comment, hunk) };
|
|
94
|
+
}
|
|
95
|
+
function pinnedConcern(concern) {
|
|
96
|
+
const { lines } = concern;
|
|
97
|
+
if (lines === undefined)
|
|
98
|
+
return { whole: true, row: () => 0 };
|
|
99
|
+
return {
|
|
100
|
+
whole: false,
|
|
101
|
+
row: (hunk) => {
|
|
102
|
+
const row = hunk.body.findIndex((r) => r.op !== " " && lines.includes(r.change));
|
|
103
|
+
return row === -1 ? 0 : row;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Where a comment points on the version it was made on. */
|
|
108
|
+
function placedOwn(comment) {
|
|
109
|
+
const { anchor } = comment;
|
|
110
|
+
return {
|
|
111
|
+
unit: anchor.kind === "hunk" ? anchor.unit : null,
|
|
112
|
+
offsets: anchor.kind === "hunk" ? (anchor.offsets ?? null) : null,
|
|
113
|
+
changed: false,
|
|
114
|
+
path: comment.context.path,
|
|
115
|
+
newLine: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pure. Moves a placement on the old diff to the new one: a unit the new diff still has keeps its
|
|
120
|
+
* place, else the hunk of the same path nearest its old tip line takes it, marked changed. A
|
|
121
|
+
* placement on no unit stays as it is.
|
|
122
|
+
*/
|
|
123
|
+
function move(at, pinned, oldUnits, newUnits) {
|
|
124
|
+
if (at.unit === null)
|
|
125
|
+
return at;
|
|
126
|
+
const kept = newUnits.get(at.unit);
|
|
127
|
+
const old = oldUnits.get(at.unit);
|
|
96
128
|
if (kept !== undefined) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
};
|
|
129
|
+
let newLine = null;
|
|
130
|
+
if (kept.kind === "hunk" && !pinned.whole) {
|
|
131
|
+
if (!at.changed)
|
|
132
|
+
newLine = tipLines(kept, newUnits)[pinned.row(kept)] ?? null;
|
|
133
|
+
else if (at.newLine !== null && old?.kind === "hunk") {
|
|
134
|
+
newLine = at.newLine - tipStartOf(old, oldUnits) + tipStartOf(kept, newUnits);
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
newLine = at.newLine;
|
|
138
|
+
}
|
|
139
|
+
return { ...at, path: kept.path, newLine };
|
|
108
140
|
}
|
|
109
|
-
const
|
|
110
|
-
const path = old?.path ?? comment.context.path;
|
|
141
|
+
const path = old?.path ?? at.path;
|
|
111
142
|
const onPath = [...newUnits.values()].filter((u) => u.path === path);
|
|
112
143
|
const first = onPath[0];
|
|
144
|
+
const moved = { offsets: null, changed: true, path };
|
|
113
145
|
if (first === undefined)
|
|
114
|
-
return { ...
|
|
115
|
-
const moved = { comment, offsets: null, changed: true, path };
|
|
146
|
+
return { ...moved, unit: null, newLine: null };
|
|
116
147
|
const hunks = onPath.filter((u) => u.kind === "hunk");
|
|
117
148
|
if (old?.kind !== "hunk" || hunks.length === 0)
|
|
118
149
|
return { ...moved, unit: first.id, newLine: null };
|
|
119
|
-
const row =
|
|
150
|
+
const row = at.changed ? rowAt(old, oldUnits, at.newLine) : pinned.row(old);
|
|
120
151
|
const oldLine = tipLines(old, oldUnits)[row] ?? tipStartOf(old, oldUnits);
|
|
121
152
|
const nearest = hunks.reduce((a, b) => distance(b, newUnits, oldLine) < distance(a, newUnits, oldLine) ? b : a);
|
|
122
153
|
return {
|
|
123
154
|
...moved,
|
|
124
155
|
unit: nearest.id,
|
|
125
|
-
newLine: whole ? null : landing(nearest, newUnits, oldLine, old.body[row]?.text),
|
|
156
|
+
newLine: pinned.whole ? null : landing(nearest, newUnits, oldLine, old.body[row]?.text),
|
|
126
157
|
};
|
|
127
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Pure. Places each comment of the previous version on the new diff: on its own unit when that
|
|
161
|
+
* hunk is unchanged, else on the hunk of the same path nearest its old tip line.
|
|
162
|
+
*/
|
|
163
|
+
export function carryComments(comments, oldUnits, newUnits) {
|
|
164
|
+
return comments.map((comment) => ({
|
|
165
|
+
comment,
|
|
166
|
+
...move(placedOwn(comment), pinnedComment(comment), oldUnits, newUnits),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/** since stays once set; a first change records the version the code still matched. */
|
|
170
|
+
function sinceOf(before, after, since, at) {
|
|
171
|
+
return since ?? (after.changed && !before.changed ? at : null);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Pure. What an open review carries from version `at` into the next: what it carried already,
|
|
175
|
+
* moved again, then the comments said on `at`. Nothing is dropped, so a comment of v1 is still
|
|
176
|
+
* there after the second swap.
|
|
177
|
+
*/
|
|
178
|
+
export function carrySession(carried, said, at, oldUnits, newUnits) {
|
|
179
|
+
const again = carried.map((c) => {
|
|
180
|
+
const moved = move(c, pinnedComment(c.comment), oldUnits, newUnits);
|
|
181
|
+
return { ...c, ...moved, since: sinceOf(c, moved, c.since, at) };
|
|
182
|
+
});
|
|
183
|
+
const fresh = said.map((comment) => {
|
|
184
|
+
const own = placedOwn(comment);
|
|
185
|
+
const moved = move(own, pinnedComment(comment), oldUnits, newUnits);
|
|
186
|
+
return { comment, version: at, ...moved, since: sinceOf(own, moved, null, at) };
|
|
187
|
+
});
|
|
188
|
+
return [...again, ...fresh];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pure. Kept concerns of version `at` that the next plan no longer has, plus those carried before,
|
|
192
|
+
* placed on the new diff. A concern the next plan has again keeps its decision there instead, so
|
|
193
|
+
* it is left out here.
|
|
194
|
+
*/
|
|
195
|
+
export function carryConcerns(carried, kept, inNext, at, oldUnits, newUnits) {
|
|
196
|
+
const fresh = kept.map((k) => {
|
|
197
|
+
const unit = k.concern.unit ?? null;
|
|
198
|
+
const path = unit === null ? null : (oldUnits.get(unit)?.path ?? null);
|
|
199
|
+
return { ...k, unit, offsets: null, changed: false, path, newLine: null, since: null };
|
|
200
|
+
});
|
|
201
|
+
return [...carried, ...fresh]
|
|
202
|
+
.filter((c) => !inNext.has(c.concern.id))
|
|
203
|
+
.map((c) => {
|
|
204
|
+
const moved = move(c, pinnedConcern(c.concern), oldUnits, newUnits);
|
|
205
|
+
return { ...c, ...moved, since: sinceOf(c, moved, c.since, at) };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Pure. Where a carried hunk comment points on the diff it was carried to, as an anchor of that
|
|
210
|
+
* diff: its own rows when the hunk is unchanged, the row at its new line when it changed, the
|
|
211
|
+
* whole hunk when it has no line. Null when the code is gone or the comment is not on a hunk.
|
|
212
|
+
*/
|
|
213
|
+
export function anchorOn(carried, units, commit) {
|
|
214
|
+
const { anchor } = carried.comment;
|
|
215
|
+
if (anchor.kind !== "hunk" || carried.unit === null)
|
|
216
|
+
return null;
|
|
217
|
+
if (!carried.changed)
|
|
218
|
+
return { ...anchor, commit };
|
|
219
|
+
const hunk = units.get(carried.unit);
|
|
220
|
+
if (hunk?.kind !== "hunk" || carried.newLine === null) {
|
|
221
|
+
return { kind: "hunk", unit: carried.unit, commit, changes: null };
|
|
222
|
+
}
|
|
223
|
+
const row = rowAt(hunk, units, carried.newLine);
|
|
224
|
+
const at = hunk.body[row];
|
|
225
|
+
const changes = at === undefined || at.op === " " ? [] : [at.change];
|
|
226
|
+
return { kind: "hunk", unit: carried.unit, commit, changes, offsets: [row] };
|
|
227
|
+
}
|
|
228
|
+
/** Pure. A carried concern as a concern of the diff it was carried to; see anchorOn. */
|
|
229
|
+
export function concernOn(carried, units) {
|
|
230
|
+
const { concern } = carried;
|
|
231
|
+
if (carried.unit === null)
|
|
232
|
+
return concern;
|
|
233
|
+
if (!carried.changed)
|
|
234
|
+
return { ...concern, unit: carried.unit };
|
|
235
|
+
const { lines: _old, ...rest } = concern;
|
|
236
|
+
const hunk = units.get(carried.unit);
|
|
237
|
+
if (hunk?.kind !== "hunk" || carried.newLine === null)
|
|
238
|
+
return { ...rest, unit: carried.unit };
|
|
239
|
+
const at = hunk.body[rowAt(hunk, units, carried.newLine)];
|
|
240
|
+
return at === undefined || at.op === " "
|
|
241
|
+
? { ...rest, unit: carried.unit }
|
|
242
|
+
: { ...rest, unit: carried.unit, lines: [at.change] };
|
|
243
|
+
}
|