@kgdiff/cli 0.3.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 +7 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +19 -2
- package/dist/args.js.map +1 -1
- package/dist/diff-command.d.ts.map +1 -1
- package/dist/diff-command.js +8 -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 +11 -1
- package/dist/help.js.map +1 -1
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +1 -0
- package/dist/html.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 +3 -2
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +5 -4
- 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 +31 -4
- package/src/diff-command.ts +8 -1
- package/src/errors.ts +30 -4
- package/src/help.ts +11 -1
- package/src/html.ts +1 -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 +7 -5
- package/src/summary.ts +145 -22
package/src/model.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Presentation model shared by every text output: hunk families, glyphs, name-status letters,
|
|
3
|
-
* phase grouping
|
|
3
|
+
* phase grouping, the per-phase counts behind the counters and the diffstat bar, the watched
|
|
4
|
+
* attributes flagged for review first, and the possible matches listed after the hunks.
|
|
4
5
|
*/
|
|
5
|
-
import type { DiffResult, Hunk, HunkKind, ReplayPhase } from '@kgdiff/core';
|
|
6
|
+
import type { DiffResult, Hunk, HunkKind, MatchSuggestion, ReplayPhase } from '@kgdiff/core';
|
|
6
7
|
|
|
7
8
|
/** The five meaning families of docs/visual-language.md (mirrors `@kgdiff/scene`). */
|
|
8
9
|
export type ChangeFamily = 'added' | 'removed' | 'content' | 'structure' | 'identity';
|
|
@@ -176,6 +177,118 @@ export function apportion(weights: readonly number[], total: number): number[] {
|
|
|
176
177
|
return cells;
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Attributes watched by default: definitions, under the keys the formats give them (OBO `def`,
|
|
182
|
+
* kgdiff JSON `definition`, `IAO:0000115` for OBO ontologies in RDF, `skos:definition`). A
|
|
183
|
+
* reworded definition can change what existing data means while the diff looks small.
|
|
184
|
+
*/
|
|
185
|
+
export const DEFAULT_WATCH_ATTRS: readonly string[] = [
|
|
186
|
+
'def',
|
|
187
|
+
'definition',
|
|
188
|
+
'IAO:0000115',
|
|
189
|
+
'skos:definition',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const DEFINITION_KEYS: ReadonlySet<string> = new Set(DEFAULT_WATCH_ATTRS);
|
|
193
|
+
|
|
194
|
+
/** Whether `key` is one of the default definition keys, shown as “definition”. */
|
|
195
|
+
export function isDefinitionKey(key: string): boolean {
|
|
196
|
+
return DEFINITION_KEYS.has(key);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The watched attributes each hunk changes, by hunk id, in `watch` order; hunks that change
|
|
201
|
+
* none are absent. Only attribute changes of matched nodes count: an added or removed node's
|
|
202
|
+
* definition is part of that addition or removal, not a change to a definition. Ignored
|
|
203
|
+
* attributes (`ignoreAttrs`) never show up here, since the diff does not report them. O(changes).
|
|
204
|
+
*/
|
|
205
|
+
export function watchedChanges(
|
|
206
|
+
diff: DiffResult,
|
|
207
|
+
watch: readonly string[],
|
|
208
|
+
): ReadonlyMap<string, readonly string[]> {
|
|
209
|
+
const watched = new Set(watch);
|
|
210
|
+
const byHunk = new Map<string, readonly string[]>();
|
|
211
|
+
if (watched.size === 0) return byHunk;
|
|
212
|
+
for (const hunk of diff.hunks) {
|
|
213
|
+
const keys = new Set<string>();
|
|
214
|
+
for (const id of hunk.nodes) {
|
|
215
|
+
for (const facet of diff.index.node(id)?.facets ?? []) {
|
|
216
|
+
if (facet.kind !== 'attrs') continue;
|
|
217
|
+
for (const change of facet.changes) if (watched.has(change.key)) keys.add(change.key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (keys.size > 0)
|
|
221
|
+
byHunk.set(
|
|
222
|
+
hunk.id,
|
|
223
|
+
[...watched].filter((k) => keys.has(k)),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return byHunk;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One side of a possible match: the node's id in its version and the name shown for it. */
|
|
230
|
+
export interface PossibleMatchSide {
|
|
231
|
+
readonly id: string;
|
|
232
|
+
readonly name: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A pairing kgdiff considered but did not apply (a `MatchSuggestion`), ready to list. */
|
|
236
|
+
export interface PossibleMatch {
|
|
237
|
+
readonly before: PossibleMatchSide;
|
|
238
|
+
readonly after: PossibleMatchSide;
|
|
239
|
+
readonly score: number;
|
|
240
|
+
readonly reason: MatchSuggestion['reason'];
|
|
241
|
+
/** Label and neighbourhood evidence, 0..1. */
|
|
242
|
+
readonly label: number;
|
|
243
|
+
readonly neighbors: number;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Longest name shown for either side (as hunk titles shorten names, @kgdiff/core i18n). */
|
|
247
|
+
const MAX_POSSIBLE_NAME = 40;
|
|
248
|
+
|
|
249
|
+
function shortName(label: string): string {
|
|
250
|
+
const chars = Array.from(label);
|
|
251
|
+
return chars.length > MAX_POSSIBLE_NAME
|
|
252
|
+
? `${chars.slice(0, MAX_POSSIBLE_NAME - 1).join('')}…`
|
|
253
|
+
: label;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const byCodePoint = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The suggestions a reviewer of the text outputs needs to see: pairings kgdiff did not apply
|
|
260
|
+
* where the before node is reported removed or the after node added, so that "removed X, added
|
|
261
|
+
* Y" can be read as "maybe X became Y". The viewer's detail panel lists the same ones. Names
|
|
262
|
+
* are the label each node had in its own version. Highest score first, then by ids.
|
|
263
|
+
*/
|
|
264
|
+
export function possibleMatches(diff: DiffResult): PossibleMatch[] {
|
|
265
|
+
const list: PossibleMatch[] = [];
|
|
266
|
+
for (const s of diff.suggestions) {
|
|
267
|
+
const before = diff.index.nodeByBefore(s.before);
|
|
268
|
+
const after = diff.index.nodeByAfter(s.after);
|
|
269
|
+
if (before?.status !== 'removed' && after?.status !== 'added') continue;
|
|
270
|
+
list.push({
|
|
271
|
+
before: { id: s.before, name: shortName(before?.before?.label ?? before?.label ?? s.before) },
|
|
272
|
+
after: { id: s.after, name: shortName(after?.after?.label ?? after?.label ?? s.after) },
|
|
273
|
+
score: s.score,
|
|
274
|
+
reason: s.reason,
|
|
275
|
+
label: s.evidence.label,
|
|
276
|
+
neighbors: s.evidence.neighbors,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return list.sort(
|
|
280
|
+
(x, y) =>
|
|
281
|
+
y.score - x.score ||
|
|
282
|
+
byCodePoint(x.before.id, y.before.id) ||
|
|
283
|
+
byCodePoint(x.after.id, y.after.id),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** A score or evidence value as a whole percentage: `0.796` → `80%`. */
|
|
288
|
+
export function percent(value: number): string {
|
|
289
|
+
return `${Math.round(value * 100)}%`;
|
|
290
|
+
}
|
|
291
|
+
|
|
179
292
|
export function assertNever(value: never): never {
|
|
180
293
|
throw new Error(`Unexpected value: ${String(value)}`);
|
|
181
294
|
}
|
package/src/node-io.ts
CHANGED
|
@@ -138,6 +138,7 @@ export function nodeIO(): CliIO {
|
|
|
138
138
|
stderrIsTTY: process.stderr.isTTY === true,
|
|
139
139
|
stdinIsTTY: process.stdin.isTTY === true,
|
|
140
140
|
columns: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
141
|
+
runtime: `${process.platform} ${process.arch}, Node.js ${process.version}`,
|
|
141
142
|
now: () => new Date(),
|
|
142
143
|
makeTempDir: () => mkdtemp(join(tmpdir(), 'kgdiff-')),
|
|
143
144
|
openFile,
|
package/src/report.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build the `ReportPayload` embedded in HTML reports: diff (`
|
|
2
|
+
* Build the `ReportPayload` embedded in HTML reports: packed diff (`packDiffJSON`) + layout
|
|
3
3
|
* (`layoutToJSON`) + meta + initial view preferences. `@kgdiff/layout` and `node:zlib` are
|
|
4
4
|
* loaded on demand, so text outputs never pay for them.
|
|
5
5
|
*/
|
|
6
|
-
import { type DiffResult, diffToJSON, type Locale } from '@kgdiff/core';
|
|
7
|
-
import type { ReportPayload, ThemePreference, ViewerMode } from '@kgdiff/viewer';
|
|
6
|
+
import { type DiffResult, diffToJSON, type Locale, packDiffJSON } from '@kgdiff/core';
|
|
7
|
+
import type { ReportPayload, ThemePreference, ViewerMode, ViewerScope } from '@kgdiff/viewer';
|
|
8
8
|
import { renderReportHtml } from './html.js';
|
|
9
9
|
|
|
10
10
|
export interface ReportInfo {
|
|
@@ -17,6 +17,7 @@ export interface ReportInfo {
|
|
|
17
17
|
readonly after: { readonly name: string; readonly format?: string };
|
|
18
18
|
readonly view: {
|
|
19
19
|
readonly mode?: ViewerMode;
|
|
20
|
+
readonly scope?: ViewerScope;
|
|
20
21
|
readonly theme?: ThemePreference;
|
|
21
22
|
readonly locale: Locale;
|
|
22
23
|
};
|
|
@@ -29,11 +30,11 @@ export async function buildReportPayload(
|
|
|
29
30
|
): Promise<ReportPayload> {
|
|
30
31
|
const { layoutDiff, layoutToJSON } = await import('@kgdiff/layout');
|
|
31
32
|
const layout = layoutToJSON(layoutDiff(diff));
|
|
32
|
-
const { mode, theme, locale } = info.view;
|
|
33
|
+
const { mode, scope, theme, locale } = info.view;
|
|
33
34
|
return {
|
|
34
35
|
format: 'kgdiff.report',
|
|
35
36
|
version: 1,
|
|
36
|
-
diff: diffToJSON(diff),
|
|
37
|
+
diff: packDiffJSON(diffToJSON(diff)),
|
|
37
38
|
layout,
|
|
38
39
|
meta: {
|
|
39
40
|
title: info.title,
|
|
@@ -44,6 +45,7 @@ export async function buildReportPayload(
|
|
|
44
45
|
},
|
|
45
46
|
view: {
|
|
46
47
|
...(mode !== undefined ? { mode } : {}),
|
|
48
|
+
...(scope !== undefined ? { scope } : {}),
|
|
47
49
|
...(theme !== undefined ? { theme } : {}),
|
|
48
50
|
locale,
|
|
49
51
|
},
|
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`;
|