@kgdiff/cli 0.4.0 → 0.5.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 +21 -4
- package/dist/args.d.ts +5 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +6 -0
- package/dist/args.js.map +1 -1
- package/dist/diff-command.d.ts.map +1 -1
- package/dist/diff-command.js +7 -1
- package/dist/diff-command.js.map +1 -1
- package/dist/errors.d.ts +8 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -4
- package/dist/errors.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +5 -0
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +5 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/locale.d.ts +34 -1
- package/dist/locale.d.ts.map +1 -1
- package/dist/locale.js +65 -0
- package/dist/locale.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/markdown.d.ts +12 -2
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -11
- package/dist/markdown.js.map +1 -1
- package/dist/model.d.ts +42 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +82 -0
- package/dist/model.js.map +1 -1
- package/dist/node-io.d.ts.map +1 -1
- package/dist/node-io.js +1 -0
- package/dist/node-io.js.map +1 -1
- package/dist/report.d.ts +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +3 -3
- package/dist/report.js.map +1 -1
- package/dist/summary.d.ts +19 -4
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +105 -19
- package/dist/summary.js.map +1 -1
- package/package.json +6 -6
- package/src/args.ts +14 -0
- package/src/diff-command.ts +7 -1
- package/src/errors.ts +30 -4
- package/src/help.ts +5 -0
- package/src/index.ts +2 -0
- package/src/io.ts +5 -0
- package/src/locale.ts +123 -1
- package/src/main.ts +5 -2
- package/src/markdown.ts +128 -12
- package/src/model.ts +115 -2
- package/src/node-io.ts +1 -0
- package/src/report.ts +3 -3
- package/src/summary.ts +145 -22
package/src/summary.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminal outputs: the default summary (header, counters, diffstat bar, hunk list
|
|
3
|
-
* and `--name-status` (docs/cli.md §Terminal summary).
|
|
2
|
+
* Terminal outputs: the default summary (header, counters, diffstat bar, hunk list, possible
|
|
3
|
+
* matches), `--stat` and `--name-status` (docs/cli.md §Terminal summary).
|
|
4
4
|
*
|
|
5
5
|
* Every line is laid out on plain text first (measured with `stringWidth`, truncated with an
|
|
6
6
|
* ellipsis) and painted last, so alignment holds with and without color and with CJK labels.
|
|
7
7
|
*/
|
|
8
8
|
import { type DiffResult, formatHunk, type Hunk, type Locale } from '@kgdiff/core';
|
|
9
|
-
import { formatInt, type Messages, messages } from './locale.js';
|
|
9
|
+
import { formatInt, type Messages, messages, watchLabels } from './locale.js';
|
|
10
10
|
import {
|
|
11
11
|
apportion,
|
|
12
|
+
type ChangeFamily,
|
|
12
13
|
COUNTER_PHASES,
|
|
14
|
+
DEFAULT_WATCH_ATTRS,
|
|
13
15
|
groupByPhase,
|
|
14
16
|
hunkFamily,
|
|
15
17
|
hunkGlyph,
|
|
@@ -18,6 +20,10 @@ import {
|
|
|
18
20
|
overview,
|
|
19
21
|
PHASE_FAMILY,
|
|
20
22
|
PHASE_GLYPH,
|
|
23
|
+
type PossibleMatch,
|
|
24
|
+
percent,
|
|
25
|
+
possibleMatches,
|
|
26
|
+
watchedChanges,
|
|
21
27
|
} from './model.js';
|
|
22
28
|
import type { Painter } from './style.js';
|
|
23
29
|
import { padEnd, padStart, sanitize, stringWidth, truncate, wrap } from './width.js';
|
|
@@ -29,6 +35,35 @@ export interface TextContext {
|
|
|
29
35
|
readonly width: number;
|
|
30
36
|
/** Maximum hunks listed in the summary; 0 lists all. */
|
|
31
37
|
readonly limit: number;
|
|
38
|
+
/**
|
|
39
|
+
* Attribute keys whose changes are flagged ⚑ for review first (`--watch-attr`). Default
|
|
40
|
+
* `DEFAULT_WATCH_ATTRS` (definitions); `[]` flags nothing.
|
|
41
|
+
*/
|
|
42
|
+
readonly watch?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Flag glyph of hunks that change a watched attribute. */
|
|
46
|
+
const FLAG = '⚑';
|
|
47
|
+
|
|
48
|
+
/** The watched attributes each hunk changes, for `ctx.watch` or the default definitions. */
|
|
49
|
+
function watchedOf(diff: DiffResult, ctx: TextContext): ReadonlyMap<string, readonly string[]> {
|
|
50
|
+
return watchedChanges(diff, ctx.watch ?? DEFAULT_WATCH_ATTRS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `⚑ Review first: definition changed in 10 hunks`, under the diffstat bar; none if no flags. */
|
|
54
|
+
function reviewFirstLines(
|
|
55
|
+
watched: ReadonlyMap<string, readonly string[]>,
|
|
56
|
+
ctx: TextContext,
|
|
57
|
+
width: number,
|
|
58
|
+
): string[] {
|
|
59
|
+
if (watched.size === 0) return [];
|
|
60
|
+
const m = messages(ctx.locale);
|
|
61
|
+
const keys = new Set<string>();
|
|
62
|
+
for (const list of watched.values()) for (const key of list) keys.add(key);
|
|
63
|
+
const watchOrder = (ctx.watch ?? DEFAULT_WATCH_ATTRS).filter((k) => keys.has(k));
|
|
64
|
+
const text = m.watch.line(watchLabels(watchOrder, m), watched.size);
|
|
65
|
+
const room = width - INDENT.length - stringWidth(FLAG) - 1;
|
|
66
|
+
return [`${INDENT}${ctx.paint.tone('accent', FLAG)} ${truncate(sanitize(text), room)}`];
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
const INDENT = ' ';
|
|
@@ -135,7 +170,13 @@ export function diffstatBar(ov: Overview, width: number, paint: Painter): string
|
|
|
135
170
|
}).join('');
|
|
136
171
|
}
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
/** Counters, the diffstat bar, and the review-first line when a watched attribute changed. */
|
|
174
|
+
function statLines(
|
|
175
|
+
ov: Overview,
|
|
176
|
+
ctx: TextContext,
|
|
177
|
+
width: number,
|
|
178
|
+
watched: ReadonlyMap<string, readonly string[]>,
|
|
179
|
+
): string[] {
|
|
139
180
|
const m = messages(ctx.locale);
|
|
140
181
|
const right = m.changes(ov.changes, ov.hunks);
|
|
141
182
|
const rows = counterLines(counterItems(ov, m, ctx.paint), width - INDENT.length);
|
|
@@ -149,6 +190,7 @@ function statLines(ov: Overview, ctx: TextContext, width: number): string[] {
|
|
|
149
190
|
),
|
|
150
191
|
);
|
|
151
192
|
lines.push(INDENT + diffstatBar(ov, width, ctx.paint));
|
|
193
|
+
lines.push(...reviewFirstLines(watched, ctx, width));
|
|
152
194
|
return lines;
|
|
153
195
|
}
|
|
154
196
|
|
|
@@ -169,30 +211,67 @@ function identicalLines(diff: DiffResult, ctx: TextContext, width: number): stri
|
|
|
169
211
|
];
|
|
170
212
|
}
|
|
171
213
|
|
|
214
|
+
/** A line of the hunk list (or of the possible matches): glyph, title, family and summary. */
|
|
172
215
|
interface Row {
|
|
173
|
-
|
|
216
|
+
/** Colour of the glyph and the family label. */
|
|
217
|
+
readonly tone: ChangeFamily;
|
|
174
218
|
readonly glyph: string;
|
|
175
219
|
readonly title: string;
|
|
176
220
|
readonly family: string;
|
|
221
|
+
/** Watched attributes the hunk changes (`definition`), shown as `⚑ definition`. */
|
|
222
|
+
readonly flag?: string;
|
|
177
223
|
readonly subtitle: string;
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
function rowsOf(
|
|
226
|
+
function rowsOf(
|
|
227
|
+
diff: DiffResult,
|
|
228
|
+
hunks: readonly Hunk[],
|
|
229
|
+
ctx: TextContext,
|
|
230
|
+
watched: ReadonlyMap<string, readonly string[]>,
|
|
231
|
+
): Row[] {
|
|
181
232
|
const m = messages(ctx.locale);
|
|
182
233
|
return hunks.map((hunk) => {
|
|
183
234
|
const { title, subtitle } = formatHunk(hunk, diff, ctx.locale);
|
|
235
|
+
const keys = watched.get(hunk.id);
|
|
184
236
|
return {
|
|
185
|
-
hunk,
|
|
237
|
+
tone: hunkFamily(hunk),
|
|
186
238
|
glyph: hunkGlyph(hunk),
|
|
187
239
|
title: sanitize(title),
|
|
188
240
|
family: m.family[hunkFamily(hunk)],
|
|
241
|
+
...(keys ? { flag: sanitize(m.watch.marker(watchLabels(keys, m))) } : {}),
|
|
189
242
|
subtitle: sanitize(subtitle),
|
|
190
243
|
};
|
|
191
244
|
});
|
|
192
245
|
}
|
|
193
246
|
|
|
194
|
-
|
|
195
|
-
|
|
247
|
+
/** What follows the family label: ` · ⚑ definition · <subtitle>`, as plain text. */
|
|
248
|
+
const restText = (row: Row): string =>
|
|
249
|
+
(row.flag ? ` · ${FLAG} ${row.flag}` : '') + (row.subtitle ? ` · ${row.subtitle}` : '');
|
|
250
|
+
|
|
251
|
+
/** Glyph of a possible match: “maybe the same”. */
|
|
252
|
+
const POSSIBLE_GLYPH = '≈';
|
|
253
|
+
/** Possible matches listed before "… and N more" (all with `--limit 0`). */
|
|
254
|
+
export const POSSIBLE_LIMIT = 10;
|
|
255
|
+
|
|
256
|
+
/** Possible match rows: both names, then the score, why it was not applied and the evidence. */
|
|
257
|
+
function possibleRows(diff: DiffResult, list: readonly PossibleMatch[], ctx: TextContext): Row[] {
|
|
258
|
+
const m = messages(ctx.locale);
|
|
259
|
+
const threshold = percent(diff.options.match.threshold);
|
|
260
|
+
return list.map((p) => ({
|
|
261
|
+
tone: 'identity',
|
|
262
|
+
glyph: POSSIBLE_GLYPH,
|
|
263
|
+
title: sanitize(m.possible.title(p.before, p.after)),
|
|
264
|
+
family: percent(p.score),
|
|
265
|
+
subtitle: sanitize(
|
|
266
|
+
[
|
|
267
|
+
p.reason === 'ambiguous' ? m.possible.ambiguous : m.possible.belowThreshold(threshold),
|
|
268
|
+
m.possible.evidence(percent(p.label), percent(p.neighbors)),
|
|
269
|
+
].join(' · '),
|
|
270
|
+
),
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const rightText = (row: Row): string => row.family + restText(row);
|
|
196
275
|
|
|
197
276
|
/** Natural width of the hunk list (before any truncation). */
|
|
198
277
|
function listWidth(rows: readonly Row[]): number {
|
|
@@ -229,16 +308,24 @@ function listColumns(rows: readonly Row[], width: number): { title: number; righ
|
|
|
229
308
|
|
|
230
309
|
function renderRow(row: Row, cols: { title: number; right: number }, ctx: TextContext): string {
|
|
231
310
|
const { paint } = ctx;
|
|
232
|
-
const tone =
|
|
311
|
+
const tone = row.tone;
|
|
233
312
|
const glyph = paint.tone(tone, row.glyph);
|
|
234
313
|
if (cols.right === 0) return `${INDENT}${glyph} ${truncate(row.title, cols.title)}`;
|
|
235
314
|
const title = padEnd(truncate(row.title, cols.title), cols.title);
|
|
236
315
|
const familyWidth = stringWidth(row.family);
|
|
237
316
|
const restRoom = cols.right - familyWidth;
|
|
238
|
-
const
|
|
317
|
+
const full = restText(row);
|
|
318
|
+
const rest = full && restRoom >= 6 ? truncate(full, restRoom) : '';
|
|
319
|
+
// The flag comes first, so it survives truncation, and keeps the accent colour; the summary
|
|
320
|
+
// after it is dimmed.
|
|
321
|
+
const flag = row.flag ? ` · ${FLAG} ${row.flag}` : '';
|
|
322
|
+
const flagShown = flag ? rest.slice(0, Math.min(rest.length, flag.length)) : '';
|
|
323
|
+
const painted = flagShown
|
|
324
|
+
? paint.dim(flagShown.slice(0, 3)) + paint.tone('accent', flagShown.slice(3))
|
|
325
|
+
: '';
|
|
239
326
|
return (
|
|
240
327
|
`${INDENT}${glyph} ${title}${' '.repeat(COLUMN_GAP)}` +
|
|
241
|
-
`${paint.tone(tone, row.family)}${paint.dim(rest)}`
|
|
328
|
+
`${paint.tone(tone, row.family)}${painted}${paint.dim(rest.slice(flagShown.length))}`
|
|
242
329
|
);
|
|
243
330
|
}
|
|
244
331
|
|
|
@@ -268,6 +355,13 @@ function headerNatural(diff: DiffResult, title: string, ctx: TextContext, ov: Ov
|
|
|
268
355
|
* The default terminal output: header (title, node and edge totals), per-phase counters, a
|
|
269
356
|
* proportional diffstat bar, then the hunks grouped by phase with glyph, title and a family /
|
|
270
357
|
* summary column. At most `ctx.limit` hunks are listed (0 = all), then "… and N more".
|
|
358
|
+
* Hunks that change a watched attribute (`ctx.watch`, definitions by default) are marked
|
|
359
|
+
* `⚑ definition` before their summary, and a line under the bar counts them.
|
|
360
|
+
*
|
|
361
|
+
* Last come the possible matches: pairings kgdiff considered but did not apply, between a node
|
|
362
|
+
* listed as removed and one listed as added (`possibleMatches`), with their score, why they were
|
|
363
|
+
* not applied, the evidence, and how to pair one. At most `POSSIBLE_LIMIT` (all with
|
|
364
|
+
* `--limit 0`).
|
|
271
365
|
*/
|
|
272
366
|
export function renderSummary(diff: DiffResult, title: string, ctx: TextContext): string {
|
|
273
367
|
const ov = overview(diff);
|
|
@@ -275,33 +369,61 @@ export function renderSummary(diff: DiffResult, title: string, ctx: TextContext)
|
|
|
275
369
|
const width = layoutWidth(headerNatural(diff, title, ctx, ov), ctx.width);
|
|
276
370
|
return `${[...headerLines(diff, title, ctx, width), '', ...identicalLines(diff, ctx, width)].join('\n')}\n`;
|
|
277
371
|
}
|
|
372
|
+
const watched = watchedOf(diff, ctx);
|
|
278
373
|
const m = messages(ctx.locale);
|
|
374
|
+
const { paint } = ctx;
|
|
279
375
|
const groups = groupByPhase(diff.hunks);
|
|
280
376
|
let shown = 0;
|
|
281
377
|
const budget = ctx.limit > 0 ? ctx.limit : Number.POSITIVE_INFINITY;
|
|
282
378
|
const listed = groups.map((g) => {
|
|
283
379
|
const take = g.hunks.slice(0, Math.max(0, budget - shown));
|
|
284
380
|
shown += take.length;
|
|
285
|
-
return { phase: g.phase, total: g.hunks.length, rows: rowsOf(diff, take, ctx) };
|
|
381
|
+
return { phase: g.phase, total: g.hunks.length, rows: rowsOf(diff, take, ctx, watched) };
|
|
286
382
|
});
|
|
383
|
+
const possible = possibleMatches(diff);
|
|
384
|
+
const possibleShown = possible.slice(
|
|
385
|
+
0,
|
|
386
|
+
ctx.limit > 0 ? Math.min(ctx.limit, POSSIBLE_LIMIT) : possible.length,
|
|
387
|
+
);
|
|
388
|
+
const possibleList = possibleRows(diff, possibleShown, ctx);
|
|
287
389
|
const allRows = listed.flatMap((g) => g.rows);
|
|
288
390
|
const width = layoutWidth(
|
|
289
|
-
Math.max(headerNatural(diff, title, ctx, ov), listWidth(allRows)),
|
|
391
|
+
Math.max(headerNatural(diff, title, ctx, ov), listWidth(allRows), listWidth(possibleList)),
|
|
290
392
|
ctx.width,
|
|
291
393
|
);
|
|
292
394
|
const cols = listColumns(allRows, width);
|
|
293
|
-
|
|
395
|
+
// Columns of their own: hunk titles are often long, and the reason and evidence matter more
|
|
396
|
+
// here than lining up with the hunks above (the section has its own heading).
|
|
397
|
+
const possibleCols = listColumns(possibleList, width);
|
|
398
|
+
const lines = [
|
|
399
|
+
...headerLines(diff, title, ctx, width),
|
|
400
|
+
'',
|
|
401
|
+
...statLines(ov, ctx, width, watched),
|
|
402
|
+
'',
|
|
403
|
+
];
|
|
294
404
|
for (const group of listed) {
|
|
295
405
|
if (group.rows.length === 0) continue;
|
|
296
|
-
lines.push(
|
|
297
|
-
`${INDENT}${ctx.paint.bold(m.phase[group.phase])} ${ctx.paint.dim(formatInt(group.total))}`,
|
|
298
|
-
);
|
|
406
|
+
lines.push(`${INDENT}${paint.bold(m.phase[group.phase])} ${paint.dim(formatInt(group.total))}`);
|
|
299
407
|
for (const row of group.rows) lines.push(renderRow(row, cols, ctx));
|
|
300
408
|
}
|
|
301
409
|
const hidden = ov.hunks - shown;
|
|
302
410
|
if (hidden > 0) {
|
|
303
411
|
const more = `${m.more(hidden)} · ${m.moreHint}`;
|
|
304
|
-
lines.push(INDENT +
|
|
412
|
+
lines.push(INDENT + paint.dim(truncate(more, width - INDENT.length)));
|
|
413
|
+
}
|
|
414
|
+
if (possible.length > 0) {
|
|
415
|
+
lines.push(
|
|
416
|
+
'',
|
|
417
|
+
`${INDENT}${paint.bold(m.possible.heading)} ${paint.dim(formatInt(possible.length))}`,
|
|
418
|
+
);
|
|
419
|
+
for (const row of possibleList) lines.push(renderRow(row, possibleCols, ctx));
|
|
420
|
+
const notListed = possible.length - possibleShown.length;
|
|
421
|
+
if (notListed > 0) {
|
|
422
|
+
lines.push(INDENT + paint.dim(truncate(m.possible.more(notListed), width - INDENT.length)));
|
|
423
|
+
}
|
|
424
|
+
for (const line of wrap(m.possible.hint, width - INDENT.length)) {
|
|
425
|
+
lines.push(INDENT + paint.dim(line));
|
|
426
|
+
}
|
|
305
427
|
}
|
|
306
428
|
return `${lines.join('\n')}\n`;
|
|
307
429
|
}
|
|
@@ -310,15 +432,16 @@ const MAX_TYPE_ROWS = 12;
|
|
|
310
432
|
const MAX_TYPE_NAME = 28;
|
|
311
433
|
|
|
312
434
|
/**
|
|
313
|
-
* `--stat`: header, counters and bar
|
|
314
|
-
* table, and a git-style per-type
|
|
435
|
+
* `--stat`: header, counters and bar (with the review-first line, as in the summary), a
|
|
436
|
+
* nodes / edges × added / removed / modified / unchanged table, and a git-style per-type
|
|
437
|
+
* breakdown (`+` added, `−` removed, `~` modified nodes).
|
|
315
438
|
*/
|
|
316
439
|
export function renderStat(diff: DiffResult, title: string, ctx: TextContext): string {
|
|
317
440
|
const ov = overview(diff);
|
|
318
441
|
const width = layoutWidth(headerNatural(diff, title, ctx, ov), ctx.width);
|
|
319
442
|
const lines = [...headerLines(diff, title, ctx, width), ''];
|
|
320
443
|
if (ov.hunks === 0) return `${[...lines, ...identicalLines(diff, ctx, width)].join('\n')}\n`;
|
|
321
|
-
lines.push(...statLines(ov, ctx, width), '');
|
|
444
|
+
lines.push(...statLines(ov, ctx, width, watchedOf(diff, ctx)), '');
|
|
322
445
|
lines.push(...countsTable(diff, ctx, width), '');
|
|
323
446
|
lines.push(...typeLines(diff, ctx, width));
|
|
324
447
|
return `${lines.join('\n')}\n`;
|