diffsplain 0.5.0 → 0.7.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 +17 -12
- package/dist/assets/{angular-html-CoQle5-T.js → angular-html-CphzFC_v.js} +1 -1
- package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-Am0-mbFw.js} +1 -1
- package/dist/assets/{apl-B7_4_FNq.js → apl-Cs2rPKDN.js} +1 -1
- package/dist/assets/{astro-CXVo4VE4.js → astro-CxKUUn9h.js} +1 -1
- package/dist/assets/{blade-BVX41dq7.js → blade-DqeCa6v5.js} +1 -1
- package/dist/assets/{c-Dhyi8Nd2.js → c-DdI6lknk.js} +1 -1
- package/dist/assets/{cobol-CU5oLeZj.js → cobol-COuAGE2X.js} +1 -1
- package/dist/assets/{coffee-D_FU6gC_.js → coffee-BFlYT3Lp.js} +1 -1
- package/dist/assets/{cpp-CxoYLSi3.js → cpp-nywLU0Np.js} +1 -1
- package/dist/assets/{crystal-BdgqBN3z.js → crystal-BWp4dz1s.js} +1 -1
- package/dist/assets/{css-wLk1vVwJ.js → css-CRb9tUpU.js} +1 -1
- package/dist/assets/{edge-DMylWoGl.js → edge-rUYSbJy4.js} +1 -1
- package/dist/assets/{elixir-Djmgto_Z.js → elixir-DOq9ybIH.js} +1 -1
- package/dist/assets/{elm-D7B-eBLA.js → elm-C8e38E5-.js} +1 -1
- package/dist/assets/{erb-g1sso1GW.js → erb-BR9Q37Et.js} +1 -1
- package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-Coo2LUtB.js} +1 -1
- package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-C7hX9jg6.js} +1 -1
- package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-CZLT0F0k.js} +1 -1
- package/dist/assets/{glsl-C8mS_t8I.js → glsl-BDZRv6e-.js} +1 -1
- package/dist/assets/{graphql-DW2pnvqi.js → graphql-DyzFc3y3.js} +1 -1
- package/dist/assets/{hack-DYc1d3he.js → hack-CWnO7WsF.js} +1 -1
- package/dist/assets/{haml-_5OmzH31.js → haml-CvDj-ghO.js} +1 -1
- package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BnU_YcVA.js} +1 -1
- package/dist/assets/{html-DQnaJUjl.js → html-CjB2TKKa.js} +1 -1
- package/dist/assets/{html-derivative-BovOubry.js → html-derivative-CYQqEXS6.js} +1 -1
- package/dist/assets/{http-BKTgwOYc.js → http-NNQbnqIx.js} +1 -1
- package/dist/assets/{hurl-ChS3_IgD.js → hurl-DHaAA1BS.js} +1 -1
- package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
- package/dist/assets/{index-CEglkSs-.js → index-f9ekN94b.js} +11 -11
- package/dist/assets/{java-Dgnyk--z.js → java-D1J5V2CD.js} +1 -1
- package/dist/assets/{javascript-BfnrwkRN.js → javascript-C3pX-HsE.js} +1 -1
- package/dist/assets/{jinja-CD2AIvOi.js → jinja-BY9CIY8g.js} +1 -1
- package/dist/assets/{jison-DmKNw3Pl.js → jison-Cr0H89gS.js} +1 -1
- package/dist/assets/{json-CKB5a8aj.js → json-CA3kKNfq.js} +1 -1
- package/dist/assets/{jsx-jJXrDRBM.js → jsx-BuMkR-Lj.js} +1 -1
- package/dist/assets/{julia-G8vVBW-t.js → julia-C2sLwg3f.js} +1 -1
- package/dist/assets/{just-DMXLrLZL.js → just-pXpkLBXv.js} +1 -1
- package/dist/assets/{latex-DS3xuxld.js → latex-7v3EPOdF.js} +1 -1
- package/dist/assets/{liquid-COQnD_C4.js → liquid-CJZq0Z0t.js} +1 -1
- package/dist/assets/{lua-BwMKwosd.js → lua-BimBzvTH.js} +1 -1
- package/dist/assets/{marko-cQCK_WIk.js → marko-CCZHVnQN.js} +1 -1
- package/dist/assets/{mdc-DQUjY13N.js → mdc-SLECNw2A.js} +1 -1
- package/dist/assets/{nginx-C4Kv3DJi.js → nginx-DUwtUge2.js} +1 -1
- package/dist/assets/{nim-ChzLzuYD.js → nim-HD8o_Omp.js} +1 -1
- package/dist/assets/{perl-z5xUiWAY.js → perl-BGMeIkNk.js} +1 -1
- package/dist/assets/{php-BHqm-Xeh.js → php-BL5xuY-v.js} +1 -1
- package/dist/assets/{pug-C9C_Llft.js → pug-C8G1U_u8.js} +1 -1
- package/dist/assets/{qml-dtpuwXJZ.js → qml-BGVSyw1x.js} +1 -1
- package/dist/assets/{r-BicQMs_c.js → r-BDnRcclC.js} +1 -1
- package/dist/assets/{razor-D5h00YTZ.js → razor-I8tbEIVJ.js} +1 -1
- package/dist/assets/{regexp-CTa8S86v.js → regexp-D0yakB9Q.js} +1 -1
- package/dist/assets/{rst-Bg-b-7HK.js → rst-DbTCn07T.js} +1 -1
- package/dist/assets/{ruby-BiWYm056.js → ruby-BwgPFIcO.js} +1 -1
- package/dist/assets/{sas-bJTdfgUz.js → sas-dN4ePfXU.js} +1 -1
- package/dist/assets/{scss-0qkNyHr1.js → scss-T7M1A4iM.js} +1 -1
- package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-HFjBHS7_.js} +1 -1
- package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-DCAGskrx.js} +1 -1
- package/dist/assets/{soy-DG9hUwLL.js → soy-BgT96TF9.js} +1 -1
- package/dist/assets/{sql-B0UoKHkx.js → sql-DCIQeRzx.js} +1 -1
- package/dist/assets/{stata-CsQbuXQp.js → stata-Dn0tad0_.js} +1 -1
- package/dist/assets/{surrealql-D63VLxos.js → surrealql-CCgFEQfW.js} +1 -1
- package/dist/assets/{svelte-BApdnAma.js → svelte-ITXimG8u.js} +1 -1
- package/dist/assets/{templ-B64pN6Lz.js → templ-DJVZZqPw.js} +1 -1
- package/dist/assets/{tex-DR2UVIPF.js → tex-Bn4N-OtH.js} +1 -1
- package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CxQ4PoC4.js} +1 -1
- package/dist/assets/{tsx-DROdxqzS.js → tsx-CxwgcjkC.js} +1 -1
- package/dist/assets/{twig-BtbUqpR4.js → twig-CtbyTyMH.js} +1 -1
- package/dist/assets/{typescript-CHpSjYZL.js → typescript-d6ru12L8.js} +1 -1
- package/dist/assets/{vue-DCvEu_ug.js → vue-RvqAsYDm.js} +1 -1
- package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BkHDVENe.js} +1 -1
- package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-DvS3Mlkn.js} +1 -1
- package/dist/assets/{xml-CkfPxZuz.js → xml-lzTq5A-V.js} +1 -1
- package/dist/assets/{xsl-CteaoFSV.js → xsl-BL0KXNWI.js} +1 -1
- package/dist/assets/{yaml-CUr3TDRN.js → yaml-CF4XlBJS.js} +1 -1
- package/dist/demo-diff-data.json +6 -1
- package/dist/index.html +2 -2
- package/package.json +50 -23
- package/scripts/access-token.mjs +21 -0
- package/scripts/build-diff-data.mjs +260 -39
- package/scripts/cache.mjs +301 -0
- package/scripts/cli-args.mjs +206 -57
- package/scripts/coding-agents.mjs +279 -162
- package/scripts/dev.mjs +218 -0
- package/scripts/doctor.mjs +265 -46
- package/scripts/generate-summaries.mjs +760 -260
- package/scripts/mock-agent.mjs +165 -0
- package/scripts/present.mjs +394 -85
- package/scripts/presenter-runtime.mjs +148 -0
- package/scripts/serve-built.mjs +343 -71
- package/scripts/summary-path.mjs +24 -7
- package/scripts/support-record.mjs +242 -0
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import {
|
|
6
|
+
closeSync,
|
|
7
|
+
constants,
|
|
6
8
|
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
lstatSync,
|
|
7
11
|
mkdirSync,
|
|
12
|
+
openSync,
|
|
8
13
|
readFileSync,
|
|
14
|
+
readlinkSync,
|
|
9
15
|
renameSync,
|
|
10
16
|
statSync,
|
|
11
17
|
writeFileSync,
|
|
12
18
|
} from 'node:fs';
|
|
13
|
-
import { dirname, relative, resolve } from 'node:path';
|
|
19
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
14
20
|
import { fileURLToPath } from 'node:url';
|
|
15
21
|
import { summaryPath } from './summary-path.mjs';
|
|
16
22
|
|
|
@@ -26,6 +32,10 @@ const option = (name) => {
|
|
|
26
32
|
if (!value || value.startsWith('--')) fail(`${name} needs a value`);
|
|
27
33
|
return value;
|
|
28
34
|
};
|
|
35
|
+
const options = (name) =>
|
|
36
|
+
args.flatMap((argument, index) =>
|
|
37
|
+
argument === name && args[index + 1] ? [args[index + 1]] : [],
|
|
38
|
+
);
|
|
29
39
|
const has = (name) => args.includes(name);
|
|
30
40
|
|
|
31
41
|
if (has('--help')) {
|
|
@@ -52,7 +62,7 @@ Options:
|
|
|
52
62
|
|
|
53
63
|
const repo = resolve(option('--repo') || process.cwd());
|
|
54
64
|
const output = resolve(option('--output') || '.cache/diff-data.json');
|
|
55
|
-
const
|
|
65
|
+
const excludedOutputs = options('--exclude-output');
|
|
56
66
|
const baseOption = option('--base');
|
|
57
67
|
const headOption = option('--head');
|
|
58
68
|
const prOption = option('--pr');
|
|
@@ -79,7 +89,18 @@ const cacheRoot = cacheOption
|
|
|
79
89
|
: resolve(projectRoot, '.cache/git');
|
|
80
90
|
const remoteMode = Boolean(prOption || branchOption);
|
|
81
91
|
const watching = has('--watch');
|
|
92
|
+
const watchContent = has('--watch-content');
|
|
82
93
|
const ignoreSummaryWatch = has('--ignore-summary-watch');
|
|
94
|
+
const noSummaries = has('--no-summaries');
|
|
95
|
+
const interval = (name, fallback) => {
|
|
96
|
+
const value = Number(process.env[name]);
|
|
97
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
98
|
+
};
|
|
99
|
+
const watchInterval = interval('DIFFSPLAIN_WATCH_INTERVAL_MS', 2_000);
|
|
100
|
+
const remoteRefreshInterval = interval(
|
|
101
|
+
'DIFFSPLAIN_REMOTE_REFRESH_INTERVAL_MS',
|
|
102
|
+
30_000,
|
|
103
|
+
);
|
|
83
104
|
|
|
84
105
|
if (prOption && branchOption) fail('--pr and --branch cannot be used together');
|
|
85
106
|
if (prOption && (baseOption || headOption)) fail('--pr cannot be used with --base or --head');
|
|
@@ -109,11 +130,13 @@ const repoPath = (file) => {
|
|
|
109
130
|
const path = relative(repo, file).replaceAll('\\', '/');
|
|
110
131
|
return path && path !== '..' && !path.startsWith('../') ? path : undefined;
|
|
111
132
|
};
|
|
133
|
+
const summariesRepoPath = repoPath(summariesPath);
|
|
112
134
|
const excludedPaths = new Set(
|
|
113
135
|
[
|
|
114
|
-
|
|
136
|
+
summariesRepoPath,
|
|
137
|
+
summariesRepoPath ? `${summariesRepoPath}.lock` : undefined,
|
|
115
138
|
repoPath(output),
|
|
116
|
-
|
|
139
|
+
...excludedOutputs.map((path) => repoPath(resolve(path))),
|
|
117
140
|
].filter(Boolean),
|
|
118
141
|
);
|
|
119
142
|
|
|
@@ -176,6 +199,22 @@ const completeChangeSummary = (value) =>
|
|
|
176
199
|
completeText(value.why) &&
|
|
177
200
|
completeList(value.highlights) &&
|
|
178
201
|
completeList(value.risks);
|
|
202
|
+
const failedFileRecords = (value) =>
|
|
203
|
+
Array.isArray(value)
|
|
204
|
+
? value
|
|
205
|
+
.filter(
|
|
206
|
+
(item) =>
|
|
207
|
+
item &&
|
|
208
|
+
typeof item.path === 'string' &&
|
|
209
|
+
item.path.trim() &&
|
|
210
|
+
typeof item.reason === 'string' &&
|
|
211
|
+
item.reason.trim(),
|
|
212
|
+
)
|
|
213
|
+
.map((item) => ({
|
|
214
|
+
path: item.path.trim(),
|
|
215
|
+
reason: item.reason.trim(),
|
|
216
|
+
}))
|
|
217
|
+
: [];
|
|
179
218
|
|
|
180
219
|
function fileSummary(path, value) {
|
|
181
220
|
return {
|
|
@@ -311,12 +350,24 @@ function normalizeBranch(value, remoteName) {
|
|
|
311
350
|
}
|
|
312
351
|
|
|
313
352
|
function resolveRemote() {
|
|
314
|
-
const configured = tryRepo([
|
|
315
|
-
|
|
353
|
+
const configured = tryRepo([
|
|
354
|
+
'config',
|
|
355
|
+
'--get-all',
|
|
356
|
+
`remote.${remoteOption}.url`,
|
|
357
|
+
])
|
|
358
|
+
.split('\n')
|
|
359
|
+
.find(Boolean);
|
|
360
|
+
if (configured) {
|
|
361
|
+
return {
|
|
362
|
+
name: remoteOption,
|
|
363
|
+
url: configured,
|
|
364
|
+
fetchUrl: tryRepo(['remote', 'get-url', remoteOption]) || configured,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
316
367
|
if (remoteOption === 'origin') {
|
|
317
368
|
throw new Error(`Git remote "origin" was not found in ${repo}`);
|
|
318
369
|
}
|
|
319
|
-
return { name: remoteOption, url: remoteOption };
|
|
370
|
+
return { name: remoteOption, url: remoteOption, fetchUrl: remoteOption };
|
|
320
371
|
}
|
|
321
372
|
|
|
322
373
|
function bareCache(remoteUrl) {
|
|
@@ -387,6 +438,34 @@ function remoteDefaultBranch(remoteUrl) {
|
|
|
387
438
|
return remoteDefaultBranchInfo(remoteUrl).name;
|
|
388
439
|
}
|
|
389
440
|
|
|
441
|
+
function remoteContainsCommits(remoteUrl, commits) {
|
|
442
|
+
let raw;
|
|
443
|
+
try {
|
|
444
|
+
raw = runRepo(['ls-remote', remoteUrl]);
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const tips = [
|
|
449
|
+
...new Set(
|
|
450
|
+
raw
|
|
451
|
+
.split('\n')
|
|
452
|
+
.map((line) => line.trim().split(/\s+/, 1)[0])
|
|
453
|
+
.filter(Boolean),
|
|
454
|
+
),
|
|
455
|
+
];
|
|
456
|
+
return commits.every((commit) =>
|
|
457
|
+
tips.some((tip) => {
|
|
458
|
+
if (tip === commit) return true;
|
|
459
|
+
const result = spawnSync(
|
|
460
|
+
'git',
|
|
461
|
+
['-C', repo, 'merge-base', '--is-ancestor', commit, tip],
|
|
462
|
+
{ stdio: 'ignore' },
|
|
463
|
+
);
|
|
464
|
+
return result.status === 0;
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
390
469
|
function localDefaultBranch(remote) {
|
|
391
470
|
if (baseOption) return { name: baseOption };
|
|
392
471
|
|
|
@@ -506,7 +585,7 @@ function resolveBranchTarget() {
|
|
|
506
585
|
const remote = resolveRemote();
|
|
507
586
|
const branch = normalizeBranch(branchOption, remote.name);
|
|
508
587
|
const baseBranch = normalizeBranch(
|
|
509
|
-
baseOption || remoteDefaultBranch(remote.
|
|
588
|
+
baseOption || remoteDefaultBranch(remote.fetchUrl),
|
|
510
589
|
remote.name,
|
|
511
590
|
);
|
|
512
591
|
const cache = bareCache(remote.url);
|
|
@@ -516,7 +595,7 @@ function resolveBranchTarget() {
|
|
|
516
595
|
.slice(0, 16);
|
|
517
596
|
const baseRef = `refs/diffsplain/branch/${key}/base`;
|
|
518
597
|
const headRef = `refs/diffsplain/branch/${key}/head`;
|
|
519
|
-
fetchInto(cache, remote.
|
|
598
|
+
fetchInto(cache, remote.fetchUrl, [
|
|
520
599
|
`+refs/heads/${baseBranch}:${baseRef}`,
|
|
521
600
|
`+refs/heads/${branch}:${headRef}`,
|
|
522
601
|
]);
|
|
@@ -535,6 +614,7 @@ function resolveBranchTarget() {
|
|
|
535
614
|
remote,
|
|
536
615
|
sourceRepositoryUrl: repository?.webUrl,
|
|
537
616
|
baseRepositoryUrl: repository?.webUrl,
|
|
617
|
+
comparisonCommitsOnRemote: true,
|
|
538
618
|
target: {
|
|
539
619
|
kind: 'branch',
|
|
540
620
|
remote: remote.name,
|
|
@@ -562,7 +642,7 @@ function resolvePullRequestTarget() {
|
|
|
562
642
|
.slice(0, 16);
|
|
563
643
|
const baseRef = `refs/diffsplain/pr/${key}/base`;
|
|
564
644
|
const headRef = `refs/diffsplain/pr/${key}/head`;
|
|
565
|
-
fetchInto(cache, remote.
|
|
645
|
+
fetchInto(cache, remote.fetchUrl, [
|
|
566
646
|
`+refs/heads/${pr.baseRefName}:${baseRef}`,
|
|
567
647
|
`+refs/pull/${pr.number}/head:${headRef}`,
|
|
568
648
|
]);
|
|
@@ -600,6 +680,7 @@ function resolvePullRequestTarget() {
|
|
|
600
680
|
pr.url.replace(/\/pull\/\d+(?:\/.*)?$/, ''),
|
|
601
681
|
baseRepositoryUrl:
|
|
602
682
|
repository?.webUrl || pr.url.replace(/\/pull\/\d+(?:\/.*)?$/, ''),
|
|
683
|
+
comparisonCommitsOnRemote: true,
|
|
603
684
|
target: {
|
|
604
685
|
kind: 'pull-request',
|
|
605
686
|
remote: remote.name,
|
|
@@ -646,6 +727,9 @@ function resolveCheckoutTarget() {
|
|
|
646
727
|
const repository = githubRepository(remoteUrl);
|
|
647
728
|
const headLabel = branch || currentHead;
|
|
648
729
|
const hasCommittedChanges = mergeBaseOid !== currentHead;
|
|
730
|
+
const hasUncommittedChanges = Boolean(
|
|
731
|
+
tryRepo(['status', '--porcelain=v1', '-z']),
|
|
732
|
+
);
|
|
649
733
|
const isDefaultBranchCheckout = branch === defaultBranch.name;
|
|
650
734
|
|
|
651
735
|
return {
|
|
@@ -659,6 +743,12 @@ function resolveCheckoutTarget() {
|
|
|
659
743
|
remote,
|
|
660
744
|
sourceRepositoryUrl: repository?.webUrl,
|
|
661
745
|
baseRepositoryUrl: repository?.webUrl,
|
|
746
|
+
comparisonCommitsOnRemote:
|
|
747
|
+
!hasUncommittedChanges &&
|
|
748
|
+
Boolean(
|
|
749
|
+
remote?.url &&
|
|
750
|
+
remoteContainsCommits(remote.url, [mergeBaseOid, currentHead]),
|
|
751
|
+
),
|
|
662
752
|
target: {
|
|
663
753
|
kind: 'checkout',
|
|
664
754
|
...(remote ? { remote: remote.name } : {}),
|
|
@@ -712,6 +802,12 @@ function resolveLocalTarget() {
|
|
|
712
802
|
baseRepositoryUrl: worktree
|
|
713
803
|
? undefined
|
|
714
804
|
: githubRepository(remoteUrl)?.webUrl,
|
|
805
|
+
comparisonCommitsOnRemote:
|
|
806
|
+
!worktree &&
|
|
807
|
+
Boolean(
|
|
808
|
+
remoteUrl &&
|
|
809
|
+
remoteContainsCommits(remoteUrl, [resolvedBase, resolvedHead]),
|
|
810
|
+
),
|
|
715
811
|
target: worktree
|
|
716
812
|
? { kind: 'worktree', base: { ref: 'HEAD', oid: currentHead || null } }
|
|
717
813
|
: {
|
|
@@ -812,6 +908,13 @@ function githubFileUrl(repositoryUrl, ref, path) {
|
|
|
812
908
|
return `${repositoryUrl}/blob/${encodeURIComponent(ref)}/${filePath}`;
|
|
813
909
|
}
|
|
814
910
|
|
|
911
|
+
function githubComparisonUrl(repositoryUrl, base, head) {
|
|
912
|
+
if (!repositoryUrl || !base || !head || head === 'WORKTREE') {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
return `${repositoryUrl}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
815
918
|
function build() {
|
|
816
919
|
const localWorkspace =
|
|
817
920
|
tryRepo(['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
@@ -820,7 +923,7 @@ function build() {
|
|
|
820
923
|
}
|
|
821
924
|
const target = resolveTarget();
|
|
822
925
|
const remoteRepository = githubRepository(target.remote?.url);
|
|
823
|
-
const summaryDoc = readJson(summariesPath, {}) || {};
|
|
926
|
+
const summaryDoc = noSummaries ? {} : readJson(summariesPath, {}) || {};
|
|
824
927
|
const allTrackedFiles = parseNameStatus(
|
|
825
928
|
target.runGit([
|
|
826
929
|
'diff',
|
|
@@ -887,6 +990,13 @@ function build() {
|
|
|
887
990
|
file.status === 'deleted' ? target.base : target.head,
|
|
888
991
|
file.path,
|
|
889
992
|
);
|
|
993
|
+
const comparisonUrl = githubComparisonUrl(
|
|
994
|
+
target.comparisonCommitsOnRemote
|
|
995
|
+
? target.sourceRepositoryUrl
|
|
996
|
+
: undefined,
|
|
997
|
+
target.base,
|
|
998
|
+
target.head,
|
|
999
|
+
);
|
|
890
1000
|
return {
|
|
891
1001
|
path: file.path,
|
|
892
1002
|
...(file.oldPath ? { oldPath: file.oldPath } : {}),
|
|
@@ -899,6 +1009,7 @@ function build() {
|
|
|
899
1009
|
patch: textPatch,
|
|
900
1010
|
snippet: binary ? '' : compactSnippet(textPatch),
|
|
901
1011
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
1012
|
+
...(comparisonUrl ? { comparisonUrl } : {}),
|
|
902
1013
|
};
|
|
903
1014
|
});
|
|
904
1015
|
|
|
@@ -926,39 +1037,64 @@ function build() {
|
|
|
926
1037
|
)
|
|
927
1038
|
.digest('hex');
|
|
928
1039
|
const generatedFor =
|
|
1040
|
+
!noSummaries &&
|
|
929
1041
|
typeof summaryDoc.meta?.reviewFingerprint === 'string'
|
|
930
1042
|
? summaryDoc.meta.reviewFingerprint
|
|
931
1043
|
: undefined;
|
|
932
|
-
const summariesAreFresh =
|
|
1044
|
+
const summariesAreFresh =
|
|
1045
|
+
!noSummaries && (!generatedFor || generatedFor === reviewFingerprint);
|
|
933
1046
|
const sourceSummaries = summariesAreFresh ? summaryDoc : {};
|
|
1047
|
+
const failedFiles = summariesAreFresh
|
|
1048
|
+
? failedFileRecords(summaryDoc.meta?.failedFiles)
|
|
1049
|
+
: [];
|
|
1050
|
+
const summaryErrors = summariesAreFresh
|
|
1051
|
+
? cleanList(summaryDoc.meta?.errors)
|
|
1052
|
+
: [];
|
|
1053
|
+
const failureByPath = new Map(
|
|
1054
|
+
failedFiles.map((failure) => [failure.path, failure.reason]),
|
|
1055
|
+
);
|
|
1056
|
+
const emptyReviewComplete =
|
|
1057
|
+
filesWithoutSummaries.length === 0 &&
|
|
1058
|
+
generatedFor === reviewFingerprint &&
|
|
1059
|
+
sourceSummaries.meta?.status === 'complete';
|
|
934
1060
|
const summariesAreComplete =
|
|
935
1061
|
summariesAreFresh &&
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1062
|
+
failedFiles.length === 0 &&
|
|
1063
|
+
summaryErrors.length === 0 &&
|
|
1064
|
+
(emptyReviewComplete ||
|
|
1065
|
+
(completeChangeSummary(sourceSummaries.change) &&
|
|
1066
|
+
filesWithoutSummaries.every((file) =>
|
|
1067
|
+
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1068
|
+
)));
|
|
1069
|
+
const completedFiles = noSummaries
|
|
1070
|
+
? 0
|
|
1071
|
+
: filesWithoutSummaries.filter((file) =>
|
|
1072
|
+
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1073
|
+
).length;
|
|
943
1074
|
const storedStatus = summaryDoc.meta?.status;
|
|
944
|
-
const noteStatus =
|
|
945
|
-
? '
|
|
946
|
-
:
|
|
947
|
-
? '
|
|
948
|
-
:
|
|
949
|
-
?
|
|
950
|
-
: '
|
|
1075
|
+
const noteStatus = noSummaries
|
|
1076
|
+
? 'idle'
|
|
1077
|
+
: summariesAreComplete
|
|
1078
|
+
? 'complete'
|
|
1079
|
+
: !summariesAreFresh
|
|
1080
|
+
? 'stale'
|
|
1081
|
+
: ['generating', 'failed'].includes(storedStatus)
|
|
1082
|
+
? storedStatus
|
|
1083
|
+
: 'idle';
|
|
951
1084
|
const files = filesWithoutSummaries.map((file) => ({
|
|
952
1085
|
...file,
|
|
953
1086
|
summary: fileSummary(file.path, sourceSummaries.files?.[file.path]),
|
|
954
1087
|
noteReady: Boolean(
|
|
955
1088
|
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
956
1089
|
),
|
|
1090
|
+
...(failureByPath.has(file.path)
|
|
1091
|
+
? { noteFailure: failureByPath.get(file.path) }
|
|
1092
|
+
: {}),
|
|
957
1093
|
}));
|
|
958
1094
|
const change = changeSummary(sourceSummaries.change, target.changeDefaults);
|
|
959
1095
|
const content = {
|
|
960
1096
|
repo: {
|
|
961
|
-
name: remoteRepository?.name || repo
|
|
1097
|
+
name: remoteRepository?.name || basename(repo),
|
|
962
1098
|
root: localWorkspace ? repo : target.remote?.url || repo,
|
|
963
1099
|
base: target.base,
|
|
964
1100
|
head: target.head,
|
|
@@ -979,9 +1115,14 @@ function build() {
|
|
|
979
1115
|
status: noteStatus,
|
|
980
1116
|
completedFiles,
|
|
981
1117
|
totalFiles: filesWithoutSummaries.length,
|
|
1118
|
+
...(failedFiles.length ? { failedFiles } : {}),
|
|
1119
|
+
...(summaryErrors.length ? { errors: summaryErrors } : {}),
|
|
982
1120
|
...(typeof summaryDoc.meta?.model === 'string'
|
|
983
1121
|
? { model: summaryDoc.meta.model }
|
|
984
1122
|
: {}),
|
|
1123
|
+
...(typeof summaryDoc.meta?.agent === 'string'
|
|
1124
|
+
? { agent: summaryDoc.meta.agent }
|
|
1125
|
+
: {}),
|
|
985
1126
|
...(typeof summaryDoc.meta?.reasoning === 'string'
|
|
986
1127
|
? { reasoning: summaryDoc.meta.reasoning }
|
|
987
1128
|
: {}),
|
|
@@ -1011,9 +1152,52 @@ function build() {
|
|
|
1011
1152
|
return true;
|
|
1012
1153
|
}
|
|
1013
1154
|
|
|
1155
|
+
function untrackedFileKind(stat) {
|
|
1156
|
+
if (stat.isFile()) return 'file';
|
|
1157
|
+
if (stat.isSymbolicLink()) return 'symlink';
|
|
1158
|
+
if (stat.isDirectory()) return 'directory';
|
|
1159
|
+
return 'other';
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function fingerprintUntrackedPath(content, path) {
|
|
1163
|
+
const absolutePath = resolve(repo, path);
|
|
1164
|
+
const stat = lstatSync(absolutePath, { bigint: true });
|
|
1165
|
+
content.update(path);
|
|
1166
|
+
content.update('\0');
|
|
1167
|
+
content.update(untrackedFileKind(stat));
|
|
1168
|
+
content.update('\0');
|
|
1169
|
+
content.update(String(stat.size));
|
|
1170
|
+
content.update('\0');
|
|
1171
|
+
content.update(String(stat.mtimeNs));
|
|
1172
|
+
content.update('\0');
|
|
1173
|
+
if (stat.isFile()) {
|
|
1174
|
+
const descriptor = openSync(
|
|
1175
|
+
absolutePath,
|
|
1176
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
1177
|
+
);
|
|
1178
|
+
try {
|
|
1179
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
1180
|
+
if (
|
|
1181
|
+
!opened.isFile()
|
|
1182
|
+
|| opened.dev !== stat.dev
|
|
1183
|
+
|| opened.ino !== stat.ino
|
|
1184
|
+
) {
|
|
1185
|
+
throw new Error(`${path} changed while its fingerprint was read`);
|
|
1186
|
+
}
|
|
1187
|
+
content.update(readFileSync(descriptor));
|
|
1188
|
+
} finally {
|
|
1189
|
+
closeSync(descriptor);
|
|
1190
|
+
}
|
|
1191
|
+
content.update('\0');
|
|
1192
|
+
} else if (stat.isSymbolicLink()) {
|
|
1193
|
+
content.update(readlinkSync(absolutePath));
|
|
1194
|
+
content.update('\0');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1014
1198
|
function fingerprint() {
|
|
1015
1199
|
let summariesTime = '';
|
|
1016
|
-
if (!ignoreSummaryWatch) {
|
|
1200
|
+
if (!ignoreSummaryWatch && !noSummaries) {
|
|
1017
1201
|
try {
|
|
1018
1202
|
summariesTime = String(statSync(summariesPath).mtimeMs);
|
|
1019
1203
|
} catch {}
|
|
@@ -1026,9 +1210,26 @@ function fingerprint() {
|
|
|
1026
1210
|
summariesTime,
|
|
1027
1211
|
].join('|');
|
|
1028
1212
|
}
|
|
1213
|
+
const content = createHash('sha256');
|
|
1214
|
+
content.update(
|
|
1215
|
+
tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD', '--']),
|
|
1216
|
+
);
|
|
1217
|
+
const untracked = tryRepo([
|
|
1218
|
+
'ls-files',
|
|
1219
|
+
'--others',
|
|
1220
|
+
'--exclude-standard',
|
|
1221
|
+
'-z',
|
|
1222
|
+
])
|
|
1223
|
+
.split('\0')
|
|
1224
|
+
.filter((path) => path && !excludedPaths.has(path))
|
|
1225
|
+
.sort();
|
|
1226
|
+
for (const path of untracked) {
|
|
1227
|
+
fingerprintUntrackedPath(content, path);
|
|
1228
|
+
}
|
|
1029
1229
|
return [
|
|
1030
1230
|
tryRepo(['rev-parse', 'HEAD']),
|
|
1031
1231
|
tryRepo(['status', '--porcelain=v1', '--untracked-files=all']),
|
|
1232
|
+
content.digest('hex'),
|
|
1032
1233
|
summariesTime,
|
|
1033
1234
|
].join('|');
|
|
1034
1235
|
}
|
|
@@ -1040,25 +1241,45 @@ const refresh = () => {
|
|
|
1040
1241
|
return true;
|
|
1041
1242
|
} catch (error) {
|
|
1042
1243
|
console.error(error.message);
|
|
1043
|
-
|
|
1244
|
+
process.exitCode = 1;
|
|
1044
1245
|
return false;
|
|
1045
1246
|
}
|
|
1046
1247
|
};
|
|
1047
1248
|
|
|
1048
1249
|
const started = refresh();
|
|
1049
1250
|
if (watching && started) {
|
|
1050
|
-
let last
|
|
1251
|
+
let last;
|
|
1051
1252
|
let remoteWait = 0;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1253
|
+
let watcher;
|
|
1254
|
+
const stopWatching = (error) => {
|
|
1255
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1256
|
+
process.exitCode = 1;
|
|
1257
|
+
if (watcher) clearInterval(watcher);
|
|
1258
|
+
};
|
|
1259
|
+
const poll = () => {
|
|
1260
|
+
try {
|
|
1261
|
+
const next = fingerprint();
|
|
1262
|
+
if (last === undefined) {
|
|
1263
|
+
last = next;
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
remoteWait += watchInterval;
|
|
1267
|
+
const remoteDue = remoteMode && remoteWait >= remoteRefreshInterval;
|
|
1268
|
+
if (next !== last || remoteDue || watchContent) {
|
|
1269
|
+
last = next;
|
|
1270
|
+
remoteWait = 0;
|
|
1271
|
+
if (!refresh()) {
|
|
1272
|
+
clearInterval(watcher);
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return true;
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
stopWatching(error);
|
|
1279
|
+
return false;
|
|
1060
1280
|
}
|
|
1061
|
-
}
|
|
1281
|
+
};
|
|
1282
|
+
if (poll()) watcher = setInterval(poll, watchInterval);
|
|
1062
1283
|
} else if (watching) {
|
|
1063
1284
|
process.exitCode = 1;
|
|
1064
1285
|
}
|