dsh-vscode-mode 0.4.6 → 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.
Files changed (44) hide show
  1. package/README.md +32 -2
  2. package/lib/client.js +8854 -4023
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1842 -65
  5. package/lib/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/client/index.ts +3 -0
  8. package/src/client/monaco/lsp/providers.ts +28 -6
  9. package/src/client/navHighlight.ts +96 -0
  10. package/src/client/paths.ts +2 -0
  11. package/src/client/rpc.ts +32 -12
  12. package/src/client/sidebar/SidebarView.ts +6 -2
  13. package/src/client/sidebar/menuItems.ts +93 -2
  14. package/src/client/sidebar/panels/FileExplorer.ts +18 -1
  15. package/src/client/sidebar/panels/SvnPanel.ts +303 -0
  16. package/src/client/sidebar/panels/index.ts +44 -3
  17. package/src/client/sidebar/types.ts +20 -0
  18. package/src/client/styles/editor.css +177 -0
  19. package/src/client/svnActions.ts +153 -0
  20. package/src/client/svnLog.ts +356 -0
  21. package/src/client/svnStatus.ts +363 -0
  22. package/src/client/svnStore.ts +150 -0
  23. package/src/client/tabMenu.ts +51 -0
  24. package/src/client/ui/EditorView.ts +583 -24
  25. package/src/client/ui/LogDialog.ts +333 -0
  26. package/src/client/ui/McpSettings.ts +98 -0
  27. package/src/client/ui/ModalShell.ts +79 -0
  28. package/src/client/ui/SideBySideDiff.tsx +139 -0
  29. package/src/client/ui/SnippetsPicker.ts +70 -58
  30. package/src/client/ui/SvnDiffPanel.ts +50 -0
  31. package/src/client/ui/SvnLogDialog.ts +1141 -0
  32. package/src/client/ui/SvnLogStats.ts +243 -0
  33. package/src/client/ui/commandCatalog.ts +72 -0
  34. package/src/debugLog.ts +81 -12
  35. package/src/fileOpenSettings.ts +40 -3
  36. package/src/index.ts +5 -1
  37. package/src/rpc.ts +41 -4
  38. package/src/shared/rpc.ts +53 -1
  39. package/src/shared/svn.ts +377 -0
  40. package/src/shared/svnActions.ts +296 -0
  41. package/src/svn.ts +870 -0
  42. package/src/svnLog.ts +315 -0
  43. package/src/svnText.ts +175 -0
  44. package/src/svnXml.ts +110 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * dsh-vscode-mode client — SVN 动作**执行器**表(与 shared/svnActions.ts 的元数据表配对)。
