dsh-diff-approval 0.31.0 → 0.33.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 +8 -23
- package/README.zh.md +6 -21
- package/lib/client.js +469 -91
- package/lib/index.js +83 -4
- package/lib/types/client/PendingPanel.d.ts +4 -0
- package/lib/types/client/discussion.d.ts +3 -3
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/produced-diff.d.ts +19 -8
- package/lib/types/client/whole-file-diff.d.ts +42 -0
- package/lib/types/types.d.ts +6 -0
- package/lib/types/vcs.d.ts +33 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3855,6 +3855,53 @@ async function listVcsChanges(input) {
|
|
|
3855
3855
|
}
|
|
3856
3856
|
}
|
|
3857
3857
|
/**
|
|
3858
|
+
* Read one file's BASELINE content — the version every branch above reads to build a change's
|
|
3859
|
+
* `oldText`: git's last commit (`HEAD`, when the scan could not see the change), svn's `BASE`, p4's
|
|
3860
|
+
* `#have`. The commands are the same ones those branches run, so a caller that needs one file's
|
|
3861
|
+
* pre-change content outside a scan (the per-file refresh's "was this committed?" check) does not
|
|
3862
|
+
* invent a second way to ask the same question — or a second set of failure modes to reason about.
|
|
3863
|
+
*
|
|
3864
|
+
* The two answers stay distinguishable, exactly as they do in a scan: a baseline that carries no
|
|
3865
|
+
* version of the path is {@link VcsBaseline} `none` (git is the only branch that can report it,
|
|
3866
|
+
* through {@link readGitBlob}'s own absence pattern), while a read that FAILED — a refused command,
|
|
3867
|
+
* a timeout, a missing client — throws, because "could not read" is not "there is nothing there".
|
|
3868
|
+
*
|
|
3869
|
+
* @param kind - the VCS to read.
|
|
3870
|
+
* @param root - the repository/checkout/client root, as {@link detectVcsRoot} found it.
|
|
3871
|
+
* @param absolutePath - the file's absolute path (inside the root's tree).
|
|
3872
|
+
* @param shell - the deployment's shell executor.
|
|
3873
|
+
* @param signal - the caller's abort signal.
|
|
3874
|
+
* @returns the baseline content, or `none` when the baseline has no version of the path.
|
|
3875
|
+
* @throws when the baseline read failed for any other reason.
|
|
3876
|
+
*/
|
|
3877
|
+
async function readVcsBaseline(kind, root, absolutePath, shell, signal) {
|
|
3878
|
+
switch (kind) {
|
|
3879
|
+
case "git": {
|
|
3880
|
+
const blob = await readGitBlob(root, "HEAD", relative(root, resolve(absolutePath)).split(sep).join("/"), shell, signal);
|
|
3881
|
+
return blob.found ? {
|
|
3882
|
+
state: "file",
|
|
3883
|
+
text: blob.text
|
|
3884
|
+
} : { state: "none" };
|
|
3885
|
+
}
|
|
3886
|
+
case "svn": try {
|
|
3887
|
+
return {
|
|
3888
|
+
state: "file",
|
|
3889
|
+
text: await runShell(shell, `svn cat -r BASE ${shq(absolutePath)}`, root, signal)
|
|
3890
|
+
};
|
|
3891
|
+
} catch (error) {
|
|
3892
|
+
throw new Error(`could not read ${absolutePath} at BASE: ${errorMessage$1(error)}`);
|
|
3893
|
+
}
|
|
3894
|
+
case "p4": try {
|
|
3895
|
+
return {
|
|
3896
|
+
state: "file",
|
|
3897
|
+
text: await runShell(shell, `p4 print -q ${shq(absolutePath)}#have`, root, signal)
|
|
3898
|
+
};
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
throw new Error(`could not read ${absolutePath}#have: ${errorMessage$1(error)}`);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3858
3905
|
* Parse `major.minor.patch`, and NOTHING else.
|
|
3859
3906
|
*
|
|
3860
3907
|
* Deliberately strict, because the alternative is a bogus "newer": a pre-release
|
|
@@ -4227,6 +4274,29 @@ function normalizeEol(text) {
|
|
|
4227
4274
|
function contentEqual(a, b) {
|
|
4228
4275
|
return contentLinesOf(normalizeEol(a)).join("\n") === contentLinesOf(normalizeEol(b)).join("\n");
|
|
4229
4276
|
}
|
|
4277
|
+
/**
|
|
4278
|
+
* Whether one entry's change is COMMITTED: its content on disk equals its content at the VCS
|
|
4279
|
+
* baseline.
|
|
4280
|
+
*
|
|
4281
|
+
* Read off the same helpers a scan uses — {@link readVcsBaseline} runs the very commands
|
|
4282
|
+
* `listVcsChanges` runs to build a change's `oldText` (git `HEAD`, svn `BASE`, p4 `#have`) — and
|
|
4283
|
+
* compared with {@link contentEqual}, the tolerance the whole-file diff already uses. A baseline
|
|
4284
|
+
* that carries no version of the path (a file the scan cannot see because it was never
|
|
4285
|
+
* committed-and-modified) is NOT "committed": it answers false, exactly like a baseline whose
|
|
4286
|
+
* content differs. Anything that failed to read throws, for the caller to read as "not proven".
|
|
4287
|
+
*
|
|
4288
|
+
* @param root - the detected VCS root.
|
|
4289
|
+
* @param kind - the VCS to read the baseline from.
|
|
4290
|
+
* @param path - the entry's absolute file path.
|
|
4291
|
+
* @param shell - the deployment's shell executor.
|
|
4292
|
+
* @param signal - the caller's abort signal.
|
|
4293
|
+
* @returns whether the on-disk content is the baseline's.
|
|
4294
|
+
*/
|
|
4295
|
+
async function committedChange(root, kind, path, shell, signal) {
|
|
4296
|
+
const baseline = await readVcsBaseline(kind, root, path, shell, signal);
|
|
4297
|
+
if (baseline.state !== "file") return false;
|
|
4298
|
+
return contentEqual(await readFile(path, "utf8"), baseline.text);
|
|
4299
|
+
}
|
|
4230
4300
|
/** Re-encode `text`'s line endings to `eol` (its content is unchanged). */
|
|
4231
4301
|
function reencodeEol(text, eol) {
|
|
4232
4302
|
const normalized = normalizeEol(text);
|
|
@@ -6723,10 +6793,19 @@ function apply(ctx, config) {
|
|
|
6723
6793
|
return rpcError(`refresh failed: ${errorMessage(error)}`);
|
|
6724
6794
|
}
|
|
6725
6795
|
const change = changes.find((candidate) => pathIdentity(candidate.path) === pathIdentity(entry.path));
|
|
6726
|
-
if (change === void 0)
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6796
|
+
if (change === void 0) {
|
|
6797
|
+
let committed = false;
|
|
6798
|
+
try {
|
|
6799
|
+
committed = await committedChange(root.root, root.kind, entry.path, shell, signal);
|
|
6800
|
+
} catch {}
|
|
6801
|
+
return {
|
|
6802
|
+
ok: true,
|
|
6803
|
+
value: committed ? {
|
|
6804
|
+
outcome: "committed",
|
|
6805
|
+
resolved: true
|
|
6806
|
+
} : { outcome: "no-change" }
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6730
6809
|
if (change.earlierVersion === entry.earlierVersion && change.oldText === entry.oldText && change.newText === entry.newText) return {
|
|
6731
6810
|
ok: true,
|
|
6732
6811
|
value: { outcome: "unchanged" }
|
|
@@ -195,8 +195,12 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
|
|
|
195
195
|
discussions?: readonly Discussion[];
|
|
196
196
|
/** Draws one thread's card at the width this view gives it (see `DiscussionBlock`). */
|
|
197
197
|
renderDiscussion?: (discussion: Discussion, bodyWidth: number, split: boolean) => ReactNode;
|
|
198
|
+
/** The keep/revert actions for the current selection, when the panel offers them for it. */
|
|
199
|
+
selectionKeepRevert?: ReactNode;
|
|
198
200
|
/** The comment action for the current selection, when the panel offers one for it. */
|
|
199
201
|
selectionComment?: ReactNode;
|
|
202
|
+
/** Whether a selection drag is running: the frame then stays up but takes no pointer events. */
|
|
203
|
+
selectionDragging?: boolean | undefined;
|
|
200
204
|
/** The changed-line runs the overview ruler draws, in whole-file row indices. */
|
|
201
205
|
rulerRuns: readonly RulerRun[];
|
|
202
206
|
/** The go-to popup, which this view centres on its own box (see `gotoDialog`). */
|
|
@@ -363,7 +363,7 @@ export declare function frameNamesNoCurrentLine(quoteLines: readonly DiscussionQ
|
|
|
363
363
|
export declare function frameCoversRemovedRow(quoteLines: readonly DiscussionQuoteLine[] | undefined): boolean;
|
|
364
364
|
/** What the selection frame shows for the current selection. */
|
|
365
365
|
export interface SelectionFrameActions {
|
|
366
|
-
/** Whether keep/revert apply (the selection
|
|
366
|
+
/** Whether keep/revert apply (the selection touches a change, whole block or part of one). */
|
|
367
367
|
keepRevert: boolean;
|
|
368
368
|
/** Whether a new discussion may be started on this range. */
|
|
369
369
|
comment: boolean;
|
|
@@ -373,11 +373,11 @@ export interface SelectionFrameActions {
|
|
|
373
373
|
/**
|
|
374
374
|
* Decide the selection frame's contents.
|
|
375
375
|
*
|
|
376
|
-
* Keep/revert needs
|
|
376
|
+
* Keep/revert needs the selection to touch a change; commenting needs a range that has no
|
|
377
377
|
* discussion yet. A frame with neither would be an empty floating box, so it is
|
|
378
378
|
* not rendered at all.
|
|
379
379
|
*
|
|
380
|
-
* @param options - whether
|
|
380
|
+
* @param options - whether the selection touches a change, and whether this range already has a discussion.
|
|
381
381
|
* @returns the three flags the caller branches on.
|
|
382
382
|
*/
|
|
383
383
|
export declare function selectionFrame(options: {
|
|
@@ -228,8 +228,13 @@ export declare const zh: {
|
|
|
228
228
|
'panel.refreshDone': string;
|
|
229
229
|
'panel.refreshUnchanged': string;
|
|
230
230
|
'panel.refreshNone': string;
|
|
231
|
+
'panel.refreshCommitted': string;
|
|
231
232
|
'panel.refreshUntrackedHint': string;
|
|
232
233
|
'panel.refreshFailed': string;
|
|
234
|
+
'panel.batchRefreshVcsAsk': string;
|
|
235
|
+
'panel.refreshManyDone': string;
|
|
236
|
+
'panel.refreshManyMixed': string;
|
|
237
|
+
'panel.refreshManyFailed': string;
|
|
233
238
|
'action.addPath': string;
|
|
234
239
|
'panel.addTitle': string;
|
|
235
240
|
'panel.addPathPlaceholder': string;
|
|
@@ -492,8 +497,13 @@ export declare const en: {
|
|
|
492
497
|
'panel.refreshDone': string;
|
|
493
498
|
'panel.refreshUnchanged': string;
|
|
494
499
|
'panel.refreshNone': string;
|
|
500
|
+
'panel.refreshCommitted': string;
|
|
495
501
|
'panel.refreshUntrackedHint': string;
|
|
496
502
|
'panel.refreshFailed': string;
|
|
503
|
+
'panel.batchRefreshVcsAsk': string;
|
|
504
|
+
'panel.refreshManyDone': string;
|
|
505
|
+
'panel.refreshManyMixed': string;
|
|
506
|
+
'panel.refreshManyFailed': string;
|
|
497
507
|
'action.addPath': string;
|
|
498
508
|
'panel.addTitle': string;
|
|
499
509
|
'panel.addPathPlaceholder': string;
|
|
@@ -8,16 +8,27 @@
|
|
|
8
8
|
* `[data-produced-files-row] button` 0.1.5's produced-file card `title` = the path
|
|
9
9
|
* `[data-presented-files-row] button` 0.1.7's presented-file card `title` = the path
|
|
10
10
|
* `[data-changed-files] button` 0.1.7's changed-files row `aria-describedby` = the path
|
|
11
|
-
* `button[title]` a message's file LINK (0.1.7) `title` = the path
|
|
12
11
|
* `button[data-ref-chip]` a `@file` chip in a message `title` = the token
|
|
13
|
-
* `
|
|
12
|
+
* `button._fileLink_…` / `._fileMention_…` a message's file LINK `title` = the path
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* A PRESS IS ADMITTED BY THE SHELL'S OWN MARK, NEVER BY THE SHAPE OF ITS TEXT. The link row's classes are
|
|
15
|
+
* the shell's CSS-module names — measured on the live shell as `_fileMention_1ypvv_85 _fileLink_1ypvv_59`,
|
|
16
|
+
* carrying `title="src/client/produced-diff.ts"` — and that name survives the hash and line suffix the
|
|
17
|
+
* module adds per build, so the marker is the `_fileLink_` / `_fileMention_` substring rather than the
|
|
18
|
+
* whole class.
|
|
19
|
+
*
|
|
20
|
+
* This replaced two blanket rules, `button[title]` and every `a[href]`, which admitted a press by how its
|
|
21
|
+
* text was spelled and were measured wrong twice on 2026-10-09: a model picker's button carries a tooltip
|
|
22
|
+
* naming the model, and a version-like name (`…v3.2`) ends in the ".extension" the text filter accepted, so
|
|
23
|
+
* pressing 修改模型 raised this menu over the shell's own control; an in-app route link (`/settings`) holds a
|
|
24
|
+
* separator and was admitted the same way. A file press is one the shell marks as one.
|
|
25
|
+
*
|
|
26
|
+
* Everything else is left entirely alone: a link to a URL, an in-page anchor, a button whose tooltip names
|
|
27
|
+
* something that is not a file, a file LINK whose file this panel does not hold, a modified press
|
|
28
|
+
* (Ctrl/Cmd/Shift/Alt, or a middle click — gestures the harness may give its own meaning later), and any
|
|
29
|
+
* press inside this plugin's own surface, whose buttons carry paths too and belong to the panel that is
|
|
30
|
+
* already showing the file. A file-list ROW is the one press taken over whether or not the panel holds its
|
|
31
|
+
* file (see `ROW_SELECTOR`); "we do not hold it" then decides what the menu offers, not whether it appears.
|
|
21
32
|
*
|
|
22
33
|
* The name of the module (and of the event it raises) dates from when the produced-file card was the
|
|
23
34
|
* only press of this kind; `data-presented-files-row` is that same list under 0.1.7's name for it.
|
|
@@ -33,6 +33,21 @@ export interface ChangeBlock {
|
|
|
33
33
|
}
|
|
34
34
|
/** Split a row list into maximal runs of non-context rows. */
|
|
35
35
|
export declare function changeBlocksOf(diff: WholeFileDiff): ChangeBlock[];
|
|
36
|
+
/**
|
|
37
|
+
* The change region a row range sits in, widened to whole runs: from the first row of the run `start`
|
|
38
|
+
* belongs to, to the last row of the run `end` belongs to. A row range that names one side of a change —
|
|
39
|
+
* one line of a replacement, or the additions alone — grows to cover that change's removals as well, which
|
|
40
|
+
* is what a reader pointing at it means. Rows that are context stand alone (a run is a maximal non-context
|
|
41
|
+
* run), so a range that touches no change is returned as it is.
|
|
42
|
+
* @param rows - the whole-file diff rows.
|
|
43
|
+
* @param start - the range's first row.
|
|
44
|
+
* @param end - the range's last row.
|
|
45
|
+
* @returns the widened row range.
|
|
46
|
+
*/
|
|
47
|
+
export declare function changeRunSpanOf(rows: readonly WholeFileDiffRow[], start: number, end: number): {
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
};
|
|
36
51
|
/**
|
|
37
52
|
* One diff block's old/new line ranges, 1-based inclusive, for block-level
|
|
38
53
|
* keep/revert. A side with no lines (a pure addition or deletion) is empty; its
|
|
@@ -46,6 +61,33 @@ export declare function changeBlocksOf(diff: WholeFileDiff): ChangeBlock[];
|
|
|
46
61
|
* @returns the old and new line ranges.
|
|
47
62
|
*/
|
|
48
63
|
export declare function blockRangesOf(rows: readonly WholeFileDiffRow[], block: ChangeBlock): DiffApprovalBlockRange;
|
|
64
|
+
/** One side-by-side pair: the whole-file row index each side holds, when it has one. */
|
|
65
|
+
export interface DiffPairRows {
|
|
66
|
+
old?: number;
|
|
67
|
+
new?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One side-by-side pair's old/new line ranges, 1-based inclusive, for pair-level keep/revert. A pair is
|
|
71
|
+
* one old line against one new line, so each side contributes a single line; a side the pair does not
|
|
72
|
+
* have (an unpaired insertion or removal) is the empty range at that side's insertion point, read off the
|
|
73
|
+
* row before the pair — the same rule `blockRangesOf` applies to a side with no lines at all.
|
|
74
|
+
* @param rows - the whole-file diff rows.
|
|
75
|
+
* @param pair - the rows the pair holds.
|
|
76
|
+
* @returns the old and new line ranges, or undefined for a pair that holds no row.
|
|
77
|
+
*/
|
|
78
|
+
export declare function pairRangeOf(rows: readonly WholeFileDiffRow[], pair: DiffPairRows): DiffApprovalBlockRange | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The old/new ranges keep/revert act on for the pairs a side-by-side selection names, in the order they
|
|
81
|
+
* must be sent. Pairs of context rows are dropped (nothing to act on). When the changed pairs occupy one
|
|
82
|
+
* contiguous span of the row list with no other changed row inside it, ONE range covers them all: one
|
|
83
|
+
* host call, one undo step, and the same range a whole-block action would send. Otherwise each pair gets
|
|
84
|
+
* its own range, ordered from the BOTTOM of the file up, so a splice never moves the line numbers of a
|
|
85
|
+
* pair still to be applied.
|
|
86
|
+
* @param rows - the whole-file diff rows.
|
|
87
|
+
* @param pairs - the pairs the selection names, in row order.
|
|
88
|
+
* @returns one range, several, or none when the selection carries no change.
|
|
89
|
+
*/
|
|
90
|
+
export declare function pairsRangeOf(rows: readonly WholeFileDiffRow[], pairs: readonly DiffPairRows[]): DiffApprovalBlockRange[];
|
|
49
91
|
/**
|
|
50
92
|
* Compute the whole-file view between two contents. The context budget is the
|
|
51
93
|
* side lengths, so every hunk covers the file and unchanged lines survive as
|
package/lib/types/types.d.ts
CHANGED
|
@@ -301,6 +301,9 @@ export type DiffApprovalRefreshOutcome =
|
|
|
301
301
|
| 'unchanged'
|
|
302
302
|
/** The file has no local VCS change (untracked-and-excluded, or already clean). */
|
|
303
303
|
| 'no-change'
|
|
304
|
+
/** The scan saw no change AND the file's content equals its VCS baseline: the change is
|
|
305
|
+
* committed, so the entry's diff is a keep away from being out of date. */
|
|
306
|
+
| 'committed'
|
|
304
307
|
/** No pending entry existed for the id. */
|
|
305
308
|
| 'missing'
|
|
306
309
|
/** The workspace is not inside a git/svn/p4 checkout. */
|
|
@@ -308,6 +311,9 @@ export type DiffApprovalRefreshOutcome =
|
|
|
308
311
|
/** Value returned by the channel's vcs-refresh endpoint. */
|
|
309
312
|
export interface DiffApprovalRefreshValue {
|
|
310
313
|
outcome: DiffApprovalRefreshOutcome;
|
|
314
|
+
/** Set when the refresh already settled the entry (the `committed` outcome keeps it, with no
|
|
315
|
+
* diff left); absent for every outcome that leaves the decision to the panel. */
|
|
316
|
+
resolved?: boolean | undefined;
|
|
311
317
|
}
|
|
312
318
|
/** Value returned by the channel's keep and revert endpoints. */
|
|
313
319
|
export interface DiffApprovalActionValue {
|
package/lib/types/vcs.d.ts
CHANGED
|
@@ -105,6 +105,18 @@ export interface VcsImportInput {
|
|
|
105
105
|
readText: VcsFileReader;
|
|
106
106
|
signal?: AbortSignal | undefined;
|
|
107
107
|
}
|
|
108
|
+
/** What a revision has to say about one path: its content, or that the path has no version at all.
|
|
109
|
+
* A FAILED read is neither: it throws (see {@link readVcsBaseline}). */
|
|
110
|
+
export type VcsBaseline =
|
|
111
|
+
/** The baseline carries this path; `text` is its content there. */
|
|
112
|
+
{
|
|
113
|
+
state: 'file';
|
|
114
|
+
text: string;
|
|
115
|
+
}
|
|
116
|
+
/** The baseline has no version of this path (a creation — there is nothing to restore). */
|
|
117
|
+
| {
|
|
118
|
+
state: 'none';
|
|
119
|
+
};
|
|
108
120
|
/**
|
|
109
121
|
* Find the VCS enclosing `start` by walking up the directory tree: the first
|
|
110
122
|
* directory (deepest) holding a marker wins, with git > svn > p4 when several
|
|
@@ -121,4 +133,25 @@ export declare function detectVcsRoot(start: string): VcsRoot | undefined;
|
|
|
121
133
|
* @returns the changes; an absent/unusable VCS surfaces as a thrown error.
|
|
122
134
|
*/
|
|
123
135
|
export declare function listVcsChanges(input: VcsImportInput): Promise<VcsChange[]>;
|
|
136
|
+
/**
|
|
137
|
+
* Read one file's BASELINE content — the version every branch above reads to build a change's
|
|
138
|
+
* `oldText`: git's last commit (`HEAD`, when the scan could not see the change), svn's `BASE`, p4's
|
|
139
|
+
* `#have`. The commands are the same ones those branches run, so a caller that needs one file's
|
|
140
|
+
* pre-change content outside a scan (the per-file refresh's "was this committed?" check) does not
|
|
141
|
+
* invent a second way to ask the same question — or a second set of failure modes to reason about.
|
|
142
|
+
*
|
|
143
|
+
* The two answers stay distinguishable, exactly as they do in a scan: a baseline that carries no
|
|
144
|
+
* version of the path is {@link VcsBaseline} `none` (git is the only branch that can report it,
|
|
145
|
+
* through {@link readGitBlob}'s own absence pattern), while a read that FAILED — a refused command,
|
|
146
|
+
* a timeout, a missing client — throws, because "could not read" is not "there is nothing there".
|
|
147
|
+
*
|
|
148
|
+
* @param kind - the VCS to read.
|
|
149
|
+
* @param root - the repository/checkout/client root, as {@link detectVcsRoot} found it.
|
|
150
|
+
* @param absolutePath - the file's absolute path (inside the root's tree).
|
|
151
|
+
* @param shell - the deployment's shell executor.
|
|
152
|
+
* @param signal - the caller's abort signal.
|
|
153
|
+
* @returns the baseline content, or `none` when the baseline has no version of the path.
|
|
154
|
+
* @throws when the baseline read failed for any other reason.
|
|
155
|
+
*/
|
|
156
|
+
export declare function readVcsBaseline(kind: VcsKind, root: string, absolutePath: string, shell: ShellExecutorLike, signal: AbortSignal | undefined): Promise<VcsBaseline>;
|
|
124
157
|
export {};
|
package/package.json
CHANGED