dsh-diff-approval 0.31.0 → 0.32.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/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) return {
6727
- ok: true,
6728
- value: { outcome: "no-change" }
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 fully covers change blocks). */
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 covered change blocks; commenting needs a range that has no
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 change blocks are covered, and whether this range already has a discussion.
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;
@@ -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
@@ -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 {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",