3
+ *
4
+ * 为什么与元数据分开:元数据要能被 host/shared 引用(纯数据、可单测),而执行器必然带
5
+ * RPC 副作用与 UI 回调。三个入口(命令栏/文件树菜单/页签菜单)与 EditorView 分派都只
6
+ * 需要「id → 执行器」,不再各自手写一份调用逻辑。
7
+ *
8
+ * 执行器约定:`(ctx) => void`,内部自行 await RPC 并回报文案;
9
+ * 破坏性动作的 confirm 守卫由执行器承担(避免各入口各自实现,漏一处就少一道保护)。
10
+ * 作者 ddj 2026年09月16号
11
+ */
12
+ import type { SvnActionDef } from '../shared/svnActions.js'
13
+ import { svnCleanup } from './svnLog.js'
14
+ import { refreshSvnChanges, svnAdd, svnDiffBase, svnRevert, svnTortoise, svnUpdate } from './svnStatus.js'
15
+
16
+ /** 动作执行上下文(各入口把自身能力投影进来)。 */
17
+ export interface SvnActionRunCtx {
18
+ /** 会话 id。 */
19
+ sessionId?: string
20
+ /** 状态作用域(刷新缓存用)。 */
21
+ scope?: string | null
22
+ /** 目标工作区相对路径('' = 工作区根;缺省 = 无目标)。 */
23
+ path?: string
24
+ /** 反馈(状态栏/notify)。 */
25
+ notify?: (message: string) => void
26
+ /** 打开基线差异视图(diff-base 动作)。 */
27
+ openSvnDiff?: (path: string) => void
28
+ /** 打开版本差异视图(日志条目差异)。 */
29
+ openSvnDiffRev?: (path: string, revision: number) => void
30
+ /** 打开日志弹窗(log 动作)。 */
31
+ openSvnLog?: (path?: string) => void
32
+ /** 重查变更清单(写操作后)。 */
33
+ refreshChanges?: () => void
34
+ /** 就地打开文件(日志面板点击变更文件)。 */
35
+ openFile?: (path: string) => void
36
+ /** 确认对话框(缺省 window.confirm;测试可注入)。 */
37
+ confirm?: (message: string) => boolean
38
+ /** 清理动作的破坏性选项(默认仅清锁)。 */
39
+ cleanupOptions?: { removeUnversioned?: boolean; removeIgnored?: boolean }
40
+ }
41
+
42
+ /** 单条动作执行器。 */
43
+ export type SvnActionRunner = (ctx: SvnActionRunCtx) => void
44
+
45
+ /** 确认对话框(缺省走 window.confirm;无 window 环境视为拒绝,避免误执行破坏性动作)。 */
46
+ function askConfirm(ctx: SvnActionRunCtx, message: string): boolean {
47
+ if (ctx.confirm) return ctx.confirm(message)
48
+ if (typeof window === 'undefined') return false
49
+ return window.confirm(message)
50
+ }
51
+
52
+ /** 统一收尾:回报文案 + 成功后重查变更(写操作让面板/徽标跟随)。 */
53
+ function afterWrite(ctx: SvnActionRunCtx, promise: Promise<{ ok: boolean; message: string }>): void {
54
+ void promise.then((outcome) => {
55
+ ctx.notify?.(outcome.message)
56
+ if (outcome.ok) ctx.refreshChanges?.()
57
+ })
58
+ }
59
+
60
+ /**
61
+ * 动作执行器表(key = shared/svnActions.ts 的动作 id)。
62
+ *
63
+ * 注意:这里不出现任何显隐判断——可见性完全由 shared 的 `svnActionOn` 决定,
64
+ * 保证「同一动作在三处入口的可见条件」逐字一致。
65
+ */
66
+ export const SVN_ACTION_RUNNERS: Record<string, SvnActionRunner> = {
67
+ update: (ctx) => {
68
+ void svnUpdate(ctx.sessionId, ctx.path ?? '').then((outcome) => {
69
+ ctx.notify?.(outcome.message)
70
+ if (outcome.ok) ctx.refreshChanges?.()
71
+ })
72
+ },
73
+ 'refresh-changes': (ctx) => {
74
+ ctx.refreshChanges?.()
75
+ ctx.notify?.('已刷新 SVN 变更')
76
+ },
77
+ 'diff-base': (ctx) => {
78
+ if (!ctx.path) { ctx.notify?.('无活动文件'); return }
79
+ ctx.openSvnDiff?.(ctx.path)
80
+ },
81
+ add: (ctx) => {
82
+ if (!ctx.path) { ctx.notify?.('无目标路径'); return }
83
+ afterWrite(ctx, svnAdd(ctx.sessionId, [ctx.path]))
84
+ },
85
+ // 键必须与 shared/svnActions.ts 的动作 id 完全一致(不一致会静默不执行,故有单测守护)
86
+ 'revert-cli': (ctx) => {
87
+ if (!ctx.path) { ctx.notify?.('无目标路径'); return }
88
+ // 破坏性动作的确认唯一入口(各调用方不得绕过)
89
+ const ok = askConfirm(ctx, '确认还原「' + ctx.path + '」的本地改动?新增(A)文件在磁盘上会保留,仅取消登记。')
90
+ if (!ok) return
91
+ afterWrite(ctx, svnRevert(ctx.sessionId, [ctx.path]))
92
+ }, log: (ctx) => {
93
+ ctx.openSvnLog?.(ctx.path || undefined)
94
+ },
95
+ cleanup: (ctx) => {
96
+ const options = ctx.cleanupOptions ?? {}
97
+ const destructive = options.removeUnversioned === true || options.removeIgnored === true
98
+ if (destructive) {
99
+ const scopeText = options.removeUnversioned ? '未纳入版本控制的文件与目录' : '被忽略的文件'
100
+ const ok = askConfirm(ctx, '确认删除工作副本中的' + scopeText + '?该操作会从磁盘真实删除,且无法通过 SVN 恢复。')
101
+ if (!ok) return
102
+ }
103
+ afterWrite(ctx, svnCleanup(ctx.sessionId, options))
104
+ },
105
+ // Tortoise 组:统一由 svnTortoise 发射(action 由 id 后缀解析)
106
+ 'tortoise-commit': (ctx) => runTortoiseAction(ctx, 'commit'),
107
+ 'tortoise-log': (ctx) => runTortoiseAction(ctx, 'log'),
108
+ 'tortoise-diff': (ctx) => runTortoiseAction(ctx, 'diff'),
109
+ 'tortoise-blame': (ctx) => runTortoiseAction(ctx, 'blame'),
110
+ 'tortoise-revert': (ctx) => runTortoiseAction(ctx, 'revert'),
111
+ }
112
+
113
+ /** Tortoise 动作统一执行(需要目标;日志/提交允许根)。 */
114
+ function runTortoiseAction(ctx: SvnActionRunCtx, action: 'commit' | 'log' | 'diff' | 'blame' | 'revert'): void {
115
+ if (action === 'log') { ctx.openSvnLog?.(ctx.path || undefined); return }
116
+ const target = ctx.path ?? ''
117
+ if (!target && action !== 'commit') { ctx.notify?.('无活动文件'); return }
118
+ void svnTortoise(ctx.sessionId, action, target).then((outcome) => ctx.notify?.(outcome.message))
119
+ }
120
+
121
+ /**
122
+ * 执行一条动作(入口唯一调用点)。
123
+ * @author ddj 2026年09月16号
124
+ * @param action 动作定义(来自 shared 目录)
125
+ * @param ctx 执行上下文
126
+ * @returns 是否找到并执行了执行器
127
+ */
128
+ export function runSvnAction(action: SvnActionDef, ctx: SvnActionRunCtx): boolean {
129
+ const runner = SVN_ACTION_RUNNERS[action.id]
130
+ if (!runner) return false
131
+ runner(ctx)
132
+ return true
133
+ }
134
+
135
+ /** 供 UI 使用的动作元数据 + 执行器合并视图。 */
136
+ export interface SvnActionEntry {
137
+ def: SvnActionDef
138
+ run: () => void
139
+ }
140
+
141
+ /**
142
+ * 把动作定义表映射为可点击条目(统一注入执行上下文)。
143
+ * @author ddj 2026年09月16号
144
+ * @param actions 可见动作(已过滤)
145
+ * @param ctx 执行上下文
146
+ * @returns 条目数组
147
+ */
148
+ export function toActionEntries(actions: readonly SvnActionDef[], ctx: SvnActionRunCtx): SvnActionEntry[] {
149
+ return actions.map((def) => ({ def, run: () => runSvnAction(def, ctx) }))
150
+ }
151
+
152
+ // 便于 svnStatus 的动作封装在其它模块复用
153
+ export { refreshSvnChanges, svnAdd, svnDiffBase, svnRevert, svnUpdate }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * dsh-vscode-mode client — SVN 日志状态层(P3)。
3
+ *
4
+ * 数据源经 svnStore 工厂创建(scope 缓存 + 在途去重 + 事件广播),键为 `scope::target`
5
+ * 复合键——同一工作区下「根的日志」与「某文件的日志」是两份数据,不能共用一个 key。
6
+ * 另提供 update 结果 / cleanup / 版本差异的动作封装,供动作执行器与面板复用。
7
+ * 作者 ddj 2026年09月16号
8
+ */
9
+ import type { SvnActionResult, SvnDiffRevResult, SvnLogEntry } from '../shared/svn.js'
10
+ import { SVN_LOG_SHOW_ALL_CAP } from '../svnLog.js'
11
+ import { createSvnStore, svnStoreKey } from './svnStore.js'
12
+ import { rpc } from './rpc.js'
13
+
14
+ /** 日志数据源(key = `scope\0target[\0variant]`;variant 编码 Stop on copy / merged / Show Range,P1-4/P1-5/P1-6)。 */
15
+ const logStore = createSvnStore<'svn.log', SvnLogEntry[]>({
16
+ method: 'svn.log',
17
+ event: 'edrv:svn-log',
18
+ args: (key) => {
19
+ const [target, variant] = splitLogKey(key)
20
+ return { path: target, ...logVariantArgsOf(variant) }
21
+ },
22
+ select: (res) => ({
23
+ value: Array.isArray(res.entries) ? (res.entries as SvnLogEntry[]) : [],
24
+ meta: {
25
+ truncated: res.truncated === true,
26
+ loadedLimit: Number(res.limit) || (Array.isArray(res.entries) ? (res.entries as SvnLogEntry[]).length : 0),
27
+ },
28
+ }),
29
+ })
30
+
31
+ /** 日志取数选项(P1-4 Stop on copy / P1-6 Show Range / P1-5 Include merged revs)。 */
32
+ export interface SvnLogOpts {
33
+ /** 勾选 Stop on copy(遇到 copy/rename 即停止回溯)。 */
34
+ stopOnCopy?: boolean
35
+ /** Show Range 区间 [较早, 较晚](不传 = 默认 HEAD:1 窗口)。 */
36
+ range?: [number, number] | null
37
+ /** 勾选 Include merged revisions(argv `-g`;P1-5)。 */
38
+ showMerged?: boolean
39
+ }
40
+
41
+ /**
42
+ * 拆日志键:`scope\0target[\0variant]` → [target, variant](scope 由 svnStoreKey 前缀保证)。
43
+ * @author ddj 2026年09月17号
44
+ * @param key 缓存键
45
+ * @returns [目标相对路径, variant 串]
46
+ */
47
+ function splitLogKey(key: string): [string, string] {
48
+ const at = key.indexOf('\u0000')
49
+ if (at < 0) return ['', '']
50
+ const rest = key.slice(at + 1)
51
+ const sep = rest.indexOf('\u0000')
52
+ if (sep < 0) return [rest, '']
53
+ return [rest.slice(0, sep), rest.slice(sep + 1)]
54
+ }
55
+
56
+ /** 日志取数选项的类型化参数(variant → RPC 参数,三处入口共用防漂移)。 */
57
+ type LogVariantArgs = { stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }
58
+
59
+ /**
60
+ * variant → RPC 参数(`soc` = stopOnCopy;`mrg` = showMerged;`rSTART-END` = 区间;`|` 连接)。
61
+ * @author ddj 2026年09月17号 / 2026年09月18号
62
+ * @param variant 键尾变体串
63
+ * @returns svn.log 附加参数
64
+ */
65
+ function logVariantArgsOf(variant: string): LogVariantArgs {
66
+ const out: LogVariantArgs = {}
67
+ for (const part of variant.split('|')) {
68
+ if (part === 'soc') { out.stopOnCopy = true; continue }
69
+ if (part === 'mrg') { out.showMerged = true; continue }
70
+ if (!part.startsWith('r')) continue
71
+ const dash = part.indexOf('-')
72
+ if (dash < 2) continue
73
+ const startRev = Number(part.slice(1, dash))
74
+ const endRev = Number(part.slice(dash + 1))
75
+ if (Number.isFinite(startRev) && startRev > 0) out.startRev = startRev
76
+ if (Number.isFinite(endRev) && endRev > 0) out.endRev = endRev
77
+ }
78
+ return out
79
+ }
80
+
81
+ /**
82
+ * 选项 → 键 variant 串(空选项 = 空串,键退回 `scope\0target`)。
83
+ * @author ddj 2026年09月17号 / 2026年09月18号
84
+ * @param opts 日志取数选项
85
+ * @returns variant 串
86
+ */
87
+ function logVariantOf(opts: SvnLogOpts): string {
88
+ const parts: string[] = []
89
+ if (opts.stopOnCopy) parts.push('soc')
90
+ if (opts.showMerged) parts.push('mrg')
91
+ if (opts.range && opts.range[0] > 0 && opts.range[1] > 0) parts.push('r' + opts.range[0] + '-' + opts.range[1])
92
+ return parts.join('|')
93
+ }
94
+
95
+ /**
96
+ * 选项 → RPC 附加参数(ensure/refresh 两个入口共用,防两处漂移)。
97
+ * @author ddj 2026年09月18号
98
+ * @param opts 日志取数选项
99
+ * @returns RPC 附加参数(limit 由调用方另加)
100
+ */
101
+ function logOptsArgsOf(opts: SvnLogOpts): LogVariantArgs {
102
+ const extra: LogVariantArgs = {}
103
+ if (opts.stopOnCopy) extra.stopOnCopy = true
104
+ if (opts.showMerged) extra.showMerged = true
105
+ if (opts.range && opts.range[0] > 0 && opts.range[1] > 0) { extra.startRev = opts.range[0]; extra.endRev = opts.range[1] }
106
+ return extra
107
+ }
108
+
109
+ /**
110
+ * 从复合键解析目标与条数(`scope\0target` 的 `\0` 之后部分)。
111
+ * P1-8 起 limit 不再参与键,第二个返回值恒为 0,仅为兼容旧复合格式保留解析。
112
+ * @author ddj 2026年09月16号
113
+ * @param key 复合键
114
+ * @returns [目标相对路径, 条数(恒 0)]
115
+ */
116
+ export function svnStoreTargetOf(key: string): [string, number] {
117
+ const at = key.indexOf('\u0000')
118
+ if (at < 0) return ['', 0]
119
+ const rest = key.slice(at + 1)
120
+ const sep = rest.indexOf('\u0000')
121
+ if (sep < 0) return [rest, 0]
122
+ return [rest.slice(0, sep), Number(rest.slice(sep + 1)) || 0]
123
+ }
124
+
125
+ /**
126
+ * 构造日志缓存键(P1-8:scope + target;P1-4/P1-6:非空选项追加 `\0variant` 维度)。
127
+ * @author ddj 2026年09月17号
128
+ * @param scope 状态作用域
129
+ * @param target 目标相对路径
130
+ * @param variant 键尾变体串('' = 默认窗口)
131
+ * @returns 复合键
132
+ */
133
+ export function logKeyOf(scope: string, target: string, variant = ''): string {
134
+ return variant ? svnStoreKey(scope, target + '\u0000' + variant) : svnStoreKey(scope, target)
135
+ }
136
+
137
+ /**
138
+ * 选项 → 缓存键(EditorView 事件订阅键与状态层共用,防止两处拼法漂移)。
139
+ * @author ddj 2026年09月17号
140
+ * @param scope 状态作用域
141
+ * @param target 目标相对路径
142
+ * @param opts 日志取数选项
143
+ * @returns 复合键
144
+ */
145
+ export function svnLogKeyOf(scope: string, target: string, opts: SvnLogOpts = {}): string {
146
+ return logKeyOf(scope, target, logVariantOf(opts))
147
+ }
148
+
149
+ /**
150
+ * Show All 的拉取上限(P0-6 / P1-7):复用 host 侧 `SVN_LOG_SHOW_ALL_CAP`(`src/svnLog.ts`
151
+ * 为纯模块、无 node 导入链,client 可安全引用),单一事实源。
152
+ * 真全量(省略 `-l`)在超大仓库会拉上百 MB,故 host 侧仍以该值为硬上限。
153
+ * @author ddj 2026年09月17号 / 2026年09月18号
154
+ */
155
+ export const SVN_LOG_SHOW_ALL_LIMIT = SVN_LOG_SHOW_ALL_CAP
156
+
157
+ /**
158
+ * 拉取(或命中缓存的)日志。
159
+ * @author ddj 2026年09月16号 / 2026年09月17号
160
+ * @param sessionId 会话 id
161
+ * @param scope 状态作用域
162
+ * @param target 目标相对路径('' = 工作区根)
163
+ * @param limit 本次请求条数(0 = 默认)
164
+ * @param force 强制重取(P1-8「加载更多/Show All」用:绕过缓存命中,成功后原地替换同键数据)
165
+ * @param opts 取数选项(P1-4 stopOnCopy / P1-6 range,参与键维度)
166
+ */
167
+ export function ensureSvnLog(
168
+ sessionId: string | undefined,
169
+ scope: string | null | undefined,
170
+ target = '',
171
+ limit = 0,
172
+ force = false,
173
+ opts: SvnLogOpts = {},
174
+ ): void {
175
+ if (!scope) return
176
+ const extra: Record<string, unknown> = { ...logOptsArgsOf(opts) }
177
+ if (limit) extra.limit = limit
178
+ logStore.ensure(sessionId, logKeyOf(scope, target, logVariantOf(opts)), force, extra)
179
+ }
180
+
181
+ /**
182
+ * 读缓存的日志(未加载返回 null)。
183
+ * @author ddj 2026年09月16号 / 2026年09月17号
184
+ * @param scope 状态作用域
185
+ * @param target 目标相对路径
186
+ * @param _limit 仅保留兼容旧签名,P1-8 起不参与键
187
+ * @param opts 取数选项(参与键维度)
188
+ * @returns 日志条目或 null
189
+ */
190
+ export function getSvnLog(scope: string | null | undefined, target = '', _limit = 0, opts: SvnLogOpts = {}): SvnLogEntry[] | null {
191
+ if (!scope) return null
192
+ return logStore.get(logKeyOf(scope, target, logVariantOf(opts)))
193
+ }
194
+
195
+ /** 日志是否可能还有更多(host 侧已达本次请求上限;按 opts 对应键的 meta 读取)。 */
196
+ export function svnLogTruncated(scope: string | null | undefined, target = '', _limit = 0, opts: SvnLogOpts = {}): boolean {
197
+ if (!scope) return false
198
+ return logStore.meta(logKeyOf(scope, target, logVariantOf(opts))).truncated === true
199
+ }
200
+
201
+ /**
202
+ * 已加载的请求上限(P1-8:「加载更多」按它递增,避免用旧的 limit 维度算出倒退值)。
203
+ * @author ddj 2026年09月17号
204
+ * @param scope 状态作用域
205
+ * @param target 目标相对路径
206
+ * @param opts 取数选项(参与键维度)
207
+ * @returns 已请求条数(未加载 = 0)
208
+ */
209
+ export function svnLogLoadedLimit(scope: string | null | undefined, target = '', opts: SvnLogOpts = {}): number {
210
+ if (!scope) return 0
211
+ return Number(logStore.meta(logKeyOf(scope, target, logVariantOf(opts))).loadedLimit) || 0
212
+ }
213
+
214
+ /**
215
+ * 强制重查日志(面板 ⟳;按已加载上限与传入上限的较大者重取,避免刷新缩水)。
216
+ * @author ddj 2026年09月16号 / 2026年09月17号
217
+ * @param sessionId 会话 id
218
+ * @param scope 状态作用域
219
+ * @param target 目标相对路径
220
+ * @param limit 期望条数(0 = 默认页大小)
221
+ * @param opts 取数选项(参与键维度)
222
+ */
223
+ export function refreshSvnLog(
224
+ sessionId: string | undefined,
225
+ scope: string | null | undefined,
226
+ target = '',
227
+ limit = 0,
228
+ opts: SvnLogOpts = {},
229
+ ): void {
230
+ if (!scope) return
231
+ const wanted = Math.max(limit || 0, svnLogLoadedLimit(scope, target, opts))
232
+ const extra: Record<string, unknown> = { ...logOptsArgsOf(opts) }
233
+ if (wanted) extra.limit = wanted
234
+ logStore.refresh(sessionId, logKeyOf(scope, target, logVariantOf(opts)), extra)
235
+ }
236
+
237
+ /** 丢弃日志缓存。 */
238
+ export function clearSvnLog(scope?: string | null): void {
239
+ logStore.clear(scope ?? null)
240
+ }
241
+
242
+ /** 动作结果(message 可直接落状态栏/notify)。 */
243
+ export interface SvnLogOutcome {
244
+ ok: boolean
245
+ message: string
246
+ }
247
+
248
+ /**
249
+ * 读取某版本的左右两侧内容(版本差异视图)。
250
+ * 左侧为 null 属正常业务分支(该版本尚无此文件 = 新增),调用方应渲染为空而非报错。
251
+ * @author ddj 2026年09月16号
252
+ * @param sessionId 会话 id
253
+ * @param path 工作区相对路径
254
+ * @param revision 目标版本
255
+ * @returns 双侧内容或错误
256
+ */
257
+ export async function svnDiffRev(sessionId: string | undefined, path: string, revision: number): Promise<SvnDiffRevResult & { ok: boolean }> {
258
+ try {
259
+ const res = await rpc('svn.diffRev', { sessionId, path, revision })
260
+ if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
261
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
262
+ } catch (error) {
263
+ return { ok: false, left: null, right: null, error: String(error) }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * 读取某文件两个指定版本的左右两侧内容(P1-2 比较两个修订)。
269
+ * 左右均为 null 属正常业务分支(该版本尚无此文件 = 新增/已删除),调用方应渲染为空而非报错。
270
+ * @author ddj 2026年09月17号
271
+ * @param sessionId 会话 id
272
+ * @param path 工作区相对路径
273
+ * @param revA 左侧版本(选中顺序在前)
274
+ * @param revB 右侧版本(选中顺序在后)
275
+ * @returns 双侧内容或错误
276
+ */
277
+ export async function svnDiffPair(sessionId: string | undefined, path: string, revA: number, revB: number): Promise<SvnDiffRevResult & { ok: boolean }> {
278
+ try {
279
+ const res = await rpc('svn.diffPair', { sessionId, path, revA, revB })
280
+ if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
281
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
282
+ } catch (error) {
283
+ return { ok: false, left: null, right: null, error: String(error) }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 读取某版本与工作副本的左右两侧内容(P1-1 Compare with working copy)。
289
+ * 左侧为 null 属正常业务分支(该版本尚无此文件 = 新增),调用方应渲染为空而非报错。
290
+ * @author ddj 2026年09月17号
291
+ * @param sessionId 会话 id
292
+ * @param path 工作区相对路径
293
+ * @param revision 左侧版本
294
+ * @returns 双侧内容或错误
295
+ */
296
+ export async function svnDiffWorking(sessionId: string | undefined, path: string, revision: number): Promise<SvnDiffRevResult & { ok: boolean }> {
297
+ try {
298
+ const res = await rpc('svn.diffWorking', { sessionId, path, revision })
299
+ if (!res.ok) return { ok: false, left: null, right: null, error: res.error }
300
+ return { ok: true, left: res.left ?? null, right: res.right ?? null, reason: res.reason, error: res.error }
301
+ } catch (error) {
302
+ return { ok: false, left: null, right: null, error: String(error) }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * 读取文件目标的工作副本版号(P1-9 版号行加粗;目录/根/失败 = null)。
308
+ * @author ddj 2026年09月17号
309
+ * @param sessionId 会话 id
310
+ * @param path 工作区相对路径
311
+ * @returns 工作副本版号或 null
312
+ */
313
+ export async function svnWcRev(sessionId: string | undefined, path: string): Promise<number | null> {
314
+ try {
315
+ const res = await rpc('svn.wcRev', { sessionId, path })
316
+ if (!res.ok) return null
317
+ return typeof res.revision === 'number' ? res.revision : null
318
+ } catch (error) {
319
+ return null
320
+ }
321
+ }
322
+
323
+ /**
324
+ * 清理工作副本(默认只清锁;破坏性选项须调用方先 confirm)。
325
+ * @author ddj 2026年09月16号
326
+ * @param sessionId 会话 id
327
+ * @param options 破坏性选项
328
+ * @returns 执行结果与反馈文案
329
+ */
330
+ export async function svnCleanup(
331
+ sessionId: string | undefined,
332
+ options: { removeUnversioned?: boolean; removeIgnored?: boolean } = {},
333
+ ): Promise<SvnLogOutcome> {
334
+ try {
335
+ const res = await rpc('svn.cleanup', {
336
+ sessionId,
337
+ removeUnversioned: options.removeUnversioned || undefined,
338
+ removeIgnored: options.removeIgnored || undefined,
339
+ })
340
+ if (!res.ok) return { ok: false, message: res.error }
341
+ return { ok: true, message: 'SVN 清理:' + res.summary }
342
+ } catch (error) {
343
+ return { ok: false, message: 'SVN 清理失败:' + String(error) }
344
+ }
345
+ }
346
+
347
+ /**
348
+ * 动作结果统一适配(把 SvnActionResult 转成带文案的结果)。
349
+ * @author ddj 2026年09月16号
350
+ * @param verb 动作动词
351
+ * @param res 后端结果
352
+ * @returns 结果与文案
353
+ */
354
+ export function actionOutcomeOf(verb: string, res: SvnActionResult): SvnLogOutcome {
355
+ return { ok: true, message: verb + ':' + res.summary }
356
+ }