dsh-vscode-mode 0.4.6 → 0.5.1
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 +38 -2
- package/lib/client.js +9316 -4038
- package/lib/client.js.map +1 -1
- package/lib/index.js +1852 -68
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/client/ai/inlineProvider.ts +42 -1
- package/src/client/index.ts +50 -45
- package/src/client/monaco/lsp/index.ts +33 -3
- package/src/client/monaco/lsp/lspClient.ts +3 -1
- package/src/client/monaco/lsp/providers.ts +60 -6
- package/src/client/navHighlight.ts +96 -0
- package/src/client/officialSidebar.ts +67 -3
- package/src/client/paths.ts +2 -0
- package/src/client/rpc.ts +32 -12
- package/src/client/sessionScope.ts +183 -0
- package/src/client/sidebar/SidebarView.ts +6 -2
- package/src/client/sidebar/menuItems.ts +93 -2
- package/src/client/sidebar/panels/FileExplorer.ts +18 -1
- package/src/client/sidebar/panels/SvnPanel.ts +303 -0
- package/src/client/sidebar/panels/index.ts +44 -3
- package/src/client/sidebar/types.ts +20 -0
- package/src/client/snippets/provider.ts +26 -4
- package/src/client/styles/editor.css +177 -0
- package/src/client/svnActions.ts +153 -0
- package/src/client/svnLog.ts +356 -0
- package/src/client/svnStatus.ts +363 -0
- package/src/client/svnStore.ts +150 -0
- package/src/client/tabActions.ts +30 -5
- package/src/client/tabMenu.ts +51 -0
- package/src/client/ui/AiSettings.ts +8 -0
- package/src/client/ui/EditorView.ts +632 -35
- package/src/client/ui/LogDialog.ts +333 -0
- package/src/client/ui/McpSettings.ts +98 -0
- package/src/client/ui/ModalShell.ts +79 -0
- package/src/client/ui/SideBySideDiff.tsx +139 -0
- package/src/client/ui/SnippetsPicker.ts +70 -58
- package/src/client/ui/SvnDiffPanel.ts +50 -0
- package/src/client/ui/SvnLogDialog.ts +1141 -0
- package/src/client/ui/SvnLogStats.ts +243 -0
- package/src/client/ui/commandCatalog.ts +72 -0
- package/src/compat.ts +1 -1
- package/src/debugLog.ts +81 -12
- package/src/dshVersion.ts +8 -1
- package/src/fileOpenSettings.ts +40 -3
- package/src/index.ts +5 -1
- package/src/rpc.ts +41 -4
- package/src/shared/ai.ts +25 -0
- package/src/shared/rpc.ts +53 -1
- package/src/shared/svn.ts +377 -0
- package/src/shared/svnActions.ts +296 -0
- package/src/svn.ts +870 -0
- package/src/svnLog.ts +315 -0
- package/src/svnText.ts +175 -0
- package/src/svnXml.ts +110 -0
package/src/rpc.ts
CHANGED
|
@@ -44,8 +44,8 @@ import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
|
44
44
|
import { normalizeRel } from './tree.js'
|
|
45
45
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
46
46
|
import { revealInExplorer } from './reveal.js'
|
|
47
|
-
import { dshHome } from './paths.js'
|
|
48
|
-
import { debugRecord } from './debugLog.js'
|
|
47
|
+
import { dshHome, debugLogFile, pluginLogRoot } from './paths.js'
|
|
48
|
+
import { clearDebugLog, debugRecord, isDebugLogName, listDebugLogs, readDebugLog } from './debugLog.js'
|
|
49
49
|
import { markActiveSessions, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf } from './perf.js'
|
|
50
50
|
import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
|
|
51
51
|
import type { FileVersions } from './fileVersions.js'
|
|
@@ -255,12 +255,15 @@ export function buildHandlers(
|
|
|
255
255
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
256
256
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
257
257
|
fileVersions?: FileVersions,
|
|
258
|
+
svnHandlers?: Partial<RpcHandlerMap>,
|
|
258
259
|
): RpcHandlerMap {
|
|
259
260
|
return {
|
|
260
261
|
// edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
|
|
261
262
|
...((lspHandlers ?? {}) as RpcHandlerMap),
|
|
262
263
|
// edrv.ai.* 由 createAiRpc 提供(AI 内联补全/模型目录/配置读写),这里并入。
|
|
263
264
|
...((aiHandlers ?? {}) as RpcHandlerMap),
|
|
265
|
+
// svn.* 由 createSvnRpc 提供(SVN 检测/更新/Tortoise 发射),这里并入。
|
|
266
|
+
...((svnHandlers ?? {}) as RpcHandlerMap),
|
|
264
267
|
'edrv.list': async (args) => {
|
|
265
268
|
const sc = await requireSession(ctx, args.sessionId)
|
|
266
269
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
@@ -505,9 +508,42 @@ export function buildHandlers(
|
|
|
505
508
|
// (console 不一定落盘,文件可靠)。只出现在调试开关开启时(client dbg 默认关)。
|
|
506
509
|
const sc = await requireSession(ctx, args.sessionId)
|
|
507
510
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
508
|
-
debugRecord(ctx, sc.cwd, String(args.text ?? ''))
|
|
511
|
+
debugRecord(ctx, sc.cwd, String(args.text ?? ''), args.level)
|
|
509
512
|
return { ok: true }
|
|
510
513
|
},
|
|
514
|
+
'edrv.dlog.list': async (args) => {
|
|
515
|
+
// 诊断日志清单:日志根下全部 debug.*.log + 当前会话对应文件名(可能尚不存在)
|
|
516
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
517
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
518
|
+
const files = await listDebugLogs()
|
|
519
|
+
const current = basename(debugLogFile(sc.cwd))
|
|
520
|
+
return { ok: true, root: pluginLogRoot(), files, current }
|
|
521
|
+
},
|
|
522
|
+
'edrv.dlog.read': async (args) => {
|
|
523
|
+
// 尾部读取:file 缺省读当前会话那份;maxBytes 夹取 [1KB, 512KB]
|
|
524
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
525
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
526
|
+
const name = isDebugLogName(args.file) ? args.file : basename(debugLogFile(sc.cwd))
|
|
527
|
+
const maxBytes = Math.min(Math.max(args.maxBytes ?? 256 * 1024, 1024), 512 * 1024)
|
|
528
|
+
const read = await readDebugLog(name, maxBytes)
|
|
529
|
+
if (!read) return { ok: false, error: '日志不存在:' + name + '(开启诊断日志并触发一次操作后生成)' }
|
|
530
|
+
return { ok: true, ...read }
|
|
531
|
+
},
|
|
532
|
+
'edrv.dlog.clear': async (args) => {
|
|
533
|
+
// 清空:file 缺省清当前会话那份;白名单外直接拒绝
|
|
534
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
535
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
536
|
+
const name = isDebugLogName(args.file) ? args.file : basename(debugLogFile(sc.cwd))
|
|
537
|
+
const cleared = await clearDebugLog(name)
|
|
538
|
+
if (!cleared) return { ok: false, error: '清空失败:日志不存在或名字非法' }
|
|
539
|
+
return { ok: true, name }
|
|
540
|
+
},
|
|
541
|
+
'edrv.dlog.reveal': async () => {
|
|
542
|
+
// 打开日志根目录(OS 文件管理器;目录形态 → 直接打开)
|
|
543
|
+
const revealed = await revealInExplorer(ctx, pluginLogRoot(), true)
|
|
544
|
+
if (!revealed.ok) return { ok: false, error: revealed.error }
|
|
545
|
+
return { ok: true, opened: true }
|
|
546
|
+
},
|
|
511
547
|
'edrv.searchFiles': async (args) => {
|
|
512
548
|
const sc = await requireSession(ctx, args.sessionId)
|
|
513
549
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
@@ -914,8 +950,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
914
950
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
915
951
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
916
952
|
fileVersions?: FileVersions,
|
|
953
|
+
svnHandlers?: Partial<RpcHandlerMap>,
|
|
917
954
|
): Promise<RpcResult<M>> {
|
|
918
|
-
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions)
|
|
955
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers)
|
|
919
956
|
const handler = handlers[method]
|
|
920
957
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
921
958
|
return handler(args)
|
package/src/shared/ai.ts
CHANGED
|
@@ -77,6 +77,31 @@ export interface AiConfigPatch {
|
|
|
77
77
|
effort?: string
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 已从官方默认模型列表下架的模型标识(依据 DSH 0.1.6-alpha.2 release notes:
|
|
82
|
+
* 「默认模型列表移除 V4 Flash 和 V4 Flash Vision Exp」)。
|
|
83
|
+
*
|
|
84
|
+
* 用途:用户此前保存过这些模型时,配置仍指向它们,而新版模型目录不再列出 →
|
|
85
|
+
* 需给出可读降级提示(而非静默失败)。按模型名小写子串匹配,
|
|
86
|
+
* 因为 provider 前缀与具体版本后缀(如 -exp)在不同部署下写法不一。
|
|
87
|
+
*/
|
|
88
|
+
export const DELISTED_MODEL_MARKERS: readonly string[] = ['v4-flash']
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 配置指向的模型是否已被官方下架(G7)。
|
|
92
|
+
*
|
|
93
|
+
* 语义:仅当确实指定了 model(非空 = 非自动路由)且命中下架标识时返回 true。
|
|
94
|
+
* 空 model 表示「自动取首个可用模型」,不构成问题,故不提示。
|
|
95
|
+
* @author ddj 2026年09月18号
|
|
96
|
+
* @param model 配置中的模型标识(可空)
|
|
97
|
+
* @returns 是否已下架
|
|
98
|
+
*/
|
|
99
|
+
export function isDelistedModel(model: unknown): boolean {
|
|
100
|
+
const text = typeof model === 'string' ? model.trim().toLowerCase() : ''
|
|
101
|
+
if (!text) return false
|
|
102
|
+
return DELISTED_MODEL_MARKERS.some((marker) => text.includes(marker))
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
/**
|
|
81
106
|
* 按 AI_PREFIX_MAX/AI_SUFFIX_MAX 裁剪前后缀(client 侧发请求前调用)。
|
|
82
107
|
* @author ddj
|
package/src/shared/rpc.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules
|
|
|
19
19
|
import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
20
20
|
import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
|
|
21
21
|
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
22
|
+
import type { SvnAction, SvnChangeEntry, SvnDiffRevResult, SvnLogEntry, SvnStatusPayload, SvnUpdateResult } from './svn.js'
|
|
23
|
+
import type { LoggerLevel } from './logger.js'
|
|
22
24
|
|
|
23
25
|
/** webServer 精确路由。 */
|
|
24
26
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -173,6 +175,22 @@ export interface DecideResult {
|
|
|
173
175
|
stale?: boolean
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/** 一份诊断日志文件的清单项(edrv.dlog.list;name 即请求回传用的 file 参数)。 */
|
|
179
|
+
export interface DebugLogFileInfo {
|
|
180
|
+
name: string
|
|
181
|
+
bytes: number
|
|
182
|
+
mtimeMs: number
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 诊断日志读取结果(edrv.dlog.read;tail 语义,truncated = 已按 maxBytes 截头保尾)。 */
|
|
186
|
+
export interface DebugLogRead {
|
|
187
|
+
name: string
|
|
188
|
+
path: string
|
|
189
|
+
content: string
|
|
190
|
+
size: number
|
|
191
|
+
truncated: boolean
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
/** 每个方法的请求参数(sessionId 为公共可选字段)。 */
|
|
177
195
|
export interface RpcRequestMap {
|
|
178
196
|
'edrv.list': { sessionId?: string; callIds?: string[]; skipStale?: boolean }
|
|
@@ -187,7 +205,11 @@ export interface RpcRequestMap {
|
|
|
187
205
|
'edrv.archiveList': { sessionId?: string }
|
|
188
206
|
'edrv.archiveRead': { sessionId?: string; path?: string }
|
|
189
207
|
'edrv.rollback': { sessionId?: string; path: string; batch?: number }
|
|
190
|
-
'edrv.debug': { sessionId?: string; text: string }
|
|
208
|
+
'edrv.debug': { sessionId?: string; text: string; level?: LoggerLevel }
|
|
209
|
+
'edrv.dlog.list': { sessionId?: string }
|
|
210
|
+
'edrv.dlog.read': { sessionId?: string; file?: string; maxBytes?: number }
|
|
211
|
+
'edrv.dlog.clear': { sessionId?: string; file?: string }
|
|
212
|
+
'edrv.dlog.reveal': { sessionId?: string }
|
|
191
213
|
'edrv.searchFiles': { sessionId?: string; query: string }
|
|
192
214
|
'edrv.searchContent': { sessionId?: string; query: string; matchCase?: boolean; wholeWord?: boolean; regex?: boolean; maxResults?: number; include?: string[]; exclude?: string[] }
|
|
193
215
|
'edrv.listDir': { sessionId?: string; path: string; force?: boolean }
|
|
@@ -263,6 +285,19 @@ export interface RpcRequestMap {
|
|
|
263
285
|
'snippets.save': SnippetSaveInput
|
|
264
286
|
'snippets.remove': SnippetRefInput
|
|
265
287
|
'snippets.entries': { sessionId?: string }
|
|
288
|
+
'svn.status': { sessionId?: string; force?: boolean }
|
|
289
|
+
'svn.changes': { sessionId?: string; force?: boolean }
|
|
290
|
+
'svn.diffBase': { sessionId?: string; path: string }
|
|
291
|
+
'svn.revert': { sessionId?: string; paths: string[] }
|
|
292
|
+
'svn.add': { sessionId?: string; paths: string[] }
|
|
293
|
+
'svn.log': { sessionId?: string; path?: string; limit?: number; stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }
|
|
294
|
+
'svn.diffRev': { sessionId?: string; path: string; revision: number }
|
|
295
|
+
'svn.diffPair': { sessionId?: string; path: string; revA: number; revB: number }
|
|
296
|
+
'svn.diffWorking': { sessionId?: string; path: string; revision: number }
|
|
297
|
+
'svn.wcRev': { sessionId?: string; path?: string }
|
|
298
|
+
'svn.cleanup': { sessionId?: string; removeUnversioned?: boolean; removeIgnored?: boolean }
|
|
299
|
+
'svn.update': { sessionId?: string; path?: string }
|
|
300
|
+
'svn.tortoise': { sessionId?: string; action: SvnAction; path?: string }
|
|
266
301
|
}
|
|
267
302
|
|
|
268
303
|
export type RpcMethod = keyof RpcRequestMap
|
|
@@ -282,6 +317,10 @@ export interface RpcOkMap {
|
|
|
282
317
|
'edrv.archiveRead': { batches: ArchiveBatch[] }
|
|
283
318
|
'edrv.rollback': { path: string; batch: number | null }
|
|
284
319
|
'edrv.debug': object
|
|
320
|
+
'edrv.dlog.list': { root: string; files: DebugLogFileInfo[]; current: string | null }
|
|
321
|
+
'edrv.dlog.read': DebugLogRead
|
|
322
|
+
'edrv.dlog.clear': { name: string }
|
|
323
|
+
'edrv.dlog.reveal': { opened: boolean }
|
|
285
324
|
'edrv.searchFiles': { files: string[]; truncated: boolean }
|
|
286
325
|
'edrv.searchContent': { matches: SearchContentMatch[]; truncated: boolean; warning?: string }
|
|
287
326
|
'edrv.listDir': { root: string; path: string; entries: TreeEntry[] }
|
|
@@ -357,6 +396,19 @@ export interface RpcOkMap {
|
|
|
357
396
|
'snippets.save': { file: SnippetInfo }
|
|
358
397
|
'snippets.remove': object
|
|
359
398
|
'snippets.entries': { entries: SnippetEntry[] }
|
|
399
|
+
'svn.status': SvnStatusPayload
|
|
400
|
+
'svn.changes': { wcRoot: string; entries: SvnChangeEntry[]; truncated: boolean }
|
|
401
|
+
'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string }
|
|
402
|
+
'svn.revert': { count: number; summary: string; output: string }
|
|
403
|
+
'svn.add': { count: number; summary: string; output: string }
|
|
404
|
+
'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
|
|
405
|
+
'svn.diffRev': SvnDiffRevResult
|
|
406
|
+
'svn.diffPair': SvnDiffRevResult
|
|
407
|
+
'svn.diffWorking': SvnDiffRevResult
|
|
408
|
+
'svn.wcRev': { revision: number | null }
|
|
409
|
+
'svn.cleanup': { summary: string; output: string }
|
|
410
|
+
'svn.update': SvnUpdateResult
|
|
411
|
+
'svn.tortoise': { launched: string }
|
|
360
412
|
}
|
|
361
413
|
|
|
362
414
|
/** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode shared — SVN 能力双面契约(类型/动作表/TortoiseProc argv/变更纯函数)。
|
|
3
|
+
* 底座:svn CLI(跨平台);TortoiseProc 对话框为 Windows 过渡增强(自研 UI 就绪后按能力标志隐藏)。
|
|
4
|
+
* P2 增补:工作副本变更条目类型与显隐/徽标纯函数(`svn status --xml` 解析结果的消费口径)。
|
|
5
|
+
* Purity rule: no node/react imports(对齐 rpc.ts/types.ts)。
|
|
6
|
+
* 作者 ddj 2026年09月16号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** TortoiseSVN 过渡动作(P1 菜单/命令面;后续阶段由自研能力逐项替换)。 */
|
|
10
|
+
export type SvnAction = 'update' | 'commit' | 'log' | 'diff' | 'blame' | 'revert'
|
|
11
|
+
|
|
12
|
+
/** svn CLI 缺省可执行名(未配置 svnPath 时从 PATH 解析)。 */
|
|
13
|
+
export const SVN_CLI_DEFAULT = 'svn'
|
|
14
|
+
|
|
15
|
+
/** TortoiseSVN 缺省安装目录(Windows;对齐 SvnHelper 的 TortoiseSVN.path 默认值)。 */
|
|
16
|
+
export const TORTOISE_DIR_DEFAULT = 'C:\\Program Files\\TortoiseSVN'
|
|
17
|
+
|
|
18
|
+
/** TortoiseSVN 主程序名。 */
|
|
19
|
+
export const TORTOISE_EXE = 'TortoiseProc.exe'
|
|
20
|
+
|
|
21
|
+
/** SvnAction → TortoiseProc /command: 参数(全部同名,显式映射防拼写漂移)。 */
|
|
22
|
+
export function tortoiseCommandOf(action: SvnAction): string {
|
|
23
|
+
const table: Record<SvnAction, string> = {
|
|
24
|
+
update: 'update',
|
|
25
|
+
commit: 'commit',
|
|
26
|
+
log: 'log',
|
|
27
|
+
diff: 'diff',
|
|
28
|
+
blame: 'blame',
|
|
29
|
+
revert: 'revert',
|
|
30
|
+
}
|
|
31
|
+
return table[action]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* TortoiseProc 启动 argv(纯函数)。
|
|
36
|
+
* Windows CRT 引号规则:node 对含空格参数整体加引号,子进程 CommandLineToArgvW
|
|
37
|
+
* 还原为单参数,TortoiseSVN 解析器取 `/path:` 后全串为路径——无 shell 注入面。
|
|
38
|
+
* @author ddj 2026年09月16号
|
|
39
|
+
* @param exe TortoiseProc.exe 绝对路径
|
|
40
|
+
* @param action Tortoise 动作
|
|
41
|
+
* @param absPath 目标绝对路径(文件/目录)
|
|
42
|
+
* @returns argv 数组(无 shell 插值)
|
|
43
|
+
*/
|
|
44
|
+
export function tortoiseLaunchArgv(exe: string, action: SvnAction, absPath: string): string[] {
|
|
45
|
+
return [exe, '/command:' + tortoiseCommandOf(action), '/path:' + absPath]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 自研 SVN 能力标志(用于「自研替换 Tortoise 项」的显隐推进)。 */
|
|
49
|
+
export type SvnFeature = 'diff' | 'revert' | 'log' | 'commit' | 'blame' | 'update'
|
|
50
|
+
|
|
51
|
+
/** SVN 能力状态(svn.status 载荷;client 显隐判定唯一数据源)。 */
|
|
52
|
+
export interface SvnStatusPayload {
|
|
53
|
+
/** 工作区是否位于 SVN 工作副本内(cwd 向上找 .svn)。 */
|
|
54
|
+
managed: boolean
|
|
55
|
+
/** 工作副本根目录绝对路径(未受管理为 null)。 */
|
|
56
|
+
wcRoot: string | null
|
|
57
|
+
/** svn CLI 是否可用(`svn --version --quiet` 退出码 0)。 */
|
|
58
|
+
svnCli: boolean
|
|
59
|
+
/** TortoiseProc.exe 是否可用(仅 Windows 可能为 true)。 */
|
|
60
|
+
tortoise: boolean
|
|
61
|
+
/**
|
|
62
|
+
* 已由**自研界面**提供等价能力的功能集合(能力标志)。
|
|
63
|
+
*
|
|
64
|
+
* 推进口径见 `plans/svn-integration/01-infra-visibility.md` 的「自研替换时间线」:
|
|
65
|
+
* 自研能力上线后,对应 TortoiseProc 项自动隐藏(避免同名项并列、误点到官方弹窗)。
|
|
66
|
+
* 缺省 `[]`(旧 payload/未探测时)→ 不隐藏任何 Tortoise 项,保持降级可用。
|
|
67
|
+
*/
|
|
68
|
+
svnFeatures?: readonly SvnFeature[]
|
|
69
|
+
/** 实际使用的 svn CLI(探测依据,诊断用)。 */
|
|
70
|
+
svnPath: string
|
|
71
|
+
/** 实际解析到的 TortoiseProc.exe(诊断用;不可用为空串)。 */
|
|
72
|
+
tortoiseExe: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** update 结果行的动作(对齐 `svn help update` 的字母表)。 */
|
|
76
|
+
export type SvnUpdateAction = 'A' | 'U' | 'D' | 'G' | 'C' | 'E' | 'R'
|
|
77
|
+
|
|
78
|
+
/** 一条 update 条目(path 为 svn 输出的原始形态)。 */
|
|
79
|
+
export interface SvnUpdateEntry {
|
|
80
|
+
/** 动作字母。 */
|
|
81
|
+
action: SvnUpdateAction
|
|
82
|
+
/** 目标路径(svn 原样输出)。 */
|
|
83
|
+
path: string
|
|
84
|
+
/** 中文说明(面板展示;未知字母回落为字母本身)。 */
|
|
85
|
+
label: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** svn.update 结果(条目级 + 宽松解析;output 始终带原文兜底)。 */
|
|
89
|
+
export interface SvnUpdateResult {
|
|
90
|
+
/** 一行摘要(状态栏展示)。 */
|
|
91
|
+
summary: string
|
|
92
|
+
/** 解析到的目标修订版号(解析失败缺省)。 */
|
|
93
|
+
revision?: number
|
|
94
|
+
/** 冲突文件清单(宽松解析,上限截断)。 */
|
|
95
|
+
conflicts: string[]
|
|
96
|
+
/** 条目级明细(update 无 --xml,由文本行解析;解析不出时为空数组)。 */
|
|
97
|
+
entries: SvnUpdateEntry[]
|
|
98
|
+
/** 原始输出(stdout+stderr 合并,诊断与兜底展示)。 */
|
|
99
|
+
output: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** revert/add 等批量动作结果(计数 + 摘要 + 原文)。 */
|
|
103
|
+
export interface SvnActionResult {
|
|
104
|
+
/** 处理条目数(宽松计数)。 */
|
|
105
|
+
count: number
|
|
106
|
+
/** 一行摘要(含失败原因兜底)。 */
|
|
107
|
+
summary: string
|
|
108
|
+
/** 原始输出。 */
|
|
109
|
+
output: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --region P2 工作副本变更(svn status --xml 消费口径)
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 工作副本条目状态(svn status `item` 属性全集)。
|
|
116
|
+
* 取值范围与 `svn help status` 的首列字母表一一对应;'normal'/'external' 仅在返回
|
|
117
|
+
* 自身条目时出现(根条目、external 目录),不属于「变更」。
|
|
118
|
+
*/
|
|
119
|
+
export type SvnItemStatus =
|
|
120
|
+
| 'normal' | 'added' | 'deleted' | 'replaced' | 'conflicted'
|
|
121
|
+
| 'missing' | 'unversioned' | 'obstructed' | 'ignored' | 'modified'
|
|
122
|
+
| 'external' | 'incomplete'
|
|
123
|
+
|
|
124
|
+
// --region P3 日志与更新结果
|
|
125
|
+
|
|
126
|
+
/** 日志条目里单个被改动的路径(对齐 `svn log -v` 的 `<path>` 节点)。 */
|
|
127
|
+
export interface SvnLogPath {
|
|
128
|
+
/** 仓库绝对路径(`/trunk/sub/x.txt`;host 会额外提供映射后的工作区相对路径)。 */
|
|
129
|
+
path: string
|
|
130
|
+
/** 映射到工作区内的相对路径;仓库前缀之外(如分支/外部)为 null。 */
|
|
131
|
+
relPath: string | null
|
|
132
|
+
/** 动作字母:A 新增 / D 删除 / R 替换 / M 修改。 */
|
|
133
|
+
action: 'A' | 'D' | 'R' | 'M'
|
|
134
|
+
/** 条目类型(file / dir)。 */
|
|
135
|
+
kind?: string
|
|
136
|
+
/** 复制来源路径(`svn copy` 记录,仓库绝对路径)。 */
|
|
137
|
+
copyFrom?: string
|
|
138
|
+
/** 复制来源版本。 */
|
|
139
|
+
copyFromRev?: number
|
|
140
|
+
/** 属性改动(`prop-mods`)。 */
|
|
141
|
+
propMods?: boolean
|
|
142
|
+
/** 内容改动(`text-mods`)。 */
|
|
143
|
+
textMods?: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 一条提交日志(对齐 `svn log --xml -v` 的 `<logentry>`)。 */
|
|
147
|
+
export interface SvnLogEntry {
|
|
148
|
+
/** 修订版号。 */
|
|
149
|
+
revision: number
|
|
150
|
+
/** 提交者(可能为空)。 */
|
|
151
|
+
author: string
|
|
152
|
+
/** 提交时间(ISO8601 原文)。 */
|
|
153
|
+
date: string
|
|
154
|
+
/** 提交信息(保留换行)。 */
|
|
155
|
+
message: string
|
|
156
|
+
/** 该版本的变更路径(`-v` 才有)。 */
|
|
157
|
+
paths: SvnLogPath[]
|
|
158
|
+
/**
|
|
159
|
+
* 被合并进本版本的修订(P1-5,仅 `-g`/`--use-merge-history` 才有)。
|
|
160
|
+
*
|
|
161
|
+
* 实测(§6 M1,svn 1.14.5):`-g` 时被合并的修订以**嵌套 `<logentry>`** 形式出现在父条目
|
|
162
|
+
* `</msg>` 之后,同一父条目下可并列多个;嵌套条目的 `<paths>` 是**源分支路径**,
|
|
163
|
+
* `mapRepoPath` 会判为仓库外(relPath=null)——UI 按「仓库外」展示,不得误映射。
|
|
164
|
+
* 缺省 undefined = 未勾选 `-g` 或该修订无合并历史。
|
|
165
|
+
*/
|
|
166
|
+
merged?: SvnLogEntry[]
|
|
167
|
+
/**
|
|
168
|
+
* 是否为反向合并(`reverse-merge="true"`;仅嵌套条目可能带,顶层从不带)。
|
|
169
|
+
* 实测只采到 `reverse-merge="false"` 样本,故按「可能存在 true」处理。
|
|
170
|
+
*/
|
|
171
|
+
reverseMerge?: boolean
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 日志动作 → 中文说明。 */
|
|
175
|
+
export const SVN_LOG_ACTION_LABEL: Record<string, string> = {
|
|
176
|
+
A: '新增',
|
|
177
|
+
D: '删除',
|
|
178
|
+
R: '替换',
|
|
179
|
+
M: '修改',
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 版本差异结果(左右两侧内容;左侧为 null 表示该版本尚无此文件)。 */
|
|
183
|
+
export interface SvnDiffRevResult {
|
|
184
|
+
/** 左(revision-1)侧内容;新增文件/版本 1 时为 null。 */
|
|
185
|
+
left: string | null
|
|
186
|
+
/** 右(revision)侧内容。 */
|
|
187
|
+
right: string | null
|
|
188
|
+
/** left 缺失原因('not-exist' = 该版本无此路径)。 */
|
|
189
|
+
reason?: string
|
|
190
|
+
/** 失败提示(诊断用)。 */
|
|
191
|
+
error?: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --endregion
|
|
195
|
+
|
|
196
|
+
/** 一条工作副本变更(path 相对工作副本根、`/` 分隔;host 已归一)。 */
|
|
197
|
+
export interface SvnChangeEntry {
|
|
198
|
+
/** 工作区/工作副本相对路径(`/` 分隔,无前导 `./`)。 */
|
|
199
|
+
path: string
|
|
200
|
+
/** 条目状态。 */
|
|
201
|
+
status: SvnItemStatus
|
|
202
|
+
/** 属性(svn:*) 修改状态:'modified'/'conflicted' 时非空(展示用)。 */
|
|
203
|
+
props?: 'none' | 'modified' | 'conflicted'
|
|
204
|
+
/** 所属 changelist 名(未分组为空)。 */
|
|
205
|
+
changelist?: string
|
|
206
|
+
/** 是否受版本控制(unversioned/ignored 为 false)。 */
|
|
207
|
+
versioned: boolean
|
|
208
|
+
/** 工作副本修订版号(`-1` = 新增未提交,展示时忽略)。 */
|
|
209
|
+
revision?: number
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 条目状态 → 展示字母(对齐 `svn status` 首列,VSCode 扩展同口径)。 */
|
|
213
|
+
export const SVN_STATUS_LETTER: Record<SvnItemStatus, string> = {
|
|
214
|
+
normal: ' ',
|
|
215
|
+
added: 'A',
|
|
216
|
+
deleted: 'D',
|
|
217
|
+
replaced: 'R',
|
|
218
|
+
conflicted: 'C',
|
|
219
|
+
missing: '!',
|
|
220
|
+
unversioned: '?',
|
|
221
|
+
obstructed: '~',
|
|
222
|
+
ignored: 'I',
|
|
223
|
+
modified: 'M',
|
|
224
|
+
external: 'X',
|
|
225
|
+
incomplete: '!',
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 条目状态 → 中文说明(徽标 tooltip / 面板行 tooltip)。 */
|
|
229
|
+
export const SVN_STATUS_LABEL: Record<SvnItemStatus, string> = {
|
|
230
|
+
normal: '未修改',
|
|
231
|
+
added: '已添加(待提交)',
|
|
232
|
+
deleted: '已删除(待提交)',
|
|
233
|
+
replaced: '已替换',
|
|
234
|
+
conflicted: '冲突',
|
|
235
|
+
missing: '缺失(已被外部删除)',
|
|
236
|
+
unversioned: '未纳入版本控制',
|
|
237
|
+
obstructed: '类型冲突(被同名的其他类型占用)',
|
|
238
|
+
ignored: '已被忽略',
|
|
239
|
+
modified: '已修改',
|
|
240
|
+
external: '外部定义',
|
|
241
|
+
incomplete: '不完整(需 cleanup)',
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 条目状态 → 颜色分级(对齐 TortoiseSVN「Check for Modifications」配色口径:
|
|
246
|
+
* 本地修改=蓝、新增=紫、删除/缺失=暗红、冲突=亮红、未版本控制/忽略=中性)。
|
|
247
|
+
*/
|
|
248
|
+
export type SvnStatusTone = 'modified' | 'added' | 'deleted' | 'conflict' | 'plain'
|
|
249
|
+
|
|
250
|
+
/** 条目状态 → 颜色分级。 */
|
|
251
|
+
export const SVN_STATUS_TONE: Record<SvnItemStatus, SvnStatusTone> = {
|
|
252
|
+
normal: 'plain',
|
|
253
|
+
added: 'added',
|
|
254
|
+
deleted: 'deleted',
|
|
255
|
+
replaced: 'added',
|
|
256
|
+
conflicted: 'conflict',
|
|
257
|
+
missing: 'deleted',
|
|
258
|
+
unversioned: 'plain',
|
|
259
|
+
obstructed: 'conflict',
|
|
260
|
+
ignored: 'plain',
|
|
261
|
+
modified: 'modified',
|
|
262
|
+
external: 'plain',
|
|
263
|
+
incomplete: 'deleted',
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 是否属于「工作副本变更」(`normal`/`external` 不算;`ignored` 由开关决定是否收录)。
|
|
268
|
+
* @author ddj 2026年09月16号
|
|
269
|
+
* @param entry 变更条目
|
|
270
|
+
* @returns 是否计入变更清单
|
|
271
|
+
*/
|
|
272
|
+
export function isSvnChange(entry: SvnChangeEntry): boolean {
|
|
273
|
+
if (entry.status === 'normal' || entry.status === 'external') return false
|
|
274
|
+
if (entry.status === 'ignored') return false
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 面板「显示」开关(未版本控制/忽略项默认关掉忽略项、保留未版本控制)。 */
|
|
279
|
+
export interface SvnChangeFilter {
|
|
280
|
+
/** 是否显示未纳入版本控制的条目(默认 true)。 */
|
|
281
|
+
unversioned?: boolean
|
|
282
|
+
/** 是否显示已被忽略的条目(默认 false)。 */
|
|
283
|
+
ignored?: boolean
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 按开关过滤变更清单(过滤规则唯一出口,面板/徽标共用,避免两处漂移)。
|
|
288
|
+
* @author ddj 2026年09月16号
|
|
289
|
+
* @param entries 变更清单(host 已按 `--no-ignore` 取全集)
|
|
290
|
+
* @param filter 显示开关
|
|
291
|
+
* @returns 过滤后的清单(保持原顺序)
|
|
292
|
+
*/
|
|
293
|
+
export function svnVisibleChanges(
|
|
294
|
+
entries: readonly SvnChangeEntry[],
|
|
295
|
+
filter: SvnChangeFilter = {},
|
|
296
|
+
): SvnChangeEntry[] {
|
|
297
|
+
const showUnversioned = filter.unversioned !== false
|
|
298
|
+
const showIgnored = filter.ignored === true
|
|
299
|
+
return entries.filter((entry) => {
|
|
300
|
+
if (entry.status === 'ignored') return showIgnored
|
|
301
|
+
if (entry.status === 'unversioned') return showUnversioned
|
|
302
|
+
return isSvnChange(entry)
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 条目 → 文件树行尾徽标字母(无可展示徽标返回空串)。
|
|
308
|
+
* 与 svnVisibleChanges 同口径:未版本控制/忽略项由开关决定,故此处直接按状态给出字母,
|
|
309
|
+
* 由调用方决定是否已过滤。
|
|
310
|
+
* @author ddj 2026年09月16号
|
|
311
|
+
* @param entry 变更条目
|
|
312
|
+
* @returns 单字母徽标;`normal`/`external` 返回空串
|
|
313
|
+
*/
|
|
314
|
+
export function svnBadgeOf(entry: SvnChangeEntry): string {
|
|
315
|
+
if (entry.status === 'normal' || entry.status === 'external') return ''
|
|
316
|
+
return SVN_STATUS_LETTER[entry.status]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 条目 → 徽标/行 tooltip 文案(`M 已修改` 形式)。
|
|
321
|
+
* @author ddj 2026年09月16号
|
|
322
|
+
* @param entry 变更条目
|
|
323
|
+
* @returns 展示文案;无徽标时返回空串
|
|
324
|
+
*/
|
|
325
|
+
export function svnBadgeTitle(entry: SvnChangeEntry): string {
|
|
326
|
+
const letter = svnBadgeOf(entry)
|
|
327
|
+
if (!letter) return ''
|
|
328
|
+
return letter + ' ' + SVN_STATUS_LABEL[entry.status]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --endregion
|
|
332
|
+
|
|
333
|
+
// --region P2 动作文案与判定
|
|
334
|
+
|
|
335
|
+
/** 与基线比较(`svn cat -r BASE` 差异视图)。 */
|
|
336
|
+
export const SVN_DIFF_BASE_LABEL = '与基线比较'
|
|
337
|
+
/** 加入版本控制(`svn add`)。 */
|
|
338
|
+
export const SVN_ADD_LABEL = '加入版本控制'
|
|
339
|
+
/** 还原(`svn revert -R`;CLI 全平台,区别于 TortoiseSVN 还原)。 */
|
|
340
|
+
export const SVN_REVERT_LABEL = 'SVN 还原'
|
|
341
|
+
/** 刷新变更(强制重查 `svn status --xml`)。 */
|
|
342
|
+
export const SVN_REFRESH_LABEL = '刷新变更'
|
|
343
|
+
|
|
344
|
+
/** 基线差异可打开的路径判据(需为可读文本文件;二进制/图片/PDF 不支持)。 */
|
|
345
|
+
const SVN_BINARY_EXT = new Set([
|
|
346
|
+
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'avif', 'pdf', 'zip', 'gz', '7z', 'rar',
|
|
347
|
+
'exe', 'dll', 'so', 'dylib', 'class', 'jar', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'mp4',
|
|
348
|
+
])
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 是否可打开基线差异(受版本控制 + 非二进制扩展名)。
|
|
352
|
+
* `added`/`unversioned` 无 BASE 内容,但 `added` 仍允许调用(host 会返回 no-pristine 原因)。
|
|
353
|
+
* @author ddj 2026年09月16号
|
|
354
|
+
* @param path 目标路径
|
|
355
|
+
* @param status 该条目的状态(缺省表示「不在变更清单里」,按受版本控制的已修改处理)
|
|
356
|
+
* @returns 是否可打开基线差异
|
|
357
|
+
*/
|
|
358
|
+
export function isSvnDiffable(path: string, status?: SvnItemStatus): boolean {
|
|
359
|
+
if (status === 'unversioned' || status === 'ignored') return false
|
|
360
|
+
const base = String(path ?? '').split(/[\\/]/).pop() ?? ''
|
|
361
|
+
const dot = base.lastIndexOf('.')
|
|
362
|
+
if (dot <= 0) return true
|
|
363
|
+
return !SVN_BINARY_EXT.has(base.slice(dot + 1).toLowerCase())
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** 菜单/页签共用的动作文案(避免 client 侧两处漂移)。 */
|
|
367
|
+
export const SVN_UPDATE_LABEL = 'SVN 更新'
|
|
368
|
+
|
|
369
|
+
/** Tortoise 菜单文案表(按动作)。 */
|
|
370
|
+
export const SVN_TORTOISE_LABELS: Record<SvnAction, string> = {
|
|
371
|
+
update: 'TortoiseSVN 更新',
|
|
372
|
+
commit: 'TortoiseSVN 提交',
|
|
373
|
+
log: 'TortoiseSVN 日志',
|
|
374
|
+
diff: 'TortoiseSVN 差异',
|
|
375
|
+
blame: 'TortoiseSVN 追溯',
|
|
376
|
+
revert: 'TortoiseSVN 还原',
|
|
377
|
+
}
|