dsh-vscode-mode 0.15.0 → 0.15.2
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 +5 -3
- package/lib/client.js +283 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +217 -5
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/styles/editor.css +4 -0
- package/src/client/svnNotice.ts +1 -1
- package/src/client/ui/SvnUpdateDialog.ts +143 -29
- package/src/client/ui/svnUpdateModel.ts +45 -1
- package/src/shared/rpc.ts +5 -1
- package/src/shared/svn.ts +10 -0
- package/src/svn.ts +70 -5
- package/src/svnResolve.ts +33 -0
package/lib/index.js
CHANGED
|
@@ -18681,6 +18681,46 @@ var SvnUpdateJobs = class {
|
|
|
18681
18681
|
}
|
|
18682
18682
|
};
|
|
18683
18683
|
//#endregion
|
|
18684
|
+
//#region src/svnResolve.ts
|
|
18685
|
+
/** @public @author ddj 2026年10月14号 @param value Requested strategy. @returns Whether SVN accepts this explicit strategy. */
|
|
18686
|
+
function validAccept(value) {
|
|
18687
|
+
return value === "mine-full" || value === "theirs-full" || value === "working";
|
|
18688
|
+
}
|
|
18689
|
+
/** @public @author ddj 2026年10月14号 @param value Untrusted relative target. @returns Whether the path names exactly one non-root workspace target. */
|
|
18690
|
+
function validTarget(value) {
|
|
18691
|
+
if (typeof value !== "string" || !value || /[\u0000-\u001f]/.test(value) || /^[A-Za-z]:|^[\\/]/.test(value)) return false;
|
|
18692
|
+
return value.replace(/\\/g, "/").split("/").every((part) => Boolean(part) && part !== "." && part !== ".." && part.toLowerCase() !== ".svn");
|
|
18693
|
+
}
|
|
18694
|
+
/** @public @author ddj 2026年10月14号 @param path Exact relative target. @returns Peg-safe SVN argument. */
|
|
18695
|
+
function pegTarget(path) {
|
|
18696
|
+
return path.includes("@") ? path + "@" : path;
|
|
18697
|
+
}
|
|
18698
|
+
/** @public @author ddj 2026年10月14号 @param xml Verbose depth-empty status. @param path Client target identity. @returns Confirmed conflict evidence. @throws Missing or incomplete status evidence. */
|
|
18699
|
+
function conflictOf(xml, path) {
|
|
18700
|
+
if (!/^\s*(?:<\?xml[^>]*>\s*)?<status\b/.test(xml) || !/<\/status>\s*$/.test(xml)) throw new Error("SVN 冲突状态输出为空或不完整,状态未确认");
|
|
18701
|
+
const statuses = [...scanXmlTags(xml, ["wc-status"])].filter((hit) => !hit.closing);
|
|
18702
|
+
if (statuses.length !== 1) throw new Error("SVN 未返回唯一目标状态,状态未确认");
|
|
18703
|
+
const attrs = statuses[0].attrs;
|
|
18704
|
+
const item = attrOf(attrs, "item");
|
|
18705
|
+
if (!item) throw new Error("SVN 目标状态缺失,状态未确认");
|
|
18706
|
+
return {
|
|
18707
|
+
path,
|
|
18708
|
+
text: item === "conflicted",
|
|
18709
|
+
props: attrOf(attrs, "props") === "conflicted",
|
|
18710
|
+
tree: attrOf(attrs, "tree-conflicted") === "true",
|
|
18711
|
+
versioned: ![
|
|
18712
|
+
"unversioned",
|
|
18713
|
+
"ignored",
|
|
18714
|
+
"external",
|
|
18715
|
+
"none"
|
|
18716
|
+
].includes(item)
|
|
18717
|
+
};
|
|
18718
|
+
}
|
|
18719
|
+
/** @public @author ddj 2026年10月14号 @param state Confirmed target evidence. @returns Whether any conflict remains. */
|
|
18720
|
+
function hasConflict(state) {
|
|
18721
|
+
return state.text || state.props || state.tree;
|
|
18722
|
+
}
|
|
18723
|
+
//#endregion
|
|
18684
18724
|
//#region src/shared/svnExplorer.ts
|
|
18685
18725
|
const ICON_KIND = {
|
|
18686
18726
|
normal: "clean",
|
|
@@ -19694,6 +19734,18 @@ function createSvnRpc(deps) {
|
|
|
19694
19734
|
const now = deps.now ?? (() => Date.now());
|
|
19695
19735
|
const findRoot = deps.findRoot ?? ((cwd) => findSvnRoot(cwd));
|
|
19696
19736
|
const findTortoise = deps.findTortoise ?? ((dir) => findTortoiseProc(dir));
|
|
19737
|
+
const conflictWrites = /* @__PURE__ */ new Set();
|
|
19738
|
+
/** @private @author ddj 2026年10月14号 @param root WC root. @returns Canonical lock identity across sessions. */
|
|
19739
|
+
const writeKey = (root) => (deps.platform ?? process.platform) === "win32" ? root.toLowerCase() : root;
|
|
19740
|
+
/** @private @author ddj 2026年10月14号 @param root WC root. @returns Whether a live update or conflict write owns the WC. */
|
|
19741
|
+
const writeBusy = (root) => {
|
|
19742
|
+
const job = updateJobs.active(root);
|
|
19743
|
+
return conflictWrites.has(writeKey(root)) || Boolean(job && [
|
|
19744
|
+
"starting",
|
|
19745
|
+
"running",
|
|
19746
|
+
"cancelling"
|
|
19747
|
+
].includes(job.phase));
|
|
19748
|
+
};
|
|
19697
19749
|
const statusCache = /* @__PURE__ */ new Map();
|
|
19698
19750
|
/** 运行中更新按会话目录复用工作副本根;TTL 到期才重新扫描 .svn。 */
|
|
19699
19751
|
const updateRootCache = /* @__PURE__ */ new Map();
|
|
@@ -19833,7 +19885,7 @@ function createSvnRpc(deps) {
|
|
|
19833
19885
|
* @param stdoutCap stdout 收集上限(缺省 1MB;大载荷场景如日志拉取需显式放大)
|
|
19834
19886
|
* @returns 退出码与输出
|
|
19835
19887
|
*/
|
|
19836
|
-
const runSvn = async (cwd, argv, graceMs, stdoutCap = OUTPUT_CAP) => {
|
|
19888
|
+
const runSvn = async (cwd, argv, graceMs, stdoutCap = OUTPUT_CAP, allowFallback = true) => {
|
|
19837
19889
|
const spawn = subprocess();
|
|
19838
19890
|
if (!spawn) throw new Error("缺少 subprocess 服务");
|
|
19839
19891
|
const fs = ctx.get("fs");
|
|
@@ -19868,7 +19920,8 @@ function createSvnRpc(deps) {
|
|
|
19868
19920
|
stdout,
|
|
19869
19921
|
stderr
|
|
19870
19922
|
};
|
|
19871
|
-
} catch {
|
|
19923
|
+
} catch (error) {
|
|
19924
|
+
if (!allowFallback) throw error;
|
|
19872
19925
|
const outcome = await attempt("inherit").done;
|
|
19873
19926
|
return {
|
|
19874
19927
|
code: outcome?.exitCode ?? outcome?.code ?? null,
|
|
@@ -19877,6 +19930,22 @@ function createSvnRpc(deps) {
|
|
|
19877
19930
|
};
|
|
19878
19931
|
}
|
|
19879
19932
|
};
|
|
19933
|
+
/** @private @author ddj 2026年10月14号 @param cwd Session workspace. @param root WC root. @param path Validated target. @returns Exact conflict evidence without recursive scanning. */
|
|
19934
|
+
const readConflict = async (cwd, root, path) => {
|
|
19935
|
+
const target = svnPathOf(cwd, root, path);
|
|
19936
|
+
if (!target) throw new Error("目标不在 SVN 工作副本内");
|
|
19937
|
+
const result = await runSvn(root, [
|
|
19938
|
+
"status",
|
|
19939
|
+
"--xml",
|
|
19940
|
+
"--verbose",
|
|
19941
|
+
"--depth",
|
|
19942
|
+
"empty",
|
|
19943
|
+
"--",
|
|
19944
|
+
pegTarget(target)
|
|
19945
|
+
], READ_GRACE_MS, OUTPUT_CAP, false);
|
|
19946
|
+
if (result.code !== 0) throw new Error(result.stderr || result.stdout || "SVN 状态读取失败");
|
|
19947
|
+
return conflictOf(result.stdout, path);
|
|
19948
|
+
};
|
|
19880
19949
|
/** 发射 TortoiseProc(GUI 长驻:不 await done,no-op catch 防未处理 rejection)。 */
|
|
19881
19950
|
const launchTortoise = (exe, action, absPath) => {
|
|
19882
19951
|
const spawn = subprocess();
|
|
@@ -19976,11 +20045,16 @@ function createSvnRpc(deps) {
|
|
|
19976
20045
|
error: "svn 命令不可用(可在设置页配置 svn 路径)"
|
|
19977
20046
|
};
|
|
19978
20047
|
try {
|
|
20048
|
+
const targets = rels.map((path) => svnPathOf(sc.cwd, status.wcRoot, path));
|
|
20049
|
+
if (targets.some((path) => path === null)) return {
|
|
20050
|
+
ok: false,
|
|
20051
|
+
error: "目标不在 SVN 工作副本内"
|
|
20052
|
+
};
|
|
19979
20053
|
const outcome = await runSvn(status.wcRoot, [
|
|
19980
20054
|
...verbArgs,
|
|
19981
20055
|
"--",
|
|
19982
|
-
...
|
|
19983
|
-
], MUTATE_GRACE_MS);
|
|
20056
|
+
...targets.map((path) => pegTarget(path))
|
|
20057
|
+
], MUTATE_GRACE_MS, OUTPUT_CAP, !verbArgs.includes("--depth"));
|
|
19984
20058
|
const result = batchResultOf(outcome.stdout, outcome.stderr, outcome.code, verb);
|
|
19985
20059
|
changesCache.delete(status.wcRoot);
|
|
19986
20060
|
if (outcome.code !== 0) return {
|
|
@@ -20498,8 +20572,142 @@ function createSvnRpc(deps) {
|
|
|
20498
20572
|
};
|
|
20499
20573
|
}
|
|
20500
20574
|
},
|
|
20575
|
+
"svn.conflictStatus": async (args) => {
|
|
20576
|
+
const scope = await updateScope(args.sessionId, true);
|
|
20577
|
+
if ("err" in scope) return {
|
|
20578
|
+
ok: false,
|
|
20579
|
+
error: scope.err
|
|
20580
|
+
};
|
|
20581
|
+
if (!scope.cli) return {
|
|
20582
|
+
ok: false,
|
|
20583
|
+
error: "svn 命令不可用"
|
|
20584
|
+
};
|
|
20585
|
+
if (!Array.isArray(args.paths) || args.paths.length > 64) return {
|
|
20586
|
+
ok: false,
|
|
20587
|
+
error: "冲突路径列表不合法或超限"
|
|
20588
|
+
};
|
|
20589
|
+
if (!args.paths.every(validTarget)) return {
|
|
20590
|
+
ok: false,
|
|
20591
|
+
error: "路径不合法"
|
|
20592
|
+
};
|
|
20593
|
+
const paths = [...new Set(args.paths.map(safeRel))];
|
|
20594
|
+
if (paths.some((path) => !path)) return {
|
|
20595
|
+
ok: false,
|
|
20596
|
+
error: "路径不合法"
|
|
20597
|
+
};
|
|
20598
|
+
try {
|
|
20599
|
+
const states = [];
|
|
20600
|
+
for (const path of paths) states.push(await readConflict(scope.cwd, scope.root, path));
|
|
20601
|
+
return {
|
|
20602
|
+
ok: true,
|
|
20603
|
+
states
|
|
20604
|
+
};
|
|
20605
|
+
} catch (error) {
|
|
20606
|
+
return {
|
|
20607
|
+
ok: false,
|
|
20608
|
+
error: String(error)
|
|
20609
|
+
};
|
|
20610
|
+
}
|
|
20611
|
+
},
|
|
20612
|
+
"svn.resolve": async (args) => {
|
|
20613
|
+
if (!validAccept(args.accept)) return {
|
|
20614
|
+
ok: false,
|
|
20615
|
+
error: "解决策略不合法"
|
|
20616
|
+
};
|
|
20617
|
+
if (!validTarget(args.path)) return {
|
|
20618
|
+
ok: false,
|
|
20619
|
+
error: "路径不合法"
|
|
20620
|
+
};
|
|
20621
|
+
const path = safeRel(args.path);
|
|
20622
|
+
if (!path) return {
|
|
20623
|
+
ok: false,
|
|
20624
|
+
error: "路径不合法"
|
|
20625
|
+
};
|
|
20626
|
+
const scope = await updateScope(args.sessionId, true);
|
|
20627
|
+
if ("err" in scope) return {
|
|
20628
|
+
ok: false,
|
|
20629
|
+
error: scope.err
|
|
20630
|
+
};
|
|
20631
|
+
if (!scope.cli) return {
|
|
20632
|
+
ok: false,
|
|
20633
|
+
error: "svn 命令不可用"
|
|
20634
|
+
};
|
|
20635
|
+
if (writeBusy(scope.root)) return {
|
|
20636
|
+
ok: false,
|
|
20637
|
+
error: "工作副本正在更新或解决冲突,请等待完成"
|
|
20638
|
+
};
|
|
20639
|
+
conflictWrites.add(writeKey(scope.root));
|
|
20640
|
+
try {
|
|
20641
|
+
const evidence = await readConflict(scope.cwd, scope.root, path);
|
|
20642
|
+
if (!hasConflict(evidence)) return {
|
|
20643
|
+
ok: false,
|
|
20644
|
+
error: "目标已无冲突,请刷新状态"
|
|
20645
|
+
};
|
|
20646
|
+
if (evidence.tree && args.accept !== "working") return {
|
|
20647
|
+
ok: false,
|
|
20648
|
+
error: "树冲突不支持通用完整版本选择,请针对性处理"
|
|
20649
|
+
};
|
|
20650
|
+
const target = svnPathOf(scope.cwd, scope.root, path);
|
|
20651
|
+
const outcome = await runSvn(scope.root, [
|
|
20652
|
+
"resolve",
|
|
20653
|
+
"--accept",
|
|
20654
|
+
args.accept,
|
|
20655
|
+
"--depth",
|
|
20656
|
+
"empty",
|
|
20657
|
+
"--",
|
|
20658
|
+
pegTarget(target)
|
|
20659
|
+
], MUTATE_GRACE_MS, OUTPUT_CAP, false);
|
|
20660
|
+
if (outcome.code !== 0) return {
|
|
20661
|
+
ok: false,
|
|
20662
|
+
error: outcome.stderr || outcome.stdout || "SVN 解决冲突失败"
|
|
20663
|
+
};
|
|
20664
|
+
return {
|
|
20665
|
+
ok: true,
|
|
20666
|
+
summary: "SVN 解决命令已执行,请核对剩余冲突",
|
|
20667
|
+
output: outcome.stdout
|
|
20668
|
+
};
|
|
20669
|
+
} catch (error) {
|
|
20670
|
+
return {
|
|
20671
|
+
ok: false,
|
|
20672
|
+
error: String(error)
|
|
20673
|
+
};
|
|
20674
|
+
} finally {
|
|
20675
|
+
conflictWrites.delete(writeKey(scope.root));
|
|
20676
|
+
changesCache.delete(scope.root);
|
|
20677
|
+
explorerCache.clear();
|
|
20678
|
+
}
|
|
20679
|
+
},
|
|
20501
20680
|
"svn.revert": async (args) => {
|
|
20502
|
-
return mutate("还原", ["revert", "-R"], args.sessionId, args.paths);
|
|
20681
|
+
if (args.depth === void 0) return mutate("还原", ["revert", "-R"], args.sessionId, args.paths);
|
|
20682
|
+
if (args.depth !== "empty") return {
|
|
20683
|
+
ok: false,
|
|
20684
|
+
error: "还原深度不合法"
|
|
20685
|
+
};
|
|
20686
|
+
if (!Array.isArray(args.paths) || !args.paths.length || !args.paths.every(validTarget)) return {
|
|
20687
|
+
ok: false,
|
|
20688
|
+
error: "路径不合法"
|
|
20689
|
+
};
|
|
20690
|
+
const scope = await updateScope(args.sessionId, true);
|
|
20691
|
+
if ("err" in scope) return {
|
|
20692
|
+
ok: false,
|
|
20693
|
+
error: scope.err
|
|
20694
|
+
};
|
|
20695
|
+
if (writeBusy(scope.root)) return {
|
|
20696
|
+
ok: false,
|
|
20697
|
+
error: "工作副本正在更新或解决冲突,请等待完成"
|
|
20698
|
+
};
|
|
20699
|
+
conflictWrites.add(writeKey(scope.root));
|
|
20700
|
+
try {
|
|
20701
|
+
return await mutate("还原", [
|
|
20702
|
+
"revert",
|
|
20703
|
+
"--depth",
|
|
20704
|
+
"empty"
|
|
20705
|
+
], args.sessionId, args.paths);
|
|
20706
|
+
} finally {
|
|
20707
|
+
conflictWrites.delete(writeKey(scope.root));
|
|
20708
|
+
changesCache.delete(scope.root);
|
|
20709
|
+
explorerCache.clear();
|
|
20710
|
+
}
|
|
20503
20711
|
},
|
|
20504
20712
|
"svn.add": async (args) => {
|
|
20505
20713
|
return mutate("加入版本控制", ["add"], args.sessionId, args.paths);
|
|
@@ -21354,6 +21562,10 @@ function createSvnRpc(deps) {
|
|
|
21354
21562
|
try {
|
|
21355
21563
|
const fs = ctx.get("fs");
|
|
21356
21564
|
const cwd = fs?.resolve && fs?.processPath ? fs.processPath(await fs.resolve(scope.cwd, {})) : scope.cwd;
|
|
21565
|
+
if (conflictWrites.has(writeKey(scope.root))) return {
|
|
21566
|
+
ok: false,
|
|
21567
|
+
error: "工作副本正在解决冲突,请等待完成"
|
|
21568
|
+
};
|
|
21357
21569
|
return {
|
|
21358
21570
|
ok: true,
|
|
21359
21571
|
...updateJobs.start({
